@foul11/awesome-db 1.1.0

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.
Files changed (101) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/BitFields.d.ts +29 -0
  3. package/dist/BitFields.d.ts.map +1 -0
  4. package/dist/Error.d.ts +21 -0
  5. package/dist/Error.d.ts.map +1 -0
  6. package/dist/ORM.d.ts +14 -0
  7. package/dist/ORM.d.ts.map +1 -0
  8. package/dist/SQLParser.d.ts +1394 -0
  9. package/dist/SQLParser.d.ts.map +1 -0
  10. package/dist/WebpackFileProvider.d.ts +12 -0
  11. package/dist/WebpackFileProvider.d.ts.map +1 -0
  12. package/dist/alter/column_add.d.ts +7 -0
  13. package/dist/alter/column_add.d.ts.map +1 -0
  14. package/dist/alter/column_drop.d.ts +6 -0
  15. package/dist/alter/column_drop.d.ts.map +1 -0
  16. package/dist/alter/column_rename.d.ts +6 -0
  17. package/dist/alter/column_rename.d.ts.map +1 -0
  18. package/dist/alter/column_update.d.ts +7 -0
  19. package/dist/alter/column_update.d.ts.map +1 -0
  20. package/dist/alter/columns_order.d.ts +6 -0
  21. package/dist/alter/columns_order.d.ts.map +1 -0
  22. package/dist/alter/index.d.ts +7 -0
  23. package/dist/alter/index.d.ts.map +1 -0
  24. package/dist/alter/pragma.d.ts +4 -0
  25. package/dist/alter/pragma.d.ts.map +1 -0
  26. package/dist/alter/utils.d.ts +6 -0
  27. package/dist/alter/utils.d.ts.map +1 -0
  28. package/dist/defaults.d.ts +2 -0
  29. package/dist/defaults.d.ts.map +1 -0
  30. package/dist/index.d.ts +36 -0
  31. package/dist/index.d.ts.map +1 -0
  32. package/dist/index.mjs +1540 -0
  33. package/dist/index.mjs.map +1 -0
  34. package/dist/indexer.d.ts +12 -0
  35. package/dist/indexer.d.ts.map +1 -0
  36. package/dist/log/access_log.d.ts +7 -0
  37. package/dist/log/access_log.d.ts.map +1 -0
  38. package/dist/log/db.d.ts +6 -0
  39. package/dist/log/db.d.ts.map +1 -0
  40. package/dist/log/index.d.ts +3 -0
  41. package/dist/log/index.d.ts.map +1 -0
  42. package/dist/tables/AccessLog/index.d.ts +79 -0
  43. package/dist/tables/AccessLog/index.d.ts.map +1 -0
  44. package/dist/tables/AccessLog/schema.d.ts +17 -0
  45. package/dist/tables/AccessLog/schema.d.ts.map +1 -0
  46. package/dist/tables/Permission/index.d.ts +43 -0
  47. package/dist/tables/Permission/index.d.ts.map +1 -0
  48. package/dist/tables/Permission/schema.d.ts +12 -0
  49. package/dist/tables/Permission/schema.d.ts.map +1 -0
  50. package/dist/tables/SetString/index.d.ts +10 -0
  51. package/dist/tables/SetString/index.d.ts.map +1 -0
  52. package/dist/tables/SetString/schema.d.ts +7 -0
  53. package/dist/tables/SetString/schema.d.ts.map +1 -0
  54. package/dist/tables/Settings/index.d.ts +42 -0
  55. package/dist/tables/Settings/index.d.ts.map +1 -0
  56. package/dist/tables/Settings/schema.d.ts +8 -0
  57. package/dist/tables/Settings/schema.d.ts.map +1 -0
  58. package/dist/tables/Transaction/index.d.ts +90 -0
  59. package/dist/tables/Transaction/index.d.ts.map +1 -0
  60. package/dist/tables/Transaction/schema.d.ts +16 -0
  61. package/dist/tables/Transaction/schema.d.ts.map +1 -0
  62. package/dist/types/index.d.ts +12 -0
  63. package/dist/types/index.d.ts.map +1 -0
  64. package/dist/utils.d.ts +42 -0
  65. package/dist/utils.d.ts.map +1 -0
  66. package/eslint.config.js +7 -0
  67. package/package.json +54 -0
  68. package/src/BitFields.ts +160 -0
  69. package/src/Error.ts +13 -0
  70. package/src/ORM.ts +49 -0
  71. package/src/SQLParser.js +1204 -0
  72. package/src/WebpackFileProvider.ts +63 -0
  73. package/src/alter/column_add.ts +79 -0
  74. package/src/alter/column_drop.ts +54 -0
  75. package/src/alter/column_rename.ts +55 -0
  76. package/src/alter/column_update.ts +92 -0
  77. package/src/alter/columns_order.ts +60 -0
  78. package/src/alter/index.ts +6 -0
  79. package/src/alter/pragma.ts +10 -0
  80. package/src/alter/utils.ts +70 -0
  81. package/src/defaults.ts +3 -0
  82. package/src/index.ts +227 -0
  83. package/src/indexer.ts +75 -0
  84. package/src/log/access_log.ts +29 -0
  85. package/src/log/db.ts +28 -0
  86. package/src/log/index.ts +2 -0
  87. package/src/tables/AccessLog/index.ts +252 -0
  88. package/src/tables/AccessLog/schema.ts +20 -0
  89. package/src/tables/Permission/index.ts +220 -0
  90. package/src/tables/Permission/schema.ts +13 -0
  91. package/src/tables/SetString/index.ts +45 -0
  92. package/src/tables/SetString/schema.ts +7 -0
  93. package/src/tables/Settings/index.ts +135 -0
  94. package/src/tables/Settings/schema.ts +8 -0
  95. package/src/tables/Transaction/index.ts +343 -0
  96. package/src/tables/Transaction/schema.ts +20 -0
  97. package/src/types/index.ts +33 -0
  98. package/src/utils.ts +48 -0
  99. package/test/sqliteExtExpert.test.ts +39 -0
  100. package/tsconfig.build.json +17 -0
  101. package/tsconfig.json +16 -0
package/src/index.ts ADDED
@@ -0,0 +1,227 @@
1
+ export * from './alter';
2
+ export * from './defaults';
3
+ export * from './Error';
4
+ export * from './indexer';
5
+ export * from './ORM';
6
+ export * from './SQLParser';
7
+ export * from './utils';
8
+ export * from './WebpackFileProvider';
9
+ export * from './log';
10
+
11
+ export type * from './types';
12
+
13
+ import { WebpackFileProvider } from './WebpackFileProvider';
14
+ import { Kysely, SqliteDialect } from 'kysely';
15
+ import { Migrator } from 'kysely/migration';
16
+ import { format as format_sql } from '@foul11/sql-formatter';
17
+ import { isWebpack } from '@foul11/awesome';
18
+ import { v4 } from 'uuid';
19
+
20
+ import Sqlite from 'better-sqlite3';
21
+ import bindings from 'bindings';
22
+ import path from 'path';
23
+
24
+ import type { logger_db } from './log';
25
+ import type { Database } from 'better-sqlite3';
26
+ import type { CompiledQuery } from 'kysely';
27
+
28
+ function create_sqlite(db_path: string): Database {
29
+ const db = new Sqlite(db_path, {
30
+ nativeBinding: isWebpack()
31
+ ? bindings({
32
+ module_root: process.cwd(),
33
+ bindings: 'better_sqlite3.node',
34
+ try: [
35
+ [ 'module_root', 'dist', 'bindings' ],
36
+ [ 'module_root', 'bindings' ],
37
+ [ 'dist', 'bindings' ],
38
+ [ 'bindings' ],
39
+ ],
40
+ path: true,
41
+ } as any)
42
+ : null,
43
+ });
44
+
45
+ db.function('new_uuid', () => {
46
+ return v4();
47
+ });
48
+
49
+ return db;
50
+ }
51
+
52
+ type KyselyDB<DB> = Kysely<DB> & {
53
+ in_migration: boolean
54
+
55
+ migration_log: string[]
56
+ transaction_logs: string[]
57
+ };
58
+
59
+ function format_sqlite_no_throw(log_kysely_db: typeof logger_db, query: CompiledQuery<unknown> | string) {
60
+ const sql = typeof query === 'string'
61
+ ? query
62
+ : query.sql;
63
+
64
+ try {
65
+ return format_sql(sql, {
66
+ language: 'sqlite',
67
+ tabWidth: 4,
68
+ indentStyle: 'tabularRight',
69
+ ...(typeof query !== 'string' && {
70
+ params: query.parameters as any,
71
+ }),
72
+ });
73
+ } catch (e) {
74
+ log_kysely_db.error(e as Error);
75
+ return sql;
76
+ }
77
+ }
78
+
79
+ function create_kysely<DB>(db: unknown, log_kysely_db: typeof logger_db) {
80
+ const db_kysely = new Kysely<DB>({
81
+ dialect: new SqliteDialect({
82
+ database: db as Database,
83
+ }),
84
+ log: (event) => {
85
+ db_kysely.transaction_logs.push(event.query.sql);
86
+
87
+ if (log_kysely_db.isSillyEnabled()) {
88
+ if (!db_kysely.in_migration) {
89
+ log_kysely_db.silly(`\n${format_sqlite_no_throw(log_kysely_db, event.query)}\n`, {
90
+ runtime: event.queryDurationMillis,
91
+ });
92
+ } else {
93
+ db_kysely.migration_log.push(format_sqlite_no_throw(log_kysely_db, event.query));
94
+ }
95
+ }
96
+
97
+ if (event.level === 'error') {
98
+ log_kysely_db.error(event.error, {
99
+ query: db_kysely.transaction_logs.length
100
+ ? db_kysely.transaction_logs.map(sql => format_sqlite_no_throw(log_kysely_db, sql)).join('\n')
101
+ : format_sqlite_no_throw(log_kysely_db, event.query),
102
+ runtime: event.queryDurationMillis,
103
+ });
104
+ }
105
+
106
+ if (/commit|begin|rollback/.test(event.query.sql)) {
107
+ db_kysely.transaction_logs = [];
108
+ }
109
+ },
110
+ }) as KyselyDB<DB>;
111
+
112
+ /**
113
+ * Nested transaction hack
114
+ * Catch execute method,
115
+ * replace arg in func to proxy transaction,
116
+ * method transaction() return this transaction
117
+ */
118
+ function proxy_transaction(trx: any) {
119
+ return new Proxy(trx, {
120
+ get(target: any, prop) {
121
+ if (prop === 'transaction')
122
+ return () => ({
123
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
124
+ execute: (fn: Function) => (
125
+ fn.apply(target, [ target ])
126
+ ),
127
+ });
128
+
129
+ if (typeof target[prop] == 'function') {
130
+ return target[prop].bind(target);
131
+ } else {
132
+ return target[prop];
133
+ }
134
+ },
135
+ });
136
+ }
137
+
138
+ const old_transaction = db_kysely.transaction;
139
+ db_kysely.transaction = function (...trx_args) {
140
+ const transaction = old_transaction.call(this, ...trx_args);
141
+
142
+ return new Proxy(transaction, {
143
+ get(target: any, prop) {
144
+ if (prop === 'execute')
145
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
146
+ return (fn: Function) => (
147
+ target.execute((trx: any, ...args: any[]) => (
148
+ fn.apply(target, [ proxy_transaction(trx), ...args ])
149
+ ))
150
+ );
151
+
152
+ if (typeof target[prop] == 'function') {
153
+ return target[prop].bind(target);
154
+ } else {
155
+ return target[prop];
156
+ }
157
+ },
158
+ });
159
+ };
160
+
161
+ return db_kysely;
162
+ }
163
+
164
+ async function create_migrator<DB>(db_name: string, db: KyselyDB<DB>, log_kysely_db: typeof logger_db) {
165
+ db.in_migration = true;
166
+ db.migration_log = [];
167
+ db.transaction_logs = [];
168
+
169
+ const migrator = new Migrator({
170
+ db, provider: new WebpackFileProvider(db_name),
171
+ });
172
+
173
+ async function migrateToLatest() {
174
+ const { error, results } = await migrator.migrateToLatest();
175
+
176
+ for (const result of results ?? []) {
177
+ if (result.status === 'Success') {
178
+ log_kysely_db.info(`migration "${result.migrationName}" was executed successfully`);
179
+ } else if (result.status === 'Error') {
180
+ log_kysely_db.error(`failed to execute migration "${result.migrationName}"`);
181
+ }
182
+ }
183
+
184
+ if (error) {
185
+ if (!(error instanceof Error))
186
+ throw error as any;
187
+
188
+ log_kysely_db.error('failed to migrate');
189
+ log_kysely_db.error(error.stack);
190
+
191
+ if (log_kysely_db.isDebugEnabled()) {
192
+ log_kysely_db.debug(`\n${db.migration_log.join('\n-------------- NEXT MIGRATION SQL-request --------------\n')}\n`, {});
193
+
194
+ // eslint-disable-next-line no-debugger
195
+ debugger;
196
+ }
197
+
198
+ throw error;
199
+ }
200
+ }
201
+
202
+ log_kysely_db.debug('starting migration to latest');
203
+ await migrateToLatest();
204
+ log_kysely_db.debug('migration to latest completed');
205
+
206
+ db.in_migration = false;
207
+ db.migration_log = [];
208
+
209
+ return migrator;
210
+ }
211
+
212
+ export async function create_db<DB>(db_path: string, log_kysely: typeof logger_db) {
213
+ const db_name = path.parse(db_path).name;
214
+ const db_sqlite = create_sqlite(db_path) as unknown;
215
+ const db_log = log_kysely.child({ label5: db_name });
216
+ const db = create_kysely<DB>(db_sqlite, db_log);
217
+ const db_migrator = await create_migrator(db_name, db, db_log);
218
+
219
+ return {
220
+ db: db as Kysely<DB>,
221
+ db_migrator,
222
+ db_sqlite,
223
+ db_log,
224
+ };
225
+ }
226
+
227
+ export const SqliteError = Sqlite.SqliteError;
package/src/indexer.ts ADDED
@@ -0,0 +1,75 @@
1
+ import { table_info, master_table } from './utils';
2
+
3
+ import type { ValueOf } from 'type-fest';
4
+ import type { Kysely, AnyColumn } from 'kysely';
5
+
6
+ export async function indexer<TDB>(
7
+ db: Kysely<TDB>,
8
+ options: {
9
+ exclude?: ValueOf<{ [TB in keyof TDB]: TB extends string ? [ TB, (true | AnyColumn<TDB, TB>[]) ] : never }>[]
10
+ include?: ValueOf<{ [TB in keyof TDB]: TB extends string ? [ TB, (true | AnyColumn<TDB, TB>[]) ] : never }>[]
11
+ cleanupOthers?: boolean
12
+ },
13
+ ) {
14
+ const master = await master_table(db);
15
+
16
+ const idx_exclude = new Map<string, true | string[]>(options.exclude || []);
17
+ const idx_include = new Map<string, true | string[]>(options.include || []);
18
+ const cleanupOthers = options.cleanupOthers || false;
19
+
20
+ const tables = master
21
+ .filter(r => r.type == 'table')
22
+ .filter(r => !options.include || idx_include.has(r.name))
23
+ .map(r => r.name);
24
+
25
+ const indexes = new Set(master
26
+ .filter(r => r.type == 'index')
27
+ .map(r => r.name),
28
+ );
29
+
30
+ const isExcluded = (table: string, column: { name: string }) => {
31
+ const exclude = idx_exclude.get(table);
32
+
33
+ if (exclude === true || exclude?.includes(column.name))
34
+ return true;
35
+
36
+ return false;
37
+ };
38
+
39
+ const isIncluded = (table: string, column: { name: string }) => {
40
+ const include = idx_include.get(table);
41
+
42
+ if (options.include && !(include == true || include?.includes(column.name)))
43
+ return false;
44
+
45
+ return true;
46
+ };
47
+
48
+ for (const table of tables) {
49
+ const info = await table_info(db, table as any);
50
+
51
+ for (const column of info) {
52
+ const idx_name = `autoIdx_${table}_${column.name}`;
53
+ const skip = !isIncluded(table, column) || isExcluded(table, column) || column.pk;
54
+
55
+ if (indexes.has(idx_name)) {
56
+ if (cleanupOthers && skip) {
57
+ await db.schema
58
+ .dropIndex(idx_name)
59
+ .execute();
60
+ }
61
+
62
+ continue;
63
+ }
64
+
65
+ if (skip)
66
+ continue;
67
+
68
+ await db.schema
69
+ .createIndex(idx_name)
70
+ .on(table)
71
+ .column(column.name)
72
+ .execute();
73
+ }
74
+ }
75
+ }
@@ -0,0 +1,29 @@
1
+ /* eslint-disable @stylistic/indent-binary-ops */
2
+
3
+ import { f_printf, color, loggers, pretty_message, pretty_ms } from '@foul11/awesome-log';
4
+ import type { AccessLogStoreData } from '../tables/AccessLog';
5
+
6
+ const logger_access_log = loggers.add<{
7
+ data?: AccessLogStoreData | undefined
8
+ runtime?: number | bigint | undefined
9
+ }>('access_log', {
10
+ format: f_printf(info => (``
11
+ + `${pretty_message(info, { disable_empty: true })}`
12
+ + `${info.runtime
13
+ ? color('ms', ` took ${pretty_ms(info.runtime).padStart(11, ' ')}`)
14
+ : ' '.repeat(6 + 11)
15
+ }`
16
+ + `${info.data
17
+ ? ` (${color('string', `'${info.data.action}'`)}, total: ${color('ms', pretty_ms(info.data.timings.total / 1e6).padStart(11, ' '))})`
18
+ : ``
19
+ }`
20
+ ).trim(), {
21
+ meta: { label: 'access_log' },
22
+ sanitize: [
23
+ 'data',
24
+ 'runtime',
25
+ ],
26
+ }),
27
+ });
28
+
29
+ export default logger_access_log;
package/src/log/db.ts ADDED
@@ -0,0 +1,28 @@
1
+ /* eslint-disable @stylistic/indent-binary-ops */
2
+
3
+ import { f_printf, color, loggers, pretty_message, pretty_ms } from '@foul11/awesome-log';
4
+
5
+ const logger_db = loggers.add<{
6
+ query?: string | undefined
7
+ runtime?: number | bigint | undefined
8
+ }>('db', {
9
+ format: f_printf(info => (``
10
+ + `${pretty_message(info, { disable_empty: true })}`
11
+ + `${info.query
12
+ ? `\n${info.query}\n`
13
+ : ``
14
+ }`
15
+ + `${info.runtime
16
+ ? color('string', `(runtime: ${color('ms', pretty_ms(info.runtime))})`)
17
+ : ``
18
+ }`
19
+ ).trim(), {
20
+ meta: { label: 'db' },
21
+ sanitize: [
22
+ 'query',
23
+ 'runtime',
24
+ ],
25
+ }),
26
+ });
27
+
28
+ export default logger_db;
@@ -0,0 +1,2 @@
1
+ export { default as logger_access_log } from './access_log';
2
+ export { default as logger_db } from './db';
@@ -0,0 +1,252 @@
1
+ export * from './schema';
2
+
3
+ import deepmerge from 'deepmerge';
4
+
5
+ import { SetString } from '../SetString';
6
+ import { BitFields } from '../../BitFields';
7
+ import { master_table } from '../../utils';
8
+ import { ORM } from '../../ORM';
9
+
10
+ import type { logger_access_log } from '../../log';
11
+
12
+ import type { Kysely } from 'kysely';
13
+ import type { BitFieldsUnpack } from '../../BitFields';
14
+ import type { DBTablesByType } from '../../types';
15
+ import type { TableSetString } from '../SetString';
16
+ import type { TableAccessLog, TableAccessLogTiming } from './schema';
17
+
18
+ type DefaultKeyAccessLog = 'access_log';
19
+ interface DefaultKyselyAccessLog {
20
+ access_log: TableAccessLog
21
+ access_log_set_ip: TableSetString
22
+ access_log_set_ua: TableSetString
23
+ access_log_set_action: TableSetString
24
+ access_log_timing: TableAccessLogTiming
25
+ access_log_set_timing: TableSetString
26
+ }
27
+
28
+ export interface AccessLogStoreData {
29
+ action: string
30
+ flag: number
31
+ ip?: string | undefined
32
+ meta?: string | undefined
33
+ user_agent?: string | undefined
34
+ timings: Record<string, number> & {
35
+ total: number
36
+ exclude: number
37
+ }
38
+ extended: Record<string, any>
39
+ }
40
+
41
+ export interface AccessLogStore {
42
+ store(data: AccessLogStoreData): Promise<void>
43
+ }
44
+
45
+ export class AccessLogStoreDB implements AccessLogStore {
46
+ protected readonly db: Kysely<DefaultKyselyAccessLog>;
47
+ protected readonly domain: DefaultKeyAccessLog;
48
+ protected readonly logger: typeof logger_access_log | undefined;
49
+
50
+ private constructor(db: any, domain: any, logger?: typeof logger_access_log) {
51
+ this.db = db;
52
+ this.domain = domain;
53
+ this.logger = logger;
54
+ }
55
+
56
+ static async create<
57
+ DB,
58
+ Domain extends DBTablesByType<DB, TableAccessLog>,
59
+ >(
60
+ db: Kysely<DB>,
61
+ domain: Domain,
62
+ logger?: typeof logger_access_log,
63
+ ) {
64
+ const master = await master_table(db);
65
+ const tables = [
66
+ `${domain}`,
67
+ `${domain}_set_ip`,
68
+ `${domain}_set_ua`,
69
+ `${domain}_set_action`,
70
+ `${domain}_timing`,
71
+ `${domain}_set_timing`,
72
+ ];
73
+
74
+ for (const table of tables) {
75
+ const info = master.find(r => r.name == table && r.type == 'table');
76
+
77
+ if (!info)
78
+ throw new Error(`Table ${table} not found, can't create access log store`);
79
+ }
80
+
81
+ return new AccessLogStoreDB(db, domain, logger);
82
+ }
83
+
84
+ async store(data: AccessLogStoreData) {
85
+ const perf_db_start = process.hrtime.bigint();
86
+ await this.db
87
+ .transaction()
88
+ .execute(async (trx) => {
89
+ const access_log_set_ip_id = await SetString.try_store_value_or_throw<DefaultKyselyAccessLog>(data.ip, `${this.domain}_set_ip`, trx);
90
+ const access_log_set_ua_id = await SetString.try_store_value_or_throw<DefaultKyselyAccessLog>(data.user_agent, `${this.domain}_set_ua`, trx);
91
+ const access_log_set_action_id = await SetString.store_value_or_throw<DefaultKyselyAccessLog>(data.action, `${this.domain}_set_action`, trx);
92
+
93
+ const access_log_id = (
94
+ await trx
95
+ .insertInto(`${this.domain}`)
96
+ .returning('id')
97
+ .values({
98
+ flag: data.flag,
99
+ meta: data.meta,
100
+ access_log_set_ip_id,
101
+ access_log_set_ua_id,
102
+ access_log_set_action_id,
103
+ ...data.extended,
104
+ })
105
+ .executeTakeFirstOrThrow()
106
+ ).id;
107
+
108
+ for (const [ key, value ] of Object.entries(data.timings)) {
109
+ const access_log_set_timing_id = await SetString.store_value_or_throw<DefaultKyselyAccessLog>(key, `${this.domain}_set_timing`, trx);
110
+
111
+ await trx
112
+ .insertInto(`${this.domain}_timing`)
113
+ .values({
114
+ access_log_id,
115
+ access_log_set_timing_id,
116
+ timing_ns: value,
117
+ })
118
+ .executeTakeFirstOrThrow();
119
+ }
120
+ });
121
+ const perf_db_end = process.hrtime.bigint();
122
+
123
+ this.logger?.silly(`AccessLogStoreDB.store`, {
124
+ runtime: Number(perf_db_end - perf_db_start) / 1e6,
125
+ data,
126
+ });
127
+ }
128
+ }
129
+
130
+ export class AccessLogStoreDBDummy implements AccessLogStore {
131
+ static async create(..._: any[]) {
132
+ return new AccessLogStoreDBDummy();
133
+ }
134
+
135
+ async store(..._: any[]) { /* NOOP */ }
136
+ }
137
+
138
+ class AccessLogTransaction<
139
+ T extends AccessLogInfo,
140
+ > extends ORM {
141
+ protected readonly access_log: AccessLog<T>;
142
+ protected committed = false;
143
+
144
+ protected readonly action: string;
145
+ protected info: AccessLogInfo & T;
146
+
147
+ protected readonly timings_start: Record<string, bigint> = {};
148
+ protected readonly timings_duration: Record<string, bigint> = {};
149
+
150
+ protected readonly perf_total_start: bigint;
151
+
152
+ constructor(access_log: AccessLog<T>, action: string, info: AccessLogInfo & T) {
153
+ super();
154
+
155
+ this.perf_total_start = process.hrtime.bigint();
156
+ this.access_log = access_log;
157
+ this.action = action;
158
+ this.info = info;
159
+ }
160
+
161
+ perf_start(name: string): this {
162
+ if (this.committed)
163
+ return this;
164
+
165
+ this.timings_start[name] = process.hrtime.bigint();
166
+ return this;
167
+ }
168
+
169
+ perf_stop(name: string): this {
170
+ if (this.committed)
171
+ return this;
172
+
173
+ const start = this.timings_start[name];
174
+ const end = process.hrtime.bigint();
175
+
176
+ if (!start)
177
+ return this;
178
+
179
+ this.timings_start[name] = end;
180
+ this.timings_duration[name] = (this.timings_duration[name] || 0n) + (end - start);
181
+ return this;
182
+ }
183
+
184
+ perf(name: string) {
185
+ return this.timings_duration[name];
186
+ }
187
+
188
+ update_info(info: AccessLogInfo & T) {
189
+ this.info = deepmerge<AccessLogInfo & T>(this.info, info);
190
+ }
191
+
192
+ async commit() {
193
+ if (this.committed)
194
+ return;
195
+
196
+ this.committed = true;
197
+ const { flag, ip, user_agent, ...extended } = this.info;
198
+
199
+ const perf_total = process.hrtime.bigint() - this.perf_total_start;
200
+ const perf_exclude = perf_total - Object.values(this.timings_duration).reduce((a, b) => a + b, 0n);
201
+
202
+ return this.access_log.store.store({
203
+ action: this.action,
204
+ flag: this.access_log.flags.pack(flag ?? {}),
205
+ timings: {
206
+ total: Number(perf_total),
207
+ exclude: Number(perf_exclude),
208
+ ...this.timings_duration,
209
+ },
210
+ ip,
211
+ user_agent,
212
+ extended,
213
+ });
214
+ }
215
+ }
216
+
217
+ const accessLogDefaultFlags = {
218
+ rejected: 0,
219
+ internal: 1,
220
+ } as const;
221
+
222
+ type AccessLogInfo = {
223
+ flag?: BitFieldsUnpack<typeof accessLogDefaultFlags>
224
+ ip?: string | undefined
225
+ user_agent?: string | undefined
226
+ };
227
+
228
+ export class AccessLog<
229
+ T extends AccessLogInfo,
230
+ // Flags extends FieldFlagsInObj<IntRange<16, 32>>>
231
+ > extends ORM {
232
+ public readonly flags: BitFields<typeof accessLogDefaultFlags>;
233
+ public readonly store: AccessLogStore;
234
+
235
+ protected constructor(store: AccessLogStore) {
236
+ super();
237
+
238
+ this.store = store;
239
+ this.flags = new BitFields(accessLogDefaultFlags);
240
+ }
241
+
242
+ static new<T extends Record<string, any>>(options: {
243
+ store: AccessLogStore
244
+ // flags: Flags
245
+ }) {
246
+ return new AccessLog<T & AccessLogInfo>(options.store);
247
+ }
248
+
249
+ begin(action: string, info: T) {
250
+ return new AccessLogTransaction(this, action, info);
251
+ }
252
+ }
@@ -0,0 +1,20 @@
1
+ import type { Generated, GeneratedAlways } from 'kysely';
2
+
3
+ export type TableAccessLog<
4
+ AdditionalFields extends Record<string, unknown> = Record<string, unknown>,
5
+ > = AdditionalFields & {
6
+ id: Generated<number>
7
+ flag: number // BitField
8
+ access_log_set_ip_id: number | null // fk, TableSetString, IP
9
+ access_log_set_ua_id: number | null // fk, TableSetString, UserAgent
10
+ access_log_set_action_id: number // fk, TableSetString, Совершенное действие, название endpoint или любой другой индификатор
11
+ meta: string | null // JSON, дополнительная информация
12
+ created_at: GeneratedAlways<string>
13
+ };
14
+
15
+ export interface TableAccessLogTiming {
16
+ id: Generated<number>
17
+ access_log_id: number // fk, TableAccessLog
18
+ access_log_set_timing_id: number // fk, TableSetString
19
+ timing_ns: number // ns, время затраченное на этот этап
20
+ }