enhance(backend): improve chart engine

This commit is contained in:
syuilo
2021-12-14 18:12:37 +09:00
parent d95fafb5b3
commit 0be4e10462
36 changed files with 604 additions and 248 deletions

View File

@ -3,10 +3,10 @@ const types = require('pg').types;
types.setTypeParser(20, Number);
import { createConnection, Logger, getConnection } from 'typeorm';
import config from '@/config/index';
import { entities as charts } from '@/services/chart/entities';
import { dbLogger } from './logger';
import * as highlight from 'cli-highlight';
import config from '@/config/index';
import { dbLogger } from './logger';
import { User } from '@/models/entities/user';
import { DriveFile } from '@/models/entities/drive-file';
@ -74,6 +74,8 @@ import { Ad } from '@/models/entities/ad';
import { PasswordResetRequest } from '@/models/entities/password-reset-request';
import { UserPending } from '@/models/entities/user-pending';
import { entities as charts } from '@/services/chart/entities';
const sqlLogger = dbLogger.createSubLogger('sql', 'white', false);
class MyCustomLogger implements Logger {
@ -175,7 +177,7 @@ export const entities = [
Ad,
PasswordResetRequest,
UserPending,
...charts as any,
...charts,
];
export function initDb(justBorrow = false, sync = false, forceRecreate = false) {

View File

@ -1,12 +1,16 @@
import autobind from 'autobind-decorator';
import Chart, { Obj, DeepPartial } from '../../core';
import Chart, { Obj, DeepPartial } from '../core';
import { User } from '@/models/entities/user';
import { SchemaType } from '@/misc/schema';
import { Users } from '@/models/index';
import { name, schema } from '../schemas/active-users';
import { name, schema } from './entities/active-users';
type ActiveUsersLog = SchemaType<typeof schema>;
/**
*
*/
// eslint-disable-next-line import/no-default-export
export default class ActiveUsersChart extends Chart<ActiveUsersLog> {
constructor() {
super(name, schema);
@ -35,7 +39,7 @@ export default class ActiveUsersChart extends Chart<ActiveUsersLog> {
}
@autobind
public async update(user: { id: User['id'], host: User['host'] }) {
public async update(user: { id: User['id'], host: User['host'] }): Promise<void> {
const update: Obj = {
users: [user.id],
};

View File

@ -1,13 +1,17 @@
import autobind from 'autobind-decorator';
import Chart, { Obj, DeepPartial } from '../../core';
import Chart, { Obj, DeepPartial } from '../core';
import { SchemaType } from '@/misc/schema';
import { DriveFiles } from '@/models/index';
import { Not, IsNull } from 'typeorm';
import { DriveFile } from '@/models/entities/drive-file';
import { name, schema } from '../schemas/drive';
import { name, schema } from './entities/drive';
type DriveLog = SchemaType<typeof schema>;
/**
*
*/
// eslint-disable-next-line import/no-default-export
export default class DriveChart extends Chart<DriveLog> {
constructor() {
super(name, schema);
@ -71,7 +75,7 @@ export default class DriveChart extends Chart<DriveLog> {
}
@autobind
public async update(file: DriveFile, isAdditional: boolean) {
public async update(file: DriveFile, isAdditional: boolean): Promise<void> {
const update: Obj = {};
update.totalCount = isAdditional ? 1 : -1;

View File

@ -1,4 +1,8 @@
export const logSchema = {
import Chart from '../../core';
export const name = 'activeUsers';
const logSchema = {
/**
*
*/
@ -12,9 +16,6 @@ export const logSchema = {
},
};
/**
*
*/
export const schema = {
type: 'object' as const,
optional: false as const, nullable: false as const,
@ -32,4 +33,4 @@ export const schema = {
},
};
export const name = 'activeUsers';
export const entity = Chart.schemaToEntity(name, schema);

View File

@ -1,3 +1,7 @@
import Chart from '../../core';
export const name = 'drive';
const logSchema = {
/**
*
@ -65,4 +69,4 @@ export const schema = {
},
};
export const name = 'drive';
export const entity = Chart.schemaToEntity(name, schema);

View File

@ -1,6 +1,7 @@
/**
*
*/
import Chart from '../../core';
export const name = 'federation';
export const schema = {
type: 'object' as const,
optional: false as const, nullable: false as const,
@ -26,4 +27,4 @@ export const schema = {
},
};
export const name = 'federation';
export const entity = Chart.schemaToEntity(name, schema);

View File

@ -1,4 +1,8 @@
export const logSchema = {
import Chart from '../../core';
export const name = 'hashtag';
const logSchema = {
/**
* 稿
*/
@ -12,9 +16,6 @@ export const logSchema = {
},
};
/**
*
*/
export const schema = {
type: 'object' as const,
optional: false as const, nullable: false as const,
@ -32,4 +33,4 @@ export const schema = {
},
};
export const name = 'hashtag';
export const entity = Chart.schemaToEntity(name, schema, true);

View File

@ -1,6 +1,7 @@
/**
*
*/
import Chart from '../../core';
export const name = 'instance';
export const schema = {
type: 'object' as const,
optional: false as const, nullable: false as const,
@ -154,4 +155,4 @@ export const schema = {
},
};
export const name = 'instance';
export const entity = Chart.schemaToEntity(name, schema, true);

View File

@ -1,6 +1,7 @@
/**
*
*/
import Chart from '../../core';
export const name = 'network';
export const schema = {
type: 'object' as const,
optional: false as const, nullable: false as const,
@ -28,4 +29,4 @@ export const schema = {
},
};
export const name = 'network';
export const entity = Chart.schemaToEntity(name, schema);

View File

@ -1,3 +1,7 @@
import Chart from '../../core';
export const name = 'notes';
const logSchema = {
total: {
type: 'number' as const,
@ -53,4 +57,4 @@ export const schema = {
},
};
export const name = 'notes';
export const entity = Chart.schemaToEntity(name, schema);

View File

@ -1,3 +1,7 @@
import Chart from '../../core';
export const name = 'perUserDrive';
export const schema = {
type: 'object' as const,
optional: false as const, nullable: false as const,
@ -52,4 +56,4 @@ export const schema = {
},
};
export const name = 'perUserDrive';
export const entity = Chart.schemaToEntity(name, schema, true);

View File

@ -1,4 +1,8 @@
export const logSchema = {
import Chart from '../../core';
export const name = 'perUserFollowing';
const logSchema = {
/**
*
*/
@ -83,4 +87,4 @@ export const schema = {
},
};
export const name = 'perUserFollowing';
export const entity = Chart.schemaToEntity(name, schema, true);

View File

@ -1,3 +1,7 @@
import Chart from '../../core';
export const name = 'perUserNotes';
export const schema = {
type: 'object' as const,
optional: false as const, nullable: false as const,
@ -40,4 +44,4 @@ export const schema = {
},
};
export const name = 'perUserNotes';
export const entity = Chart.schemaToEntity(name, schema, true);

View File

@ -1,6 +1,10 @@
export const logSchema = {
import Chart from '../../core';
export const name = 'perUserReaction';
const logSchema = {
/**
*
*
*/
count: {
type: 'number' as const,
@ -8,9 +12,6 @@ export const logSchema = {
},
};
/**
*
*/
export const schema = {
type: 'object' as const,
optional: false as const, nullable: false as const,
@ -28,4 +29,4 @@ export const schema = {
},
};
export const name = 'perUserReaction';
export const entity = Chart.schemaToEntity(name, schema, true);

View File

@ -1,3 +1,7 @@
import Chart from '../../core';
export const name = 'testGrouped';
export const schema = {
type: 'object' as const,
optional: false as const, nullable: false as const,
@ -25,4 +29,4 @@ export const schema = {
},
};
export const name = 'testGrouped';
export const entity = Chart.schemaToEntity(name, schema, true);

View File

@ -1,3 +1,7 @@
import Chart from '../../core';
export const name = 'testUnique';
export const schema = {
type: 'object' as const,
optional: false as const, nullable: false as const,
@ -13,4 +17,4 @@ export const schema = {
},
};
export const name = 'testUnique';
export const entity = Chart.schemaToEntity(name, schema);

View File

@ -1,3 +1,7 @@
import Chart from '../../core';
export const name = 'test';
export const schema = {
type: 'object' as const,
optional: false as const, nullable: false as const,
@ -25,4 +29,4 @@ export const schema = {
},
};
export const name = 'test';
export const entity = Chart.schemaToEntity(name, schema);

View File

@ -1,3 +1,7 @@
import Chart from '../../core';
export const name = 'users';
const logSchema = {
/**
*
@ -41,4 +45,4 @@ export const schema = {
},
};
export const name = 'users';
export const entity = Chart.schemaToEntity(name, schema);

View File

@ -1,11 +1,15 @@
import autobind from 'autobind-decorator';
import Chart, { Obj, DeepPartial } from '../../core';
import Chart, { Obj, DeepPartial } from '../core';
import { SchemaType } from '@/misc/schema';
import { Instances } from '@/models/index';
import { name, schema } from '../schemas/federation';
import { name, schema } from './entities/federation';
type FederationLog = SchemaType<typeof schema>;
/**
*
*/
// eslint-disable-next-line import/no-default-export
export default class FederationChart extends Chart<FederationLog> {
constructor() {
super(name, schema);
@ -45,7 +49,7 @@ export default class FederationChart extends Chart<FederationLog> {
}
@autobind
public async update(isAdditional: boolean) {
public async update(isAdditional: boolean): Promise<void> {
const update: Obj = {};
update.total = isAdditional ? 1 : -1;

View File

@ -1,12 +1,16 @@
import autobind from 'autobind-decorator';
import Chart, { Obj, DeepPartial } from '../../core';
import Chart, { Obj, DeepPartial } from '../core';
import { User } from '@/models/entities/user';
import { SchemaType } from '@/misc/schema';
import { Users } from '@/models/index';
import { name, schema } from '../schemas/hashtag';
import { name, schema } from './entities/hashtag';
type HashtagLog = SchemaType<typeof schema>;
/**
*
*/
// eslint-disable-next-line import/no-default-export
export default class HashtagChart extends Chart<HashtagLog> {
constructor() {
super(name, schema, true);
@ -35,7 +39,7 @@ export default class HashtagChart extends Chart<HashtagLog> {
}
@autobind
public async update(hashtag: string, user: { id: User['id'], host: User['host'] }) {
public async update(hashtag: string, user: { id: User['id'], host: User['host'] }): Promise<void> {
const update: Obj = {
users: [user.id],
};

View File

@ -1,17 +1,21 @@
import autobind from 'autobind-decorator';
import Chart, { Obj, DeepPartial } from '../../core';
import Chart, { Obj, DeepPartial } from '../core';
import { SchemaType } from '@/misc/schema';
import { DriveFiles, Followings, Users, Notes } from '@/models/index';
import { DriveFile } from '@/models/entities/drive-file';
import { name, schema } from '../schemas/instance';
import { Note } from '@/models/entities/note';
import { toPuny } from '@/misc/convert-host';
import { name, schema } from './entities/instance';
type InstanceLog = SchemaType<typeof schema>;
/**
*
*/
// eslint-disable-next-line import/no-default-export
export default class InstanceChart extends Chart<InstanceLog> {
constructor() {
super(name, schema);
super(name, schema, true);
}
@autobind
@ -119,7 +123,7 @@ export default class InstanceChart extends Chart<InstanceLog> {
}
@autobind
public async requestReceived(host: string) {
public async requestReceived(host: string): Promise<void> {
await this.inc({
requests: {
received: 1,
@ -128,7 +132,7 @@ export default class InstanceChart extends Chart<InstanceLog> {
}
@autobind
public async requestSent(host: string, isSucceeded: boolean) {
public async requestSent(host: string, isSucceeded: boolean): Promise<void> {
const update: Obj = {};
if (isSucceeded) {
@ -143,7 +147,7 @@ export default class InstanceChart extends Chart<InstanceLog> {
}
@autobind
public async newUser(host: string) {
public async newUser(host: string): Promise<void> {
await this.inc({
users: {
total: 1,
@ -153,8 +157,8 @@ export default class InstanceChart extends Chart<InstanceLog> {
}
@autobind
public async updateNote(host: string, note: Note, isAdditional: boolean) {
const diffs = {} as any;
public async updateNote(host: string, note: Note, isAdditional: boolean): Promise<void> {
const diffs = {} as Record<string, unknown>;
if (note.replyId != null) {
diffs.reply = isAdditional ? 1 : -1;
@ -175,7 +179,7 @@ export default class InstanceChart extends Chart<InstanceLog> {
}
@autobind
public async updateFollowing(host: string, isAdditional: boolean) {
public async updateFollowing(host: string, isAdditional: boolean): Promise<void> {
await this.inc({
following: {
total: isAdditional ? 1 : -1,
@ -186,7 +190,7 @@ export default class InstanceChart extends Chart<InstanceLog> {
}
@autobind
public async updateFollowers(host: string, isAdditional: boolean) {
public async updateFollowers(host: string, isAdditional: boolean): Promise<void> {
await this.inc({
followers: {
total: isAdditional ? 1 : -1,
@ -197,7 +201,7 @@ export default class InstanceChart extends Chart<InstanceLog> {
}
@autobind
public async updateDrive(file: DriveFile, isAdditional: boolean) {
public async updateDrive(file: DriveFile, isAdditional: boolean): Promise<void> {
const update: Obj = {};
update.totalFiles = isAdditional ? 1 : -1;

View File

@ -1,10 +1,14 @@
import autobind from 'autobind-decorator';
import Chart, { DeepPartial } from '../../core';
import Chart, { DeepPartial } from '../core';
import { SchemaType } from '@/misc/schema';
import { name, schema } from '../schemas/network';
import { name, schema } from './entities/network';
type NetworkLog = SchemaType<typeof schema>;
/**
*
*/
// eslint-disable-next-line import/no-default-export
export default class NetworkChart extends Chart<NetworkLog> {
constructor() {
super(name, schema);
@ -32,7 +36,7 @@ export default class NetworkChart extends Chart<NetworkLog> {
}
@autobind
public async update(incomingRequests: number, time: number, incomingBytes: number, outgoingBytes: number) {
public async update(incomingRequests: number, time: number, incomingBytes: number, outgoingBytes: number): Promise<void> {
const inc: DeepPartial<NetworkLog> = {
incomingRequests: incomingRequests,
totalTime: time,

View File

@ -1,13 +1,17 @@
import autobind from 'autobind-decorator';
import Chart, { Obj, DeepPartial } from '../../core';
import Chart, { Obj, DeepPartial } from '../core';
import { SchemaType } from '@/misc/schema';
import { Notes } from '@/models/index';
import { Not, IsNull } from 'typeorm';
import { Note } from '@/models/entities/note';
import { name, schema } from '../schemas/notes';
import { name, schema } from './entities/notes';
type NotesLog = SchemaType<typeof schema>;
/**
*
*/
// eslint-disable-next-line import/no-default-export
export default class NotesChart extends Chart<NotesLog> {
constructor() {
super(name, schema);
@ -69,7 +73,7 @@ export default class NotesChart extends Chart<NotesLog> {
}
@autobind
public async update(note: Note, isAdditional: boolean) {
public async update(note: Note, isAdditional: boolean): Promise<void> {
const update: Obj = {
diffs: {},
};

View File

@ -1,12 +1,16 @@
import autobind from 'autobind-decorator';
import Chart, { Obj, DeepPartial } from '../../core';
import Chart, { Obj, DeepPartial } from '../core';
import { SchemaType } from '@/misc/schema';
import { DriveFiles } from '@/models/index';
import { DriveFile } from '@/models/entities/drive-file';
import { name, schema } from '../schemas/per-user-drive';
import { name, schema } from './entities/per-user-drive';
type PerUserDriveLog = SchemaType<typeof schema>;
/**
*
*/
// eslint-disable-next-line import/no-default-export
export default class PerUserDriveChart extends Chart<PerUserDriveLog> {
constructor() {
super(name, schema, true);
@ -46,7 +50,7 @@ export default class PerUserDriveChart extends Chart<PerUserDriveLog> {
}
@autobind
public async update(file: DriveFile, isAdditional: boolean) {
public async update(file: DriveFile, isAdditional: boolean): Promise<void> {
const update: Obj = {};
update.totalCount = isAdditional ? 1 : -1;

View File

@ -1,13 +1,17 @@
import autobind from 'autobind-decorator';
import Chart, { Obj, DeepPartial } from '../../core';
import Chart, { Obj, DeepPartial } from '../core';
import { SchemaType } from '@/misc/schema';
import { Followings, Users } from '@/models/index';
import { Not, IsNull } from 'typeorm';
import { User } from '@/models/entities/user';
import { name, schema } from '../schemas/per-user-following';
import { name, schema } from './entities/per-user-following';
type PerUserFollowingLog = SchemaType<typeof schema>;
/**
*
*/
// eslint-disable-next-line import/no-default-export
export default class PerUserFollowingChart extends Chart<PerUserFollowingLog> {
constructor() {
super(name, schema, true);
@ -100,7 +104,7 @@ export default class PerUserFollowingChart extends Chart<PerUserFollowingLog> {
}
@autobind
public async update(follower: { id: User['id']; host: User['host']; }, followee: { id: User['id']; host: User['host']; }, isFollow: boolean) {
public async update(follower: { id: User['id']; host: User['host']; }, followee: { id: User['id']; host: User['host']; }, isFollow: boolean): Promise<void> {
const update: Obj = {};
update.total = isFollow ? 1 : -1;

View File

@ -1,13 +1,17 @@
import autobind from 'autobind-decorator';
import Chart, { Obj, DeepPartial } from '../../core';
import Chart, { Obj, DeepPartial } from '../core';
import { User } from '@/models/entities/user';
import { SchemaType } from '@/misc/schema';
import { Notes } from '@/models/index';
import { Note } from '@/models/entities/note';
import { name, schema } from '../schemas/per-user-notes';
import { name, schema } from './entities/per-user-notes';
type PerUserNotesLog = SchemaType<typeof schema>;
/**
*
*/
// eslint-disable-next-line import/no-default-export
export default class PerUserNotesChart extends Chart<PerUserNotesLog> {
constructor() {
super(name, schema, true);
@ -46,7 +50,7 @@ export default class PerUserNotesChart extends Chart<PerUserNotesLog> {
}
@autobind
public async update(user: { id: User['id'] }, note: Note, isAdditional: boolean) {
public async update(user: { id: User['id'] }, note: Note, isAdditional: boolean): Promise<void> {
const update: Obj = {
diffs: {},
};

View File

@ -1,13 +1,17 @@
import autobind from 'autobind-decorator';
import Chart, { DeepPartial } from '../../core';
import Chart, { DeepPartial } from '../core';
import { User } from '@/models/entities/user';
import { Note } from '@/models/entities/note';
import { SchemaType } from '@/misc/schema';
import { Users } from '@/models/index';
import { name, schema } from '../schemas/per-user-reactions';
import { name, schema } from './entities/per-user-reactions';
type PerUserReactionsLog = SchemaType<typeof schema>;
/**
*
*/
// eslint-disable-next-line import/no-default-export
export default class PerUserReactionsChart extends Chart<PerUserReactionsLog> {
constructor() {
super(name, schema, true);
@ -36,7 +40,7 @@ export default class PerUserReactionsChart extends Chart<PerUserReactionsLog> {
}
@autobind
public async update(user: { id: User['id'], host: User['host'] }, note: Note) {
public async update(user: { id: User['id'], host: User['host'] }, note: Note): Promise<void> {
this.inc({
[Users.isLocalUser(user) ? 'local' : 'remote']: { count: 1 },
}, note.userId);

View File

@ -1,10 +1,14 @@
import autobind from 'autobind-decorator';
import Chart, { Obj, DeepPartial } from '../../core';
import Chart, { Obj, DeepPartial } from '../core';
import { SchemaType } from '@/misc/schema';
import { name, schema } from '../schemas/test-grouped';
import { name, schema } from './entities/test-grouped';
type TestGroupedLog = SchemaType<typeof schema>;
/**
* For testing
*/
// eslint-disable-next-line import/no-default-export
export default class TestGroupedChart extends Chart<TestGroupedLog> {
private total = {} as Record<string, number>;
@ -42,7 +46,7 @@ export default class TestGroupedChart extends Chart<TestGroupedLog> {
}
@autobind
public async increment(group: string) {
public async increment(group: string): Promise<void> {
if (this.total[group] == null) this.total[group] = 0;
const update: Obj = {};

View File

@ -1,10 +1,14 @@
import autobind from 'autobind-decorator';
import Chart, { DeepPartial } from '../../core';
import Chart, { DeepPartial } from '../core';
import { SchemaType } from '@/misc/schema';
import { name, schema } from '../schemas/test-unique';
import { name, schema } from './entities/test-unique';
type TestUniqueLog = SchemaType<typeof schema>;
/**
* For testing
*/
// eslint-disable-next-line import/no-default-export
export default class TestUniqueChart extends Chart<TestUniqueLog> {
constructor() {
super(name, schema);
@ -28,7 +32,7 @@ export default class TestUniqueChart extends Chart<TestUniqueLog> {
}
@autobind
public async uniqueIncrement(key: string) {
public async uniqueIncrement(key: string): Promise<void> {
await this.inc({
foo: [key],
});

View File

@ -1,10 +1,14 @@
import autobind from 'autobind-decorator';
import Chart, { Obj, DeepPartial } from '../../core';
import Chart, { Obj, DeepPartial } from '../core';
import { SchemaType } from '@/misc/schema';
import { name, schema } from '../schemas/test';
import { name, schema } from './entities/test';
type TestLog = SchemaType<typeof schema>;
/**
* For testing
*/
// eslint-disable-next-line import/no-default-export
export default class TestChart extends Chart<TestLog> {
public total = 0; // publicにするのはテストのため
@ -42,7 +46,7 @@ export default class TestChart extends Chart<TestLog> {
}
@autobind
public async increment() {
public async increment(): Promise<void> {
const update: Obj = {};
update.total = 1;
@ -55,7 +59,7 @@ export default class TestChart extends Chart<TestLog> {
}
@autobind
public async decrement() {
public async decrement(): Promise<void> {
const update: Obj = {};
update.total = -1;

View File

@ -1,13 +1,17 @@
import autobind from 'autobind-decorator';
import Chart, { Obj, DeepPartial } from '../../core';
import Chart, { Obj, DeepPartial } from '../core';
import { SchemaType } from '@/misc/schema';
import { Users } from '@/models/index';
import { Not, IsNull } from 'typeorm';
import { User } from '@/models/entities/user';
import { name, schema } from '../schemas/users';
import { name, schema } from './entities/users';
type UsersLog = SchemaType<typeof schema>;
/**
*
*/
// eslint-disable-next-line import/no-default-export
export default class UsersChart extends Chart<UsersLog> {
constructor() {
super(name, schema);
@ -59,7 +63,7 @@ export default class UsersChart extends Chart<UsersLog> {
}
@autobind
public async update(user: { id: User['id'], host: User['host'] }, isAdditional: boolean) {
public async update(user: { id: User['id'], host: User['host'] }, isAdditional: boolean): Promise<void> {
const update: Obj = {};
update.total = isAdditional ? 1 : -1;

View File

@ -30,7 +30,7 @@ type Log = {
/**
* 集計のグループ
*/
group: string | null;
group?: string | null;
/**
* 集計日時のUnixタイムスタンプ(秒)
@ -38,7 +38,7 @@ type Log = {
date: number;
};
const camelToSnake = (str: string) => {
const camelToSnake = (str: string): string => {
return str.replace(/([A-Z])/g, s => '_' + s.charAt(0).toLowerCase());
};
@ -47,6 +47,7 @@ const removeDuplicates = (array: any[]) => Array.from(new Set(array));
/**
* 様々なチャートの管理を司るクラス
*/
// eslint-disable-next-line import/no-default-export
export default abstract class Chart<T extends Record<string, any>> {
private static readonly columnPrefix = '___';
private static readonly columnDot = '_';
@ -57,7 +58,8 @@ export default abstract class Chart<T extends Record<string, any>> {
group: string | null;
}[] = [];
public schema: SimpleSchema;
protected repository: Repository<Log>;
protected repositoryForHour: Repository<Log>;
protected repositoryForDay: Repository<Log>;
protected abstract genNewLog(latest: T): DeepPartial<T>;
@ -181,9 +183,15 @@ export default abstract class Chart<T extends Record<string, any>> {
}
@autobind
public static schemaToEntity(name: string, schema: SimpleSchema): EntitySchema {
return new EntitySchema({
name: `__chart__${camelToSnake(name)}`,
public static schemaToEntity(name: string, schema: SimpleSchema, grouped = false): {
hour: EntitySchema,
day: EntitySchema,
} {
const createEntity = (span: 'hour' | 'day'): EntitySchema => new EntitySchema({
name:
span === 'hour' ? `__chart__${camelToSnake(name)}` :
span === 'day' ? `__chart_day__${camelToSnake(name)}` :
new Error('not happen') as never,
columns: {
id: {
type: 'integer',
@ -193,37 +201,45 @@ export default abstract class Chart<T extends Record<string, any>> {
date: {
type: 'integer',
},
group: {
type: 'varchar',
length: 128,
nullable: true,
},
...(grouped ? {
group: {
type: 'varchar',
length: 128,
},
} : {}),
...Chart.convertSchemaToFlatColumnDefinitions(schema),
},
indices: [{
columns: ['date', 'group'],
columns: grouped ? ['date', 'group'] : ['date'],
unique: true,
}, { // groupにnullが含まれると↑のuniqueは機能しないので↓の部分インデックスでカバー
columns: ['date'],
unique: true,
where: '"group" IS NULL',
}],
uniques: [{
columns: grouped ? ['date', 'group'] : ['date'],
}],
relations: {
/* TODO
group: {
target: () => Foo,
type: 'many-to-one',
onDelete: 'CASCADE',
},
*/
},
});
return {
hour: createEntity('hour'),
day: createEntity('day'),
};
}
constructor(name: string, schema: SimpleSchema, grouped = false) {
this.name = name;
this.schema = schema;
const entity = Chart.schemaToEntity(name, schema);
const keys = ['date'];
if (grouped) keys.push('group');
entity.options.uniques = [{
columns: keys,
}];
this.repository = getRepository<Log>(entity);
const { hour, day } = Chart.schemaToEntity(name, schema, grouped);
this.repositoryForHour = getRepository<Log>(hour);
this.repositoryForDay = getRepository<Log>(day);
}
@autobind
@ -247,24 +263,40 @@ export default abstract class Chart<T extends Record<string, any>> {
}
@autobind
private getLatestLog(group: string | null = null): Promise<Log | null> {
return this.repository.findOne({
private getLatestLog(group: string | null, span: 'hour' | 'day'): Promise<Log | null> {
const repository =
span === 'hour' ? this.repositoryForHour :
span === 'day' ? this.repositoryForDay :
new Error('not happen') as never;
return repository.findOne(group ? {
group: group,
}, {
} : {}, {
order: {
date: -1,
},
}).then(x => x || null);
}
/**
* 現在(=今のHour or Day)のログをデータベースから探して、あればそれを返し、なければ作成して返します。
*/
@autobind
private async getCurrentLog(group: string | null = null): Promise<Log> {
private async claimCurrentLog(group: string | null, span: 'hour' | 'day'): Promise<Log> {
const [y, m, d, h] = Chart.getCurrentDate();
const current = dateUTC([y, m, d, h]);
const current = dateUTC(
span === 'hour' ? [y, m, d, h] :
span === 'day' ? [y, m, d] :
new Error('not happen') as never);
// 現在(=今のHour)のログ
const currentLog = await this.repository.findOne({
const repository =
span === 'hour' ? this.repositoryForHour :
span === 'day' ? this.repositoryForDay :
new Error('not happen') as never;
// 現在(=今のHour or Day)のログ
const currentLog = await repository.findOne({
date: Chart.dateToTimestamp(current),
...(group ? { group: group } : {}),
});
@ -283,7 +315,7 @@ export default abstract class Chart<T extends Record<string, any>> {
// * 昨日何もチャートを更新するような出来事がなかった場合は、
// * ログがそもそも作られずドキュメントが存在しないということがあり得るため、
// * 「昨日の」と決め打ちせずに「もっとも最近の」とします
const latest = await this.getLatestLog(group);
const latest = await this.getLatestLog(group, span);
if (latest != null) {
const obj = Chart.convertFlattenColumnsToObject(latest) as T;
@ -297,16 +329,16 @@ export default abstract class Chart<T extends Record<string, any>> {
// 初期ログデータを作成
data = this.getNewLog(null);
logger.info(`${this.name + (group ? `:${group}` : '')}: Initial commit created`);
logger.info(`${this.name + (group ? `:${group}` : '')}(${span}): Initial commit created`);
}
const date = Chart.dateToTimestamp(current);
const lockKey = `${this.name}:${date}:${group}`;
const lockKey = group ? `${this.name}:${date}:${span}:${group}` : `${this.name}:${date}:${span}`;
const unlock = await getChartInsertLock(lockKey);
try {
// ロック内でもう1回チェックする
const currentLog = await this.repository.findOne({
const currentLog = await repository.findOne({
date: date,
...(group ? { group: group } : {}),
});
@ -315,13 +347,13 @@ export default abstract class Chart<T extends Record<string, any>> {
if (currentLog != null) return currentLog;
// 新規ログ挿入
log = await this.repository.insert({
group: group,
log = await repository.insert({
date: date,
...(group ? { group: group } : {}),
...Chart.convertObjectToFlattenColumns(data),
}).then(x => this.repository.findOneOrFail(x.identifiers[0]));
}).then(x => repository.findOneOrFail(x.identifiers[0]));
logger.info(`${this.name + (group ? `:${group}` : '')}: New commit created`);
logger.info(`${this.name + (group ? `:${group}` : '')}(${span}): New commit created`);
return log;
} finally {
@ -349,10 +381,10 @@ export default abstract class Chart<T extends Record<string, any>> {
// そのログは本来は 01:00~ のログとしてDBに保存されて欲しいのに、02:00~ のログ扱いになってしまう。
// これを回避するための実装は複雑になりそうなため、一旦保留。
const update = async (log: Log) => {
const update = async (logHour: Log, logDay: Log): Promise<void> => {
const finalDiffs = {} as Record<string, number | unknown[]>;
for (const diff of this.buffer.filter(q => q.group === log.group).map(q => q.diff)) {
for (const diff of this.buffer.filter(q => q.group == null || (q.group === logHour.group)).map(q => q.diff)) {
const columns = Chart.convertObjectToFlattenColumns(diff);
for (const [k, v] of Object.entries(columns)) {
@ -371,36 +403,60 @@ export default abstract class Chart<T extends Record<string, any>> {
const query = Chart.convertQuery(finalDiffs);
// ログ更新
await this.repository.createQueryBuilder()
.update()
.set(query)
.where('id = :id', { id: log.id })
.execute();
await Promise.all([
this.repositoryForHour.createQueryBuilder()
.update()
.set(query)
.where('id = :id', { id: logHour.id })
.execute(),
this.repositoryForDay.createQueryBuilder()
.update()
.set(query)
.where('id = :id', { id: logDay.id })
.execute(),
]);
logger.info(`${this.name + (log.group ? `:${log.group}` : '')}: Updated`);
logger.info(`${this.name + (logHour.group ? `:${logHour.group}` : '')}: Updated`);
// TODO: この一連の処理が始まった後に新たにbufferに入ったものは消さないようにする
this.buffer = this.buffer.filter(q => q.group !== log.group);
this.buffer = this.buffer.filter(q => q.group != null && (q.group !== logHour.group));
};
const groups = removeDuplicates(this.buffer.map(log => log.group));
await Promise.all(groups.map(group => this.getCurrentLog(group).then(log => update(log))));
await Promise.all(
groups.map(group =>
Promise.all([
this.claimCurrentLog(group, 'hour'),
this.claimCurrentLog(group, 'day'),
]).then(([logHour, logDay]) =>
update(logHour, logDay))));
}
@autobind
public async resync(group: string | null = null): Promise<any> {
public async resync(group: string | null = null): Promise<void> {
const data = await this.fetchActual(group);
const update = async (log: Log) => {
await this.repository.createQueryBuilder()
.update()
.set(Chart.convertObjectToFlattenColumns(data))
.where('id = :id', { id: log.id })
.execute();
const update = async (logHour: Log, logDay: Log): Promise<void> => {
await Promise.all([
this.repositoryForHour.createQueryBuilder()
.update()
.set(Chart.convertObjectToFlattenColumns(data))
.where('id = :id', { id: logHour.id })
.execute(),
this.repositoryForDay.createQueryBuilder()
.update()
.set(Chart.convertObjectToFlattenColumns(data))
.where('id = :id', { id: logDay.id })
.execute(),
]);
};
return this.getCurrentLog(group).then(log => update(log));
return Promise.all([
this.claimCurrentLog(group, 'hour'),
this.claimCurrentLog(group, 'day'),
]).then(([logHour, logDay]) =>
update(logHour, logDay));
}
@autobind
@ -418,13 +474,18 @@ export default abstract class Chart<T extends Record<string, any>> {
const gt =
span === 'day' ? subtractTime(cursor ? dateUTC([y2, m2, d2, 0]) : dateUTC([y, m, d, 0]), amount - 1, 'day') :
span === 'hour' ? subtractTime(cursor ? dateUTC([y2, m2, d2, h2]) : dateUTC([y, m, d, h]), amount - 1, 'hour') :
null as never;
new Error('not happen') as never;
const repository =
span === 'hour' ? this.repositoryForHour :
span === 'day' ? this.repositoryForDay :
new Error('not happen') as never;
// ログ取得
let logs = await this.repository.find({
let logs = await repository.find({
where: {
group: group,
date: Between(Chart.dateToTimestamp(gt), Chart.dateToTimestamp(lt)),
...(group ? { group: group } : {}),
},
order: {
date: -1,
@ -435,9 +496,9 @@ export default abstract class Chart<T extends Record<string, any>> {
if (logs.length === 0) {
// もっとも新しいログを持ってくる
// (すくなくともひとつログが無いと隙間埋めできないため)
const recentLog = await this.repository.findOne({
const recentLog = await repository.findOne(group ? {
group: group,
}, {
} : {}, {
order: {
date: -1,
},
@ -451,9 +512,9 @@ export default abstract class Chart<T extends Record<string, any>> {
} else if (!isTimeSame(new Date(logs[logs.length - 1].date * 1000), gt)) {
// 要求された範囲の最も古い箇所時点での最も新しいログを持ってきて末尾に追加する
// (隙間埋めできないため)
const outdatedLog = await this.repository.findOne({
group: group,
const outdatedLog = await repository.findOne({
date: LessThan(Chart.dateToTimestamp(gt)),
...(group ? { group: group } : {}),
}, {
order: {
date: -1,
@ -467,60 +528,26 @@ export default abstract class Chart<T extends Record<string, any>> {
const chart: T[] = [];
if (span === 'hour') {
for (let i = (amount - 1); i >= 0; i--) {
const current = subtractTime(dateUTC([y, m, d, h]), i, 'hour');
for (let i = (amount - 1); i >= 0; i--) {
const current =
span === 'hour' ? subtractTime(dateUTC([y, m, d, h]), i, 'hour') :
span === 'day' ? subtractTime(dateUTC([y, m, d]), i, 'day') :
new Error('not happen') as never;
const log = logs.find(l => isTimeSame(new Date(l.date * 1000), current));
const log = logs.find(l => isTimeSame(new Date(l.date * 1000), current));
if (log) {
const data = Chart.convertFlattenColumnsToObject(log);
chart.unshift(Chart.countUniqueFields(data) as T);
} else {
// 隙間埋め
const latest = logs.find(l => isTimeBefore(new Date(l.date * 1000), current));
const data = latest ? Chart.convertFlattenColumnsToObject(latest) as T : null;
chart.unshift(Chart.countUniqueFields(this.getNewLog(data)) as T);
}
}
} else if (span === 'day') {
const logsForEachDays: T[][] = [];
let currentDay = -1;
let currentDayIndex = -1;
for (let i = ((amount - 1) * 24) + h; i >= 0; i--) {
const current = subtractTime(dateUTC([y, m, d, h]), i, 'hour');
const _currentDay = Chart.parseDate(current)[2];
if (currentDay != _currentDay) currentDayIndex++;
currentDay = _currentDay;
const log = logs.find(l => isTimeSame(new Date(l.date * 1000), current));
if (log) {
if (logsForEachDays[currentDayIndex]) {
logsForEachDays[currentDayIndex].unshift(Chart.convertFlattenColumnsToObject(log) as T);
} else {
logsForEachDays[currentDayIndex] = [Chart.convertFlattenColumnsToObject(log) as T];
}
} else {
// 隙間埋め
const latest = logs.find(l => isTimeBefore(new Date(l.date * 1000), current));
const data = latest ? Chart.convertFlattenColumnsToObject(latest) as T : null;
const newLog = this.getNewLog(data);
if (logsForEachDays[currentDayIndex]) {
logsForEachDays[currentDayIndex].unshift(newLog);
} else {
logsForEachDays[currentDayIndex] = [newLog];
}
}
}
for (const logs of logsForEachDays) {
const log = this.aggregate(logs);
chart.unshift(Chart.countUniqueFields(log) as T);
if (log) {
const data = Chart.convertFlattenColumnsToObject(log);
chart.unshift(Chart.countUniqueFields(data) as T);
} else {
// 隙間埋め
const latest = logs.find(l => isTimeBefore(new Date(l.date * 1000), current));
const data = latest ? Chart.convertFlattenColumnsToObject(latest) as T : null;
chart.unshift(Chart.countUniqueFields(this.getNewLog(data)) as T);
}
}
const res: ArrayValue<T> = {} as any;
const res = {} as Record<string, unknown>;
/**
* [{ foo: 1, bar: 5 }, { foo: 2, bar: 6 }, { foo: 3, bar: 7 }]
@ -528,7 +555,7 @@ export default abstract class Chart<T extends Record<string, any>> {
* { foo: [1, 2, 3], bar: [5, 6, 7] }
* にする
*/
const compact = (x: Obj, path?: string) => {
const compact = (x: Obj, path?: string): void => {
for (const [k, v] of Object.entries(x)) {
const p = path ? `${path}.${k}` : k;
if (typeof v === 'object' && !Array.isArray(v)) {
@ -542,7 +569,7 @@ export default abstract class Chart<T extends Record<string, any>> {
compact(chart[0]);
return res;
return res as ArrayValue<T>;
}
}

View File

@ -1,15 +1,27 @@
import { fileURLToPath } from 'url';
import { dirname } from 'path';
import Chart from './core';
import { entity as FederationChart } from './charts/entities/federation';
import { entity as NotesChart } from './charts/entities/notes';
import { entity as UsersChart } from './charts/entities/users';
import { entity as NetworkChart } from './charts/entities/network';
import { entity as ActiveUsersChart } from './charts/entities/active-users';
import { entity as InstanceChart } from './charts/entities/instance';
import { entity as PerUserNotesChart } from './charts/entities/per-user-notes';
import { entity as DriveChart } from './charts/entities/drive';
import { entity as PerUserReactionsChart } from './charts/entities/per-user-reactions';
import { entity as HashtagChart } from './charts/entities/hashtag';
import { entity as PerUserFollowingChart } from './charts/entities/per-user-following';
import { entity as PerUserDriveChart } from './charts/entities/per-user-drive';
//const _filename = fileURLToPath(import.meta.url);
const _filename = __filename;
const _dirname = dirname(_filename);
export const entities = Object.values(require('require-all')({
dirname: _dirname + '/charts/schemas',
filter: /^.+\.[jt]s$/,
resolve: (x: any) => {
return Chart.schemaToEntity(x.name, x.schema);
},
}));
export const entities = [
FederationChart.hour, FederationChart.day,
NotesChart.hour, NotesChart.day,
UsersChart.hour, UsersChart.day,
NetworkChart.hour, NetworkChart.day,
ActiveUsersChart.hour, ActiveUsersChart.day,
InstanceChart.hour, InstanceChart.day,
PerUserNotesChart.hour, PerUserNotesChart.day,
DriveChart.hour, DriveChart.day,
PerUserReactionsChart.hour, PerUserReactionsChart.day,
HashtagChart.hour, HashtagChart.day,
PerUserFollowingChart.hour, PerUserFollowingChart.day,
PerUserDriveChart.hour, PerUserDriveChart.day,
];

View File

@ -1,17 +1,18 @@
import FederationChart from './charts/classes/federation';
import NotesChart from './charts/classes/notes';
import UsersChart from './charts/classes/users';
import NetworkChart from './charts/classes/network';
import ActiveUsersChart from './charts/classes/active-users';
import InstanceChart from './charts/classes/instance';
import PerUserNotesChart from './charts/classes/per-user-notes';
import DriveChart from './charts/classes/drive';
import PerUserReactionsChart from './charts/classes/per-user-reactions';
import HashtagChart from './charts/classes/hashtag';
import PerUserFollowingChart from './charts/classes/per-user-following';
import PerUserDriveChart from './charts/classes/per-user-drive';
import { beforeShutdown } from '@/misc/before-shutdown';
import FederationChart from './charts/federation';
import NotesChart from './charts/notes';
import UsersChart from './charts/users';
import NetworkChart from './charts/network';
import ActiveUsersChart from './charts/active-users';
import InstanceChart from './charts/instance';
import PerUserNotesChart from './charts/per-user-notes';
import DriveChart from './charts/drive';
import PerUserReactionsChart from './charts/per-user-reactions';
import HashtagChart from './charts/hashtag';
import PerUserFollowingChart from './charts/per-user-following';
import PerUserDriveChart from './charts/per-user-drive';
export const federationChart = new FederationChart();
export const notesChart = new NotesChart();
export const usersChart = new UsersChart();