@rtbnext/core 2.0.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +11 -0
- package/dist/abstract/Cache.d.ts +9 -0
- package/dist/abstract/Cache.js +19 -0
- package/dist/abstract/Index.d.ts +22 -0
- package/dist/abstract/Index.js +81 -0
- package/dist/abstract/Job.d.ts +19 -0
- package/dist/abstract/Job.js +49 -0
- package/dist/abstract/Snapshot.d.ts +22 -0
- package/dist/abstract/Snapshot.js +78 -0
- package/dist/bin/cli.d.ts +2 -0
- package/dist/bin/cli.js +25 -0
- package/dist/bin/cron.d.ts +2 -0
- package/dist/bin/cron.js +4 -0
- package/dist/core/Config.d.ts +30 -0
- package/dist/core/Config.js +66 -0
- package/dist/core/Cron.d.ts +12 -0
- package/dist/core/Cron.js +52 -0
- package/dist/core/Fetch.d.ts +28 -0
- package/dist/core/Fetch.js +172 -0
- package/dist/core/Logger.d.ts +30 -0
- package/dist/core/Logger.js +92 -0
- package/dist/core/Queue.d.ts +37 -0
- package/dist/core/Queue.js +136 -0
- package/dist/core/Storage.d.ts +28 -0
- package/dist/core/Storage.js +166 -0
- package/dist/core/Utils.d.ts +33 -0
- package/dist/core/Utils.js +167 -0
- package/dist/interfaces/cache.d.ts +6 -0
- package/dist/interfaces/config.d.ts +21 -0
- package/dist/interfaces/cron.d.ts +3 -0
- package/dist/interfaces/fetch.d.ts +13 -0
- package/dist/interfaces/filter.d.ts +12 -0
- package/dist/interfaces/index.d.ts +30 -0
- package/dist/interfaces/job.d.ts +9 -0
- package/dist/interfaces/list.d.ts +9 -0
- package/dist/interfaces/logger.d.ts +20 -0
- package/dist/interfaces/mover.d.ts +7 -0
- package/dist/interfaces/parser.d.ts +68 -0
- package/dist/interfaces/profile.d.ts +30 -0
- package/dist/interfaces/queue.d.ts +17 -0
- package/dist/interfaces/snapshot.d.ts +16 -0
- package/dist/interfaces/stats.d.ts +45 -0
- package/dist/interfaces/storage.d.ts +16 -0
- package/dist/job/Alias.d.ts +8 -0
- package/dist/job/Alias.js +42 -0
- package/dist/job/Annual.d.ts +8 -0
- package/dist/job/Annual.js +41 -0
- package/dist/job/List.d.ts +11 -0
- package/dist/job/List.js +101 -0
- package/dist/job/Merge.d.ts +10 -0
- package/dist/job/Merge.js +59 -0
- package/dist/job/Move.d.ts +7 -0
- package/dist/job/Move.js +33 -0
- package/dist/job/Performance.d.ts +8 -0
- package/dist/job/Performance.js +27 -0
- package/dist/job/Profile.d.ts +11 -0
- package/dist/job/Profile.js +76 -0
- package/dist/job/Queue.d.ts +8 -0
- package/dist/job/Queue.js +54 -0
- package/dist/job/RTB.d.ts +12 -0
- package/dist/job/RTB.js +121 -0
- package/dist/job/Stats.d.ts +11 -0
- package/dist/job/Stats.js +46 -0
- package/dist/job/Top10.d.ts +9 -0
- package/dist/job/Top10.js +48 -0
- package/dist/job/Wiki.d.ts +9 -0
- package/dist/job/Wiki.js +40 -0
- package/dist/job/index.d.ts +26 -0
- package/dist/job/index.js +26 -0
- package/dist/lib/const.d.ts +31 -0
- package/dist/lib/const.js +74 -0
- package/dist/lib/list.d.ts +90 -0
- package/dist/lib/list.js +72 -0
- package/dist/lib/regex.d.ts +7 -0
- package/dist/lib/regex.js +7 -0
- package/dist/model/Filter.d.ts +28 -0
- package/dist/model/Filter.js +122 -0
- package/dist/model/List.d.ts +12 -0
- package/dist/model/List.js +43 -0
- package/dist/model/ListIndex.d.ts +8 -0
- package/dist/model/ListIndex.js +10 -0
- package/dist/model/Mover.d.ts +15 -0
- package/dist/model/Mover.js +74 -0
- package/dist/model/Profile.d.ts +49 -0
- package/dist/model/Profile.js +181 -0
- package/dist/model/ProfileIndex.d.ts +20 -0
- package/dist/model/ProfileIndex.js +140 -0
- package/dist/model/Stats.d.ts +56 -0
- package/dist/model/Stats.js +435 -0
- package/dist/parser/BillionairesListParser.d.ts +3 -0
- package/dist/parser/BillionairesListParser.js +2 -0
- package/dist/parser/ListParser.d.ts +7 -0
- package/dist/parser/ListParser.js +11 -0
- package/dist/parser/Parser.d.ts +43 -0
- package/dist/parser/Parser.js +146 -0
- package/dist/parser/PersonListParser.d.ts +29 -0
- package/dist/parser/PersonListParser.js +111 -0
- package/dist/parser/ProfileParser.d.ts +44 -0
- package/dist/parser/ProfileParser.js +193 -0
- package/dist/parser/RTBListParser.d.ts +15 -0
- package/dist/parser/RTBListParser.js +91 -0
- package/dist/types/annual.d.ts +7 -0
- package/dist/types/config.d.ts +35 -0
- package/dist/types/fetch.d.ts +3 -0
- package/dist/types/generic.d.ts +10 -0
- package/dist/types/job.d.ts +71 -0
- package/dist/types/list.d.ts +49 -0
- package/dist/types/parser.d.ts +7 -0
- package/dist/types/profile.d.ts +9 -0
- package/dist/types/queue.d.ts +15 -0
- package/dist/types/response.d.ts +183 -0
- package/dist/types/storage.d.ts +3 -0
- package/dist/types/wiki.d.ts +1 -0
- package/dist/utils/Annual.d.ts +7 -0
- package/dist/utils/Annual.js +99 -0
- package/dist/utils/Performance.d.ts +8 -0
- package/dist/utils/Performance.js +39 -0
- package/dist/utils/ProfileManager.d.ts +24 -0
- package/dist/utils/ProfileManager.js +60 -0
- package/dist/utils/ProfileMerger.d.ts +11 -0
- package/dist/utils/ProfileMerger.js +67 -0
- package/dist/utils/Ranking.d.ts +11 -0
- package/dist/utils/Ranking.js +77 -0
- package/dist/utils/Wiki.d.ts +11 -0
- package/dist/utils/Wiki.js +168 -0
- package/package.json +45 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { Snapshot } from '../abstract/Snapshot.js';
|
|
2
|
+
import { Utils } from '../core/Utils.js';
|
|
3
|
+
import { Parser } from '../parser/Parser.js';
|
|
4
|
+
export class Mover extends Snapshot {
|
|
5
|
+
static instance;
|
|
6
|
+
constructor() {
|
|
7
|
+
super('mover');
|
|
8
|
+
}
|
|
9
|
+
// --- parse mover items ---
|
|
10
|
+
prepItems(items, method, dir) {
|
|
11
|
+
return items
|
|
12
|
+
.sort((a, b) => (dir === 'asc' ? a.value - b.value : b.value - a.value))
|
|
13
|
+
.slice(0, 10)
|
|
14
|
+
.map(({ uri, name, value }) => ({ uri, name, value: Parser[method](value) }));
|
|
15
|
+
}
|
|
16
|
+
parseEntry(entry, method) {
|
|
17
|
+
return { winner: this.prepItems(entry.winner, method, 'desc'), loser: this.prepItems(entry.loser, method, 'asc') };
|
|
18
|
+
}
|
|
19
|
+
parseBucket(bucket) {
|
|
20
|
+
return {
|
|
21
|
+
total: Parser.container({
|
|
22
|
+
value: { value: bucket.total.value, type: 'money' },
|
|
23
|
+
percent: { value: bucket.total.percent, type: 'pct' }
|
|
24
|
+
}),
|
|
25
|
+
networth: this.parseEntry(bucket.networth, 'money'),
|
|
26
|
+
percent: this.parseEntry(bucket.percent, 'pct')
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
// --- (override) save mover snapshot ---
|
|
30
|
+
saveSnapshot(snapshot, force) {
|
|
31
|
+
return super.saveSnapshot(
|
|
32
|
+
{
|
|
33
|
+
...Utils.metaData(),
|
|
34
|
+
date: Parser.date(snapshot.date),
|
|
35
|
+
today: this.parseBucket(snapshot.today),
|
|
36
|
+
ytd: this.parseBucket(snapshot.ytd)
|
|
37
|
+
},
|
|
38
|
+
force
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
// --- instantiate ---
|
|
42
|
+
static getInstance() {
|
|
43
|
+
return (this.instance ??= new Mover());
|
|
44
|
+
}
|
|
45
|
+
// --- factory ---
|
|
46
|
+
static factory(date) {
|
|
47
|
+
return {
|
|
48
|
+
date: Parser.date(date ?? new Date(), 'ymd'),
|
|
49
|
+
today: {
|
|
50
|
+
total: { value: 0, percent: 0 },
|
|
51
|
+
networth: { winner: [], loser: [] },
|
|
52
|
+
percent: { winner: [], loser: [] }
|
|
53
|
+
},
|
|
54
|
+
ytd: { total: { value: 0, percent: 0 }, networth: { winner: [], loser: [] }, percent: { winner: [], loser: [] } }
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
// --- aggregate mover data ---
|
|
58
|
+
static aggregate(data, uri, name, mover, total = 0) {
|
|
59
|
+
if (data?.today?.value) {
|
|
60
|
+
const type = data.today.value > 0 ? 'winner' : 'loser';
|
|
61
|
+
mover.today.total.value += data.today.value;
|
|
62
|
+
mover.today.total.percent = total ? (mover.today.total.value / total) * 100 : 0;
|
|
63
|
+
mover.today.networth[type].push({ uri, name, value: data.today.value });
|
|
64
|
+
mover.today.percent[type].push({ uri, name, value: data.today.percent });
|
|
65
|
+
}
|
|
66
|
+
if (data?.ytd?.value) {
|
|
67
|
+
const type = data.ytd.value > 0 ? 'winner' : 'loser';
|
|
68
|
+
mover.ytd.total.value += data.ytd.value;
|
|
69
|
+
mover.ytd.total.percent = total ? (mover.ytd.total.value / total) * 100 : 0;
|
|
70
|
+
mover.ytd.networth[type].push({ uri, name, value: data.ytd.value });
|
|
71
|
+
mover.ytd.percent[type].push({ uri, name, value: data.ytd.percent });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { ArrayMode } from '@komed3/deepmerge';
|
|
2
|
+
import type {
|
|
3
|
+
TProfileData,
|
|
4
|
+
TProfileHistory,
|
|
5
|
+
TProfileHistoryItem,
|
|
6
|
+
TProfileIndexItem,
|
|
7
|
+
TProfileMetaData
|
|
8
|
+
} from '@rtbnext/schema/src/model/profile';
|
|
9
|
+
import type { IProfile } from '../interfaces/profile';
|
|
10
|
+
export declare class Profile implements IProfile {
|
|
11
|
+
private static readonly storage;
|
|
12
|
+
private static readonly index;
|
|
13
|
+
private touched;
|
|
14
|
+
private uri;
|
|
15
|
+
private path;
|
|
16
|
+
private meta;
|
|
17
|
+
private data?;
|
|
18
|
+
private history?;
|
|
19
|
+
private constructor();
|
|
20
|
+
private resolvePath;
|
|
21
|
+
private metaData;
|
|
22
|
+
private resolveHistory;
|
|
23
|
+
getUri(): string;
|
|
24
|
+
getMeta(): TProfileMetaData['$metadata'];
|
|
25
|
+
schemaVersion(): number;
|
|
26
|
+
lastModified(): string;
|
|
27
|
+
lastModifiedTime(): number;
|
|
28
|
+
lastLookup(): string | undefined;
|
|
29
|
+
lastLookupTime(): number | undefined;
|
|
30
|
+
verify(id: string): boolean;
|
|
31
|
+
touch(): void;
|
|
32
|
+
touchLookup(): void;
|
|
33
|
+
needSave(): boolean;
|
|
34
|
+
save(syncIndex?: boolean): void;
|
|
35
|
+
getData(): TProfileData;
|
|
36
|
+
setData(data: TProfileData): void;
|
|
37
|
+
updateData(data: Partial<TProfileData>, mode?: ArrayMode): void;
|
|
38
|
+
getHistory(): TProfileHistory;
|
|
39
|
+
setHistory(history: TProfileHistory): void;
|
|
40
|
+
addHistory(row: TProfileHistoryItem): void;
|
|
41
|
+
mergeHistory(history: TProfileHistory): void;
|
|
42
|
+
move(uriLike: string, makeAlias?: boolean): boolean;
|
|
43
|
+
static factory(data?: Partial<TProfileData>): Partial<TProfileData>;
|
|
44
|
+
static get(uriLike: string): IProfile | false;
|
|
45
|
+
static getByItem(item: TProfileIndexItem): IProfile | false;
|
|
46
|
+
static find(uriLike: string): IProfile | false;
|
|
47
|
+
static create(uriLike: string, data: TProfileData, history?: TProfileHistory, lookup?: boolean): Profile | false;
|
|
48
|
+
static delete(uriLike: string): boolean;
|
|
49
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { log } from '../core/Logger.js';
|
|
3
|
+
import { Storage } from '../core/Storage.js';
|
|
4
|
+
import { Utils } from '../core/Utils.js';
|
|
5
|
+
import { ProfileIndex } from './ProfileIndex.js';
|
|
6
|
+
export class Profile {
|
|
7
|
+
static storage = Storage.getInstance();
|
|
8
|
+
static index = ProfileIndex.getInstance();
|
|
9
|
+
touched = false;
|
|
10
|
+
uri;
|
|
11
|
+
path;
|
|
12
|
+
meta;
|
|
13
|
+
data;
|
|
14
|
+
history;
|
|
15
|
+
constructor(item) {
|
|
16
|
+
if (!item || !item.uri) throw new Error('No valid profile index item given');
|
|
17
|
+
this.uri = item.uri;
|
|
18
|
+
this.path = join('profile', item.uri);
|
|
19
|
+
Profile.storage.ensurePath(this.path, true);
|
|
20
|
+
this.meta = this.metaData();
|
|
21
|
+
}
|
|
22
|
+
// --- helper ---
|
|
23
|
+
resolvePath(path) {
|
|
24
|
+
return join(this.path, path);
|
|
25
|
+
}
|
|
26
|
+
metaData() {
|
|
27
|
+
return Profile.storage.readJSON(this.resolvePath('meta.json')) || Utils.metaData();
|
|
28
|
+
}
|
|
29
|
+
resolveHistory(...history) {
|
|
30
|
+
return [...new Map([...this.getHistory(), ...history].map(i => [i[0], i])).values()].sort((a, b) =>
|
|
31
|
+
a[0].localeCompare(b[0])
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
// --- public getter ---
|
|
35
|
+
getUri() {
|
|
36
|
+
return this.uri;
|
|
37
|
+
}
|
|
38
|
+
getMeta() {
|
|
39
|
+
return this.meta.$metadata;
|
|
40
|
+
}
|
|
41
|
+
schemaVersion() {
|
|
42
|
+
return this.meta.$metadata.schemaVersion;
|
|
43
|
+
}
|
|
44
|
+
lastModified() {
|
|
45
|
+
return this.meta.$metadata.lastModified;
|
|
46
|
+
}
|
|
47
|
+
lastModifiedTime() {
|
|
48
|
+
return new Date(this.meta.$metadata.lastModified).getTime();
|
|
49
|
+
}
|
|
50
|
+
lastLookup() {
|
|
51
|
+
return this.meta.$metadata.lastLookup;
|
|
52
|
+
}
|
|
53
|
+
lastLookupTime() {
|
|
54
|
+
return this.meta.$metadata.lastLookup ? new Date(this.meta.$metadata.lastLookup).getTime() : undefined;
|
|
55
|
+
}
|
|
56
|
+
// --- verify profile ---
|
|
57
|
+
verify(id) {
|
|
58
|
+
return Utils.verifyHash(id, this.getData().id);
|
|
59
|
+
}
|
|
60
|
+
// --- work flow ---
|
|
61
|
+
touch() {
|
|
62
|
+
this.meta.$metadata.lastModified = Utils.date('iso');
|
|
63
|
+
this.touched = true;
|
|
64
|
+
}
|
|
65
|
+
touchLookup() {
|
|
66
|
+
this.touch();
|
|
67
|
+
this.meta.$metadata.lastLookup = this.meta.$metadata.lastModified;
|
|
68
|
+
}
|
|
69
|
+
needSave() {
|
|
70
|
+
return this.touched;
|
|
71
|
+
}
|
|
72
|
+
save(syncIndex = true) {
|
|
73
|
+
if (!this.touched) return;
|
|
74
|
+
log.debug(`Saving profile: ${this.uri}`);
|
|
75
|
+
log.catch(() => {
|
|
76
|
+
if (syncIndex && this.data && !Profile.index.syncFromData(this.data))
|
|
77
|
+
throw new Error('Failed to update profile index');
|
|
78
|
+
if (this.data && !Profile.storage.writeJSON(this.resolvePath('profile.json'), this.data))
|
|
79
|
+
throw new Error('Failed to write profile data');
|
|
80
|
+
if (this.history && !Profile.storage.writeCSV(this.resolvePath('history.csv'), this.history))
|
|
81
|
+
throw new Error('Failed to write profile history');
|
|
82
|
+
if (this.meta && !Profile.storage.writeJSON(this.resolvePath('meta.json'), this.meta))
|
|
83
|
+
throw new Error('Failed to write profile metadata');
|
|
84
|
+
this.touched = false;
|
|
85
|
+
}, `Failed to save profile: ${this.uri}`);
|
|
86
|
+
}
|
|
87
|
+
// --- profile data ---
|
|
88
|
+
getData() {
|
|
89
|
+
return (this.data ??= Profile.storage.readJSON(this.resolvePath('profile.json')) ?? Profile.factory());
|
|
90
|
+
}
|
|
91
|
+
setData(data) {
|
|
92
|
+
this.data = Profile.factory(data);
|
|
93
|
+
this.touch();
|
|
94
|
+
}
|
|
95
|
+
updateData(data, mode = 'replace' /* ArrayMode.Replace */) {
|
|
96
|
+
this.data = Utils.merge(mode, this.getData(), data);
|
|
97
|
+
this.touch();
|
|
98
|
+
}
|
|
99
|
+
// --- profile history ---
|
|
100
|
+
getHistory() {
|
|
101
|
+
return (this.history ??= Profile.storage.readCSV(this.resolvePath('history.csv')) ?? []);
|
|
102
|
+
}
|
|
103
|
+
setHistory(history) {
|
|
104
|
+
this.history = history;
|
|
105
|
+
this.touch();
|
|
106
|
+
}
|
|
107
|
+
addHistory(row) {
|
|
108
|
+
this.setHistory(this.resolveHistory(row));
|
|
109
|
+
}
|
|
110
|
+
mergeHistory(history) {
|
|
111
|
+
this.setHistory(this.resolveHistory(...history));
|
|
112
|
+
}
|
|
113
|
+
// --- move profile ---
|
|
114
|
+
move(uriLike, makeAlias = true) {
|
|
115
|
+
const uri = Utils.sanitize(uriLike);
|
|
116
|
+
log.debug(`Moving profile: ${this.uri} -> ${uri}`);
|
|
117
|
+
return (
|
|
118
|
+
log.catch(() => {
|
|
119
|
+
const item = Profile.index.move(this.uri, uri, makeAlias);
|
|
120
|
+
if (!item) throw new Error('Failed to move profile index item');
|
|
121
|
+
const oldPath = this.path;
|
|
122
|
+
this.uri = uri;
|
|
123
|
+
this.path = join('profile', uri);
|
|
124
|
+
if (!Profile.storage.move(oldPath, this.path)) throw new Error('Failed to move profile storage');
|
|
125
|
+
this.updateData({ uri: uri });
|
|
126
|
+
this.save(false);
|
|
127
|
+
return true;
|
|
128
|
+
}, `Failed to move profile: ${this.uri} -> ${uri}`) ?? false
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
// --- factory ---
|
|
132
|
+
static factory(data) {
|
|
133
|
+
return { ...{ info: {}, bio: {}, related: [], media: [], ranking: [], annual: [], assets: [] }, ...data };
|
|
134
|
+
}
|
|
135
|
+
// --- instantiate ---
|
|
136
|
+
static get(uriLike) {
|
|
137
|
+
return log.catch(() => new Profile(Profile.index.get(uriLike)), `Failed to get profile: ${uriLike}`) ?? false;
|
|
138
|
+
}
|
|
139
|
+
static getByItem(item) {
|
|
140
|
+
return log.catch(() => new Profile(item), `Failed to get profile by item: ${item.uri}`) ?? false;
|
|
141
|
+
}
|
|
142
|
+
static find(uriLike) {
|
|
143
|
+
return (
|
|
144
|
+
log.catch(
|
|
145
|
+
() => new Profile(Profile.index.find(uriLike).values().next().value),
|
|
146
|
+
`Failed to find profile: ${uriLike}`
|
|
147
|
+
) ?? false
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
// --- create profile ---
|
|
151
|
+
static create(uriLike, data, history, lookup = false) {
|
|
152
|
+
const uri = Utils.sanitize(uriLike);
|
|
153
|
+
log.debug(`Creating profile: ${uri}`);
|
|
154
|
+
return (
|
|
155
|
+
log.catch(() => {
|
|
156
|
+
if (!Profile.index.isAliasAvailable(uri)) throw new Error(`Profile URI ${uri} is already taken`);
|
|
157
|
+
const profile = new Profile({ uri });
|
|
158
|
+
profile.setData(Profile.factory(data));
|
|
159
|
+
profile.setHistory(history ?? []);
|
|
160
|
+
if (lookup) profile.touchLookup();
|
|
161
|
+
profile.save();
|
|
162
|
+
log.debug(`Profile created: ${uri}`);
|
|
163
|
+
return profile;
|
|
164
|
+
}, `Failed to create profile: ${uri}`) ?? false
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
// --- delete profile ---
|
|
168
|
+
static delete(uriLike) {
|
|
169
|
+
const uri = Utils.sanitize(uriLike);
|
|
170
|
+
log.debug(`Deleting profile: ${uri}`);
|
|
171
|
+
return (
|
|
172
|
+
log.catch(() => {
|
|
173
|
+
const path = join('profile', uri);
|
|
174
|
+
if (!Profile.storage.remove(path)) throw new Error('Failed to remove profile from storage');
|
|
175
|
+
Profile.index.delete(uri);
|
|
176
|
+
log.debug(`Profile deleted: ${uri}`);
|
|
177
|
+
return true;
|
|
178
|
+
}, `Failed to delete profile: ${uri}`) ?? false
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { TProfileData, TProfileIndex, TProfileIndexItem } from '@rtbnext/schema/src/model/profile';
|
|
2
|
+
import { Index } from '../abstract/Index.js';
|
|
3
|
+
import type { IProfileIndex } from '../interfaces/index';
|
|
4
|
+
export declare class ProfileIndex extends Index<TProfileIndexItem, TProfileIndex> implements IProfileIndex {
|
|
5
|
+
protected static instance: IProfileIndex;
|
|
6
|
+
private constructor();
|
|
7
|
+
private sanitizeAliases;
|
|
8
|
+
private getUriByAlias;
|
|
9
|
+
private assertAvailableAlias;
|
|
10
|
+
private resolveAliases;
|
|
11
|
+
find(uriLike: string): TProfileIndex;
|
|
12
|
+
move(from: string, to: string, makeAlias?: boolean): TProfileIndexItem | false;
|
|
13
|
+
syncFromData(data: TProfileData): TProfileIndexItem | false;
|
|
14
|
+
hasAlias(aliasLike: string, uriLike?: string): string | false;
|
|
15
|
+
isAliasAvailable(aliasLike: string): boolean;
|
|
16
|
+
removeAlias(aliasLike: string): boolean;
|
|
17
|
+
addAliases(uriLike: string, ...aliases: string[]): TProfileIndexItem | false;
|
|
18
|
+
rmvAliases(uriLike: string, ...aliases: string[]): TProfileIndexItem | false;
|
|
19
|
+
static getInstance(): IProfileIndex;
|
|
20
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { Index } from '../abstract/Index.js';
|
|
2
|
+
import { log } from '../core/Logger.js';
|
|
3
|
+
import { Utils } from '../core/Utils.js';
|
|
4
|
+
export class ProfileIndex extends Index {
|
|
5
|
+
static instance;
|
|
6
|
+
constructor() {
|
|
7
|
+
super('profile', 'profile/index.json');
|
|
8
|
+
}
|
|
9
|
+
// --- alias helper ---
|
|
10
|
+
sanitizeAliases(aliases) {
|
|
11
|
+
return aliases.map(a => Utils.sanitize(a)).filter(Boolean);
|
|
12
|
+
}
|
|
13
|
+
getUriByAlias(alias) {
|
|
14
|
+
return [...this.index.values()].find(({ aliases }) => aliases.includes(alias))?.uri || false;
|
|
15
|
+
}
|
|
16
|
+
assertAvailableAlias(alias, whitelist = []) {
|
|
17
|
+
if (this.has(alias)) throw new Error(`Alias ${alias} conflicts with existing profile URI`);
|
|
18
|
+
const owner = this.getUriByAlias(alias);
|
|
19
|
+
if (owner && !whitelist.includes(owner)) throw new Error(`Alias ${alias} already exists for profile ${owner}`);
|
|
20
|
+
}
|
|
21
|
+
resolveAliases(uri, aliases = [], add = [], rmv = []) {
|
|
22
|
+
for (const a of add) this.assertAvailableAlias(a, [uri]);
|
|
23
|
+
return Utils.mergeArray(
|
|
24
|
+
aliases.filter(alias => !rmv.includes(alias)),
|
|
25
|
+
add,
|
|
26
|
+
'unique' /* ArrayMode.Unique */
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
// --- special profile index operations ---
|
|
30
|
+
find(uriLike) {
|
|
31
|
+
const uri = Utils.sanitize(uriLike);
|
|
32
|
+
return new Map([...this.index].filter(([key, { aliases }]) => key === uri || aliases.includes(uri)));
|
|
33
|
+
}
|
|
34
|
+
move(from, to, makeAlias = true) {
|
|
35
|
+
log.debug(`Moving profile index item from ${from} to ${to}`);
|
|
36
|
+
return (
|
|
37
|
+
log.catch(() => {
|
|
38
|
+
((from = Utils.sanitize(from)), (to = Utils.sanitize(to)));
|
|
39
|
+
const target = this.index.get(from),
|
|
40
|
+
match = this.find(to);
|
|
41
|
+
if (!target || match.size > 1) throw new Error('Invalid move operation');
|
|
42
|
+
const foundKey = match.keys().next().value;
|
|
43
|
+
if (foundKey && foundKey !== from) throw new Error('Destination already exists');
|
|
44
|
+
const item = {
|
|
45
|
+
...target,
|
|
46
|
+
uri: to,
|
|
47
|
+
aliases: this.resolveAliases(to, target.aliases, makeAlias ? [from] : [], [to])
|
|
48
|
+
};
|
|
49
|
+
this.index.delete(from);
|
|
50
|
+
this.index.set(to, item);
|
|
51
|
+
this.saveIndex();
|
|
52
|
+
return item;
|
|
53
|
+
}, `Failed to move profile index item ${from} to ${to}`) ?? false
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
syncFromData(data) {
|
|
57
|
+
const {
|
|
58
|
+
uri,
|
|
59
|
+
info: {
|
|
60
|
+
name: { shortName: name }
|
|
61
|
+
},
|
|
62
|
+
bio: { cv },
|
|
63
|
+
wiki: { desc, image } = {}
|
|
64
|
+
} = data;
|
|
65
|
+
const item = this.get(uri);
|
|
66
|
+
return this.update(uri, {
|
|
67
|
+
...item,
|
|
68
|
+
uri,
|
|
69
|
+
name,
|
|
70
|
+
desc,
|
|
71
|
+
image: image?.thumb ?? image?.file,
|
|
72
|
+
aliases: this.resolveAliases(uri, item?.aliases),
|
|
73
|
+
text: Utils.buildSearchText(cv)
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
// --- alias handling ---
|
|
77
|
+
hasAlias(aliasLike, uriLike) {
|
|
78
|
+
const owner = this.getUriByAlias(Utils.sanitize(aliasLike));
|
|
79
|
+
if (uriLike !== undefined && owner !== Utils.sanitize(uriLike)) return false;
|
|
80
|
+
return owner;
|
|
81
|
+
}
|
|
82
|
+
isAliasAvailable(aliasLike) {
|
|
83
|
+
try {
|
|
84
|
+
this.assertAvailableAlias(Utils.sanitize(aliasLike));
|
|
85
|
+
return true;
|
|
86
|
+
} catch {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
removeAlias(aliasLike) {
|
|
91
|
+
const alias = Utils.sanitize(aliasLike);
|
|
92
|
+
log.debug(`Removing profile alias ${alias}`);
|
|
93
|
+
return (
|
|
94
|
+
log.catch(() => {
|
|
95
|
+
for (const item of this.index.values()) {
|
|
96
|
+
const index = item.aliases.indexOf(alias);
|
|
97
|
+
if (index >= 0) {
|
|
98
|
+
item.aliases.splice(index, 1);
|
|
99
|
+
this.saveIndex();
|
|
100
|
+
log.debug(`Removed alias ${alias} from profile index item ${item.uri}`);
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return false;
|
|
105
|
+
}, `Failed to remove profile alias ${alias}`) ?? false
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
addAliases(uriLike, ...aliases) {
|
|
109
|
+
const uri = Utils.sanitize(uriLike);
|
|
110
|
+
log.debug(`Adding profile aliases [${aliases.join(', ')}] to ${uri}`);
|
|
111
|
+
return (
|
|
112
|
+
log.catch(() => {
|
|
113
|
+
const item = this.index.get(uri);
|
|
114
|
+
if (!item) throw new Error(`Profile index item ${uri} not found`);
|
|
115
|
+
if (!aliases.length) return item;
|
|
116
|
+
item.aliases = this.resolveAliases(uri, item.aliases, this.sanitizeAliases(aliases));
|
|
117
|
+
this.saveIndex();
|
|
118
|
+
return item;
|
|
119
|
+
}, `Failed to add profile aliases to ${uri}`) ?? false
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
rmvAliases(uriLike, ...aliases) {
|
|
123
|
+
const uri = Utils.sanitize(uriLike);
|
|
124
|
+
log.debug(`Removing profile aliases [${aliases.join(', ')}] from ${uri}`);
|
|
125
|
+
return (
|
|
126
|
+
log.catch(() => {
|
|
127
|
+
const item = this.index.get(uri);
|
|
128
|
+
if (!item) throw new Error(`Profile index item ${uri} not found`);
|
|
129
|
+
if (!aliases.length) return item;
|
|
130
|
+
item.aliases = this.resolveAliases(uri, item.aliases, [], this.sanitizeAliases(aliases));
|
|
131
|
+
this.saveIndex();
|
|
132
|
+
return item;
|
|
133
|
+
}, `Failed to remove profile aliases from ${uri}`) ?? false
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
// --- instantitate ---
|
|
137
|
+
static getInstance() {
|
|
138
|
+
return (ProfileIndex.instance ??= new ProfileIndex());
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { TStatsGroup as TStatsGroupType } from '@rtbnext/schema/src/base/const';
|
|
2
|
+
import type { TListSnapshot, TPersonListItem } from '@rtbnext/schema/src/model/list';
|
|
3
|
+
import type { TProfileData } from '@rtbnext/schema/src/model/profile';
|
|
4
|
+
import type {
|
|
5
|
+
TDBStats,
|
|
6
|
+
TDBStatsData,
|
|
7
|
+
TGlobalStats,
|
|
8
|
+
TGlobalStatsData,
|
|
9
|
+
THistory,
|
|
10
|
+
TProfileStats,
|
|
11
|
+
TProfileStatsData,
|
|
12
|
+
TScatter,
|
|
13
|
+
TScatterItem,
|
|
14
|
+
TStatsGroup,
|
|
15
|
+
TStatsGroupItem,
|
|
16
|
+
TTop10,
|
|
17
|
+
TTop10Data,
|
|
18
|
+
TTop10List,
|
|
19
|
+
TWealthStats,
|
|
20
|
+
TWealthStatsData
|
|
21
|
+
} from '@rtbnext/schema/src/model/stats';
|
|
22
|
+
import type { IStats } from '../interfaces/stats';
|
|
23
|
+
export declare class Stats implements IStats {
|
|
24
|
+
private static readonly storage;
|
|
25
|
+
private static instance;
|
|
26
|
+
private constructor();
|
|
27
|
+
private initDB;
|
|
28
|
+
private resolvePath;
|
|
29
|
+
private prepStats;
|
|
30
|
+
private getStats;
|
|
31
|
+
private saveStats;
|
|
32
|
+
getGlobalStats(): TGlobalStats;
|
|
33
|
+
getProfileStats(): TProfileStats;
|
|
34
|
+
getWealthStats(): TWealthStats;
|
|
35
|
+
getScatter(): TScatter;
|
|
36
|
+
getTop10(): TTop10;
|
|
37
|
+
getDBStats(): TDBStats;
|
|
38
|
+
getHistory(): THistory;
|
|
39
|
+
getGroupedStatsIndex(group: TStatsGroupType): TStatsGroup<string>['index'];
|
|
40
|
+
getGroupedStatsHistory(group: TStatsGroupType, key: string): THistory;
|
|
41
|
+
getGroupedStats(group: TStatsGroupType): TStatsGroup<string>;
|
|
42
|
+
setGlobalStats(data: TGlobalStatsData): boolean;
|
|
43
|
+
setProfileStats(data: TProfileStatsData): boolean;
|
|
44
|
+
setWealthStats(data: TWealthStatsData): boolean;
|
|
45
|
+
setScatter(items: TScatterItem[]): boolean;
|
|
46
|
+
setTop10(data: TTop10Data): boolean;
|
|
47
|
+
updateTop10(key: string, list: TTop10List): boolean;
|
|
48
|
+
setDBStats(data: TDBStatsData): boolean;
|
|
49
|
+
setGroupedStats<T extends string = string>(group: TStatsGroupType, raw: Record<T, TStatsGroupItem>): boolean;
|
|
50
|
+
updateHistory(data: Partial<TGlobalStats>): boolean;
|
|
51
|
+
generateWealthStats(scatter: TScatterItem[]): boolean;
|
|
52
|
+
generateTop10Entry(snapshot: TListSnapshot<TPersonListItem>): boolean;
|
|
53
|
+
generateDBStats(): boolean;
|
|
54
|
+
static getInstance(): IStats;
|
|
55
|
+
static aggregate(data: TProfileData, date: string, stats: any): void;
|
|
56
|
+
}
|