@travetto/model-sql 2.1.3 → 2.2.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.
package/README.md CHANGED
@@ -20,7 +20,7 @@ or
20
20
  npm install pg
21
21
  ```
22
22
 
23
- This module provides a [SQL](https://en.wikipedia.org/wiki/SQL)-based implementation for the [Data Modeling Support](https://github.com/travetto/travetto/tree/main/module/model#readme "Datastore abstraction for core operations.") module. This source allows the [Data Modeling Support](https://github.com/travetto/travetto/tree/main/module/model#readme "Datastore abstraction for core operations.") module to read, write and query against [SQL](https://en.wikipedia.org/wiki/SQL) databases. In development mode, the [SQLModelService](https://github.com/travetto/travetto/tree/main/module/model-sql/src/service.ts#L36) will also modify the database schema in real time to minimize impact to development.
23
+ This module provides a [SQL](https://en.wikipedia.org/wiki/SQL)-based implementation for the [Data Modeling Support](https://github.com/travetto/travetto/tree/main/module/model#readme "Datastore abstraction for core operations.") module. This source allows the [Data Modeling Support](https://github.com/travetto/travetto/tree/main/module/model#readme "Datastore abstraction for core operations.") module to read, write and query against [SQL](https://en.wikipedia.org/wiki/SQL) databases. In development mode, the [SQLModelService](https://github.com/travetto/travetto/tree/main/module/model-sql/src/service.ts#L38) will also modify the database schema in real time to minimize impact to development.
24
24
 
25
25
  The schema generated will not generally map to existing tables as it is attempting to produce a document store like experience on top of
26
26
  a [SQL](https://en.wikipedia.org/wiki/SQL) database. Every table generated will have a `path_id` which determines it's location in the document hierarchy as well as sub tables will have a `parent_path_id` to associate records with the parent values.
@@ -34,7 +34,7 @@ The current SQL client support stands at:
34
34
 
35
35
  **Note**: Wider client support will roll out as usage increases.
36
36
 
37
- Supported featrues:
37
+ Supported features:
38
38
 
39
39
  * [CRUD](https://github.com/travetto/travetto/tree/main/module/model/src/service/crud.ts#L11)
40
40
  * [Bulk](https://github.com/travetto/travetto/tree/main/module/model/src/service/bulk.ts#L23)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@travetto/model-sql",
3
3
  "displayName": "SQL Model Service",
4
- "version": "2.1.3",
4
+ "version": "2.2.0",
5
5
  "description": "SQL backing for the travetto model module, with real-time modeling support for SQL schemas.",
6
6
  "keywords": [
7
7
  "sql",
@@ -28,20 +28,20 @@
28
28
  "directory": "module/model-sql"
29
29
  },
30
30
  "dependencies": {
31
- "@travetto/config": "^2.1.3",
32
- "@travetto/context": "^2.1.3",
33
- "@travetto/model": "^2.1.3",
34
- "@travetto/model-query": "2.1.3"
31
+ "@travetto/config": "^2.2.0",
32
+ "@travetto/context": "^2.2.0",
33
+ "@travetto/model": "^2.2.0",
34
+ "@travetto/model-query": "2.2.0"
35
35
  },
36
36
  "devDependencies": {
37
- "@travetto/app": "^2.1.3"
37
+ "@travetto/app": "^2.2.0"
38
38
  },
39
39
  "optionalPeerDependencies": {
40
40
  "@types/mysql": "^2.15.21",
41
41
  "@types/pg": "^8.6.5",
42
42
  "mysql": "^2.18.1",
43
43
  "pg": "^8.7.3",
44
- "better-sqlite3": "^7.5.3",
44
+ "better-sqlite3": "^7.6.2",
45
45
  "@types/better-sqlite3": "^7.5.0"
46
46
  },
47
47
  "publishConfig": {
@@ -24,14 +24,14 @@ export abstract class Connection<C = unknown> {
24
24
  * Get active connection
25
25
  */
26
26
  get active(): C {
27
- return this.context.get(ContextActiveⲐ) as C;
27
+ return this.context.get<C>(ContextActiveⲐ);
28
28
  }
29
29
 
30
30
  /**
31
31
  * Get active tx state
32
32
  */
33
- get activeTx() {
34
- return !!this.context.get(TxActiveⲐ) as boolean;
33
+ get activeTx(): boolean {
34
+ return !!this.context.get<boolean>(TxActiveⲐ);
35
35
  }
36
36
 
37
37
  /**
@@ -85,7 +85,7 @@ export abstract class Connection<C = unknown> {
85
85
  * @param op
86
86
  * @param args
87
87
  */
88
- async * iterateWithActive<R>(op: () => AsyncGenerator<R>) {
88
+ async * iterateWithActive<R>(op: () => AsyncGenerator<R>): AsyncIterable<R> {
89
89
  if (this.active) {
90
90
  yield* op();
91
91
  }
@@ -134,7 +134,7 @@ export abstract class Connection<C = unknown> {
134
134
  /**
135
135
  * Start a transaction
136
136
  */
137
- async startTx(conn: C, transactionId?: string) {
137
+ async startTx(conn: C, transactionId?: string): Promise<void> {
138
138
  if (transactionId) {
139
139
  if (this.nestedTransactions) {
140
140
  await this.execute(conn, `SAVEPOINT ${transactionId};`);
@@ -150,7 +150,7 @@ export abstract class Connection<C = unknown> {
150
150
  /**
151
151
  * Commit active transaction
152
152
  */
153
- async commitTx(conn: C, transactionId?: string) {
153
+ async commitTx(conn: C, transactionId?: string): Promise<void> {
154
154
  if (transactionId) {
155
155
  if (this.nestedTransactions) {
156
156
  await this.execute(conn, `RELEASE SAVEPOINT ${transactionId};`);
@@ -163,7 +163,7 @@ export abstract class Connection<C = unknown> {
163
163
  /**
164
164
  * Rollback active transaction
165
165
  */
166
- async rollbackTx(conn: C, transactionId?: string) {
166
+ async rollbackTx(conn: C, transactionId?: string): Promise<void> {
167
167
  if (transactionId) {
168
168
  if (this.isolatedTransactions) {
169
169
  await this.execute(conn, `ROLLBACK TO ${transactionId};`);
@@ -12,12 +12,12 @@ export interface ConnectionAware<C = unknown> {
12
12
  * Decorator to ensure a method runs with a valid connection
13
13
  */
14
14
  export function Connected<T extends ConnectionAware>() {
15
- return function (target: T, prop: string | symbol, desc: MethodDescriptor) {
15
+ return function (target: T, prop: string | symbol, desc: MethodDescriptor): void {
16
16
  const og = desc.value!;
17
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
18
- desc.value = async function (this: any, ...args: any[]) {
17
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
18
+ desc.value = async function (this: T, ...args: unknown[]) {
19
19
  return this.conn.runWithActive(() => og.call(this, ...args));
20
- };
20
+ } as typeof og;
21
21
  };
22
22
  }
23
23
 
@@ -25,12 +25,12 @@ export function Connected<T extends ConnectionAware>() {
25
25
  * Decorator to ensure a method runs with a valid connection
26
26
  */
27
27
  export function ConnectedIterator<T extends ConnectionAware>() {
28
- return function (target: T, prop: string | symbol, desc: MethodDescriptor) {
28
+ return function (target: T, prop: string | symbol, desc: MethodDescriptor): void {
29
29
  const og = desc.value!;
30
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
31
- desc.value = async function* (this: any, ...args: any[]) {
30
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
31
+ desc.value = async function* (this: T, ...args: unknown[]) {
32
32
  yield* this.conn.iterateWithActive(() => og.call(this, ...args));
33
- };
33
+ } as typeof og;
34
34
  };
35
35
  }
36
36
 
@@ -38,11 +38,11 @@ export function ConnectedIterator<T extends ConnectionAware>() {
38
38
  * Decorator to ensure a method runs with a valid transaction
39
39
  */
40
40
  export function Transactional<T extends ConnectionAware>(mode: TransactionType = 'required') {
41
- return function (target: T, prop: string | symbol, desc: MethodDescriptor) {
41
+ return function (target: T, prop: string | symbol, desc: MethodDescriptor): void {
42
42
  const og = desc.value!;
43
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
44
- desc.value = function (this: any, ...args: any[]) {
43
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
44
+ desc.value = function (this: T, ...args: unknown[]) {
45
45
  return this.conn.runWithTransaction(mode, () => og.call(this, ...args));
46
- };
46
+ } as typeof og;
47
47
  };
48
48
  }
@@ -10,8 +10,11 @@ import { SQLUtil, VisitStack } from '../internal/util';
10
10
  import { DeleteWrapper, InsertWrapper, DialectState } from '../internal/types';
11
11
  import { Connection } from '../connection/base';
12
12
 
13
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
13
14
  const has$And = (o: unknown): o is ({ $and: WhereClause<unknown>[] }) => !!o && '$and' in (o as object);
15
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
14
16
  const has$Or = (o: unknown): o is ({ $or: WhereClause<unknown>[] }) => !!o && '$or' in (o as object);
17
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
15
18
  const has$Not = (o: unknown): o is ({ $not: WhereClause<unknown> }) => !!o && '$not' in (o as object);
16
19
 
17
20
  interface Alias {
@@ -24,15 +27,16 @@ class Total {
24
27
  total: number;
25
28
  }
26
29
 
27
- function makeField(name: string, type: Class, required: boolean, extra: Partial<FieldConfig>) {
30
+ function makeField(name: string, type: Class, required: boolean, extra: Partial<FieldConfig>): FieldConfig {
28
31
  return {
29
32
  name,
30
- owner: null,
33
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
34
+ owner: null as unknown as Class,
31
35
  type,
32
36
  array: false,
33
37
  ...(required ? { required: { active: true } } : {}),
34
38
  ...extra
35
- } as FieldConfig;
39
+ };
36
40
  }
37
41
 
38
42
  /**
@@ -81,7 +85,7 @@ export abstract class SQLDialect implements DialectState {
81
85
  BOOLEAN: 'BOOLEAN',
82
86
  TINYINT: 'TINYINT',
83
87
  SMALLINT: 'SMALLINT',
84
- MEDIUMINIT: 'MEDIUMINT',
88
+ MEDIUMINT: 'MEDIUMINT',
85
89
  INT: 'INT',
86
90
  BIGINT: 'BIGINT',
87
91
  TIMESTAMP: 'TIMESTAMP',
@@ -91,7 +95,7 @@ export abstract class SQLDialect implements DialectState {
91
95
  /**
92
96
  * Column types with inputs
93
97
  */
94
- PARAMETERIZED_COLUMN_TYPES: Record<'VARCHAR' | 'DECIMAL', (...nums: number[]) => string> = {
98
+ PARAMETERIZED_COLUMN_TYPES: Record<'VARCHAR' | 'DECIMAL', (...values: number[]) => string> = {
95
99
  VARCHAR: n => `VARCHAR(${n})`,
96
100
  DECIMAL: (d, p) => `DECIMAL(${d},${p})`
97
101
  };
@@ -153,7 +157,7 @@ export abstract class SQLDialect implements DialectState {
153
157
  */
154
158
  abstract hash(inp: string): string;
155
159
 
156
- executeSQL<T>(sql: string) {
160
+ executeSQL<T>(sql: string): Promise<{ records: T[], count: number }> {
157
161
  return this.conn.execute<T>(this.conn.active, sql);
158
162
  }
159
163
 
@@ -171,15 +175,15 @@ export abstract class SQLDialect implements DialectState {
171
175
  * @param value
172
176
  * @returns
173
177
  */
174
- resolveDateValue(value: Date) {
175
- const [day, time] = (value as Date).toISOString().split(/[TZ]/);
178
+ resolveDateValue(value: Date): string {
179
+ const [day, time] = value.toISOString().split(/[TZ]/);
176
180
  return this.quote(`${day} ${time}`);
177
181
  }
178
182
 
179
183
  /**
180
184
  * Convert value to SQL valid representation
181
185
  */
182
- resolveValue(conf: FieldConfig, value: unknown) {
186
+ resolveValue(conf: FieldConfig, value: unknown): string {
183
187
  if (value === undefined || value === null) {
184
188
  return 'NULL';
185
189
  } else if (conf.type === String) {
@@ -187,6 +191,7 @@ export abstract class SQLDialect implements DialectState {
187
191
  const src = Util.toRegex(value).source.replace(/\\b/g, this.regexWordBoundary);
188
192
  return this.quote(src);
189
193
  } else {
194
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
190
195
  return this.quote(value as string);
191
196
  }
192
197
  } else if (conf.type === Boolean) {
@@ -194,6 +199,7 @@ export abstract class SQLDialect implements DialectState {
194
199
  } else if (conf.type === Number) {
195
200
  return `${value}`;
196
201
  } else if (conf.type === Date) {
202
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
197
203
  return this.resolveDateValue(ModelQueryUtil.resolveComparator(value) as Date);
198
204
  } else if (conf.type === PointImpl && Array.isArray(value)) {
199
205
  return `point(${value[0]},${value[1]})`;
@@ -206,7 +212,7 @@ export abstract class SQLDialect implements DialectState {
206
212
  /**
207
213
  * Get column type from field config
208
214
  */
209
- getColumnType(conf: FieldConfig) {
215
+ getColumnType(conf: FieldConfig): string {
210
216
  let type: string = '';
211
217
 
212
218
  if (conf.type === Number) {
@@ -221,7 +227,7 @@ export abstract class SQLDialect implements DialectState {
221
227
  } else if (digits < 5) {
222
228
  type = this.COLUMN_TYPES.SMALLINT;
223
229
  } else if (digits < 7) {
224
- type = this.COLUMN_TYPES.MEDIUMINIT;
230
+ type = this.COLUMN_TYPES.MEDIUMINT;
225
231
  } else if (digits < 10) {
226
232
  type = this.COLUMN_TYPES.INT;
227
233
  } else {
@@ -253,7 +259,7 @@ export abstract class SQLDialect implements DialectState {
253
259
  /**
254
260
  * FieldConfig to Column definition
255
261
  */
256
- getColumnDefinition(conf: FieldConfig) {
262
+ getColumnDefinition(conf: FieldConfig): string | undefined {
257
263
  const type = this.getColumnType(conf);
258
264
  if (!type) {
259
265
  return;
@@ -264,7 +270,8 @@ export abstract class SQLDialect implements DialectState {
264
270
  /**
265
271
  * Delete query and return count removed
266
272
  */
267
- async deleteAndGetCount<T>(cls: Class<T>, query: Query<T>) {
273
+ async deleteAndGetCount<T>(cls: Class<T>, query: Query<T>): Promise<number> {
274
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
268
275
  const { count } = await this.executeSQL(this.getDeleteSQL(SQLUtil.classToStack(cls), query.where as WhereClause<T>));
269
276
  return count;
270
277
  }
@@ -272,7 +279,7 @@ export abstract class SQLDialect implements DialectState {
272
279
  /**
273
280
  * Get the count for a given query
274
281
  */
275
- async getCountForQuery<T>(cls: Class<T>, query: Query<T>) {
282
+ async getCountForQuery<T>(cls: Class<T>, query: Query<T>): Promise<number> {
276
283
  const { records } = await this.executeSQL<{ total: number }>(this.getQueryCountSQL(cls, query));
277
284
  const [record] = records;
278
285
  return Total.from(record).total;
@@ -281,7 +288,7 @@ export abstract class SQLDialect implements DialectState {
281
288
  /**
282
289
  * Remove a sql column
283
290
  */
284
- getDropColumnSQL(stack: VisitStack[]) {
291
+ getDropColumnSQL(stack: VisitStack[]): string {
285
292
  const field = stack[stack.length - 1];
286
293
  return `ALTER TABLE ${this.parentTable(stack)} DROP COLUMN ${this.ident(field.name)};`;
287
294
  }
@@ -289,8 +296,9 @@ export abstract class SQLDialect implements DialectState {
289
296
  /**
290
297
  * Add a sql column
291
298
  */
292
- getAddColumnSQL(stack: VisitStack[]) {
299
+ getAddColumnSQL(stack: VisitStack[]): string {
293
300
  const field = stack[stack.length - 1];
301
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
294
302
  return `ALTER TABLE ${this.parentTable(stack)} ADD COLUMN ${this.getColumnDefinition(field as FieldConfig)};`;
295
303
  }
296
304
 
@@ -309,49 +317,49 @@ export abstract class SQLDialect implements DialectState {
309
317
  /**
310
318
  * Determine table/field namespace for a given stack location
311
319
  */
312
- namespace(stack: VisitStack[]) {
320
+ namespace(stack: VisitStack[]): string {
313
321
  return `${this.ns}${SQLUtil.buildTable(stack)}`;
314
322
  }
315
323
 
316
324
  /**
317
325
  * Determine namespace for a given stack location - 1
318
326
  */
319
- namespaceParent(stack: VisitStack[]) {
327
+ namespaceParent(stack: VisitStack[]): string {
320
328
  return this.namespace(stack.slice(0, stack.length - 1));
321
329
  }
322
330
 
323
331
  /**
324
332
  * Determine table name for a given stack location
325
333
  */
326
- table(stack: VisitStack[]) {
334
+ table(stack: VisitStack[]): string {
327
335
  return this.ident(this.namespace(stack));
328
336
  }
329
337
 
330
338
  /**
331
339
  * Determine parent table name for a given stack location
332
340
  */
333
- parentTable(stack: VisitStack[]) {
341
+ parentTable(stack: VisitStack[]): string {
334
342
  return this.table(stack.slice(0, stack.length - 1));
335
343
  }
336
344
 
337
345
  /**
338
346
  * Get lookup key for cls and name
339
347
  */
340
- getKey(cls: Class, name: string) {
348
+ getKey(cls: Class, name: string): string {
341
349
  return `${cls.name}:${name}`;
342
350
  }
343
351
 
344
352
  /**
345
353
  * Alias a field for usage
346
354
  */
347
- alias(field: string | FieldConfig, alias: string = this.rootAlias) {
355
+ alias(field: string | FieldConfig, alias: string = this.rootAlias): string {
348
356
  return `${alias}.${this.ident(field)}`;
349
357
  }
350
358
 
351
359
  /**
352
360
  * Get alias cache for the stack
353
361
  */
354
- getAliasCache(stack: VisitStack[], resolve: (path: VisitStack[]) => string) {
362
+ getAliasCache(stack: VisitStack[], resolve: (path: VisitStack[]) => string): Map<string, Alias> {
355
363
  const cls = stack[0].type;
356
364
 
357
365
  if (this.aliasCache.has(cls)) {
@@ -402,6 +410,7 @@ export abstract class SQLDialect implements DialectState {
402
410
  const { foreignMap, localMap } = SQLUtil.getFieldsByLocation(stack);
403
411
  const SQL_OPS = this.SQL_OPS;
404
412
 
413
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
405
414
  for (const key of Object.keys(o) as ((keyof (typeof o)))[]) {
406
415
  const top = o[key];
407
416
  const field = localMap[key] ?? foreignMap[key];
@@ -437,18 +446,19 @@ export abstract class SQLDialect implements DialectState {
437
446
  const arr = [...new Set(Array.isArray(v) ? v : [v])].map(el => resolve(el));
438
447
  const valueTable = this.parentTable(sStack);
439
448
  const alias = `_all_${sStack.length}`;
440
- const ppath = this.ident(this.parentPathField.name);
441
- const rppath = this.resolveName([...sStack, field, this.parentPathField]);
449
+ const pPath = this.ident(this.parentPathField.name);
450
+ const rpPath = this.resolveName([...sStack, field, this.parentPathField]);
442
451
 
443
452
  items.push(`${arr.length} = (
444
453
  SELECT COUNT(DISTINCT ${alias}.${this.ident(field.name)})
445
454
  FROM ${valueTable} ${alias}
446
- WHERE ${alias}.${ppath} = ${rppath}
455
+ WHERE ${alias}.${pPath} = ${rpPath}
447
456
  AND ${alias}.${this.ident(field.name)} IN (${arr.join(',')})
448
457
  )`);
449
458
  break;
450
459
  }
451
460
  case '$regex': {
461
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
452
462
  const re = Util.toRegex(v as string);
453
463
  const src = re.source;
454
464
  const ins = re.flags && re.flags.includes('i');
@@ -482,6 +492,7 @@ export abstract class SQLDialect implements DialectState {
482
492
  break;
483
493
  }
484
494
  case '$lt': case '$gt': case '$gte': case '$lte': {
495
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
485
496
  const subItems = (Object.keys(top) as (keyof typeof SQL_OPS)[])
486
497
  .map(ssk => `${sPath} ${SQL_OPS[ssk]} ${resolve(top[ssk])}`);
487
498
  items.push(subItems.length > 1 ? `(${subItems.join(` ${SQL_OPS.$and} `)})` : subItems[0]);
@@ -529,6 +540,7 @@ export abstract class SQLDialect implements DialectState {
529
540
  getWhereSQL<T>(cls: Class<T>, where?: WhereClause<T>): string {
530
541
  return !where || !Object.keys(where).length ?
531
542
  '' :
543
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
532
544
  `WHERE ${this.getWhereGroupingSQL(cls, where as WhereClause<T>)}`;
533
545
  }
534
546
 
@@ -598,16 +610,19 @@ LEFT OUTER JOIN ${from} ON
598
610
  /**
599
611
  * Generate full query
600
612
  */
601
- getQuerySQL<T>(cls: Class<T>, query: Query<T>) {
613
+ getQuerySQL<T>(cls: Class<T>, query: Query<T>): string {
602
614
  const sortFields = !query.sort ?
603
615
  '' :
604
616
  SQLUtil.orderBy(cls, query.sort)
605
617
  .map(x => this.resolveName(x.stack))
606
618
  .join(', ');
619
+
620
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
621
+ const where = query.where as WhereClause<T>;
607
622
  return `
608
623
  ${this.getSelectSQL(cls, query.select)}
609
624
  ${this.getFromSQL(cls)}
610
- ${this.getWhereSQL(cls, query.where as WhereClause<T>)}
625
+ ${this.getWhereSQL(cls, where)}
611
626
  ${this.getGroupBySQL(cls, query)}${sortFields ? `, ${sortFields}` : ''}
612
627
  ${this.getOrderBySQL(cls, query.sort)}
613
628
  ${this.getLimitSQL(cls, query)}`;
@@ -624,6 +639,7 @@ ${this.getLimitSQL(cls, query)}`;
624
639
 
625
640
  const fields = SchemaRegistry.has(config.type) ?
626
641
  [...SQLUtil.getFieldsByLocation(stack).local] :
642
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
627
643
  (array ? [config as FieldConfig] : []);
628
644
 
629
645
  if (!parent) {
@@ -660,19 +676,19 @@ CREATE TABLE IF NOT EXISTS ${this.table(stack)} (
660
676
  /**
661
677
  * Generate drop SQL
662
678
  */
663
- getDropTableSQL(stack: VisitStack[]) {
679
+ getDropTableSQL(stack: VisitStack[]): string {
664
680
  return `DROP TABLE IF EXISTS ${this.table(stack)}; `;
665
681
  }
666
682
 
667
683
  /**
668
684
  * Generate truncate SQL
669
685
  */
670
- getTruncateTableSQL(stack: VisitStack[]) {
686
+ getTruncateTableSQL(stack: VisitStack[]): string {
671
687
  return `TRUNCATE ${this.table(stack)}; `;
672
688
  }
673
689
 
674
690
  /**
675
- * Get all table creat queries for a class
691
+ * Get all table create queries for a class
676
692
  */
677
693
  getCreateAllTablesSQL(cls: Class): string[] {
678
694
  const out: string[] = [];
@@ -697,11 +713,13 @@ CREATE TABLE IF NOT EXISTS ${this.table(stack)} (
697
713
  getCreateIndexSQL<T extends ModelType>(cls: Class<T>, idx: IndexConfig<T>): string {
698
714
  const table = this.namespace(SQLUtil.classToStack(cls));
699
715
  const fields: [string, boolean][] = idx.fields.map(x => {
716
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
700
717
  const key = Object.keys(x)[0] as keyof typeof x;
701
718
  const val = x[key];
702
719
  if (Util.isPlainObject(val)) {
703
720
  throw new Error('Unable to supported nested fields for indices');
704
721
  }
722
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
705
723
  return [key as string, typeof val === 'number' ? val === 1 : (!!val)];
706
724
  });
707
725
  const constraint = `idx_${table}_${fields.map(([f]) => f).join('_')}`;
@@ -751,7 +769,7 @@ CREATE TABLE IF NOT EXISTS ${this.table(stack)} (
751
769
  const isArray = !!config.array;
752
770
 
753
771
  if (isArray) {
754
- const newInstances = [] as InsertWrapper['records'];
772
+ const newInstances: typeof instances = [];
755
773
  for (const el of instances) {
756
774
  if (el.value === null || el.value === undefined) {
757
775
  continue;
@@ -776,6 +794,7 @@ CREATE TABLE IF NOT EXISTS ${this.table(stack)} (
776
794
  return;
777
795
  }
778
796
 
797
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
779
798
  const matrix = instances.map(inst => columns.map(c => this.resolveValue(c, (inst.value as Record<string, string>)[c.name])));
780
799
 
781
800
  columnNames.push(this.pathField.name);
@@ -812,7 +831,7 @@ ${matrix.map(row => `(${row.join(', ')})`).join(',\n')};`;
812
831
  */
813
832
  getAllInsertSQL<T extends ModelType>(cls: Class<T>, instance: T): string[] {
814
833
  const out: string[] = [];
815
- const add = (text?: string) => text && out.push(text);
834
+ const add = (text?: string): void => { text && out.push(text); };
816
835
  SQLUtil.visitSchemaInstance(cls, instance, {
817
836
  onRoot: ({ value, path }) => add(this.getInsertSQL(path, [{ stack: path, value }])),
818
837
  onSub: ({ value, path }) => add(this.getInsertSQL(path, [{ stack: path, value }])),
@@ -824,7 +843,7 @@ ${matrix.map(row => `(${row.join(', ')})`).join(',\n')};`;
824
843
  /**
825
844
  * Simple data base updates
826
845
  */
827
- getUpdateSQL(stack: VisitStack[], data: Record<string, unknown>, where?: WhereClause<unknown>) {
846
+ getUpdateSQL(stack: VisitStack[], data: Record<string, unknown>, where?: WhereClause<unknown>): string {
828
847
  const { type } = stack[stack.length - 1];
829
848
  const { localMap } = SQLUtil.getFieldsByLocation(stack);
830
849
  return `
@@ -866,18 +885,21 @@ ${orderBy};`;
866
885
  /**
867
886
  * Get COUNT(1) query
868
887
  */
869
- getQueryCountSQL<T>(cls: Class<T>, query: Query<T>) {
888
+ getQueryCountSQL<T>(cls: Class<T>, query: Query<T>): string {
889
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
890
+ const where = query.where as WhereClause<T>;
870
891
  return `
871
892
  SELECT COUNT(DISTINCT ${this.rootAlias}.id) as total
872
893
  ${this.getFromSQL(cls)}
873
- ${this.getWhereSQL(cls, query.where as WhereClause<T>)}`;
894
+ ${this.getWhereSQL(cls, where)}`;
874
895
  }
875
896
 
876
897
  async fetchDependents<T>(cls: Class<T>, items: T[], select?: SelectClause<T>): Promise<T[]> {
877
898
  const stack: Record<string, unknown>[] = [];
878
899
  const selectStack: (SelectClause<T> | undefined)[] = [];
879
900
 
880
- const buildSet = (children: unknown[], field?: FieldConfig) => SQLUtil.collectDependents(this, stack[stack.length - 1], children, field);
901
+ const buildSet = (children: unknown[], field?: FieldConfig): Record<string, unknown> =>
902
+ SQLUtil.collectDependents(this, stack[stack.length - 1], children, field);
881
903
 
882
904
  await SQLUtil.visitSchema(SchemaRegistry.get(cls), {
883
905
  onRoot: async (config) => {
@@ -895,6 +917,7 @@ ${this.getWhereSQL(cls, query.where as WhereClause<T>)}`;
895
917
 
896
918
  // See if a selection exists at all
897
919
  const sel: FieldConfig[] = subSelectTop ? fields
920
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
898
921
  .filter(f => subSelectTop[f.name as keyof SelectClause<T>] === 1)
899
922
  : [];
900
923
 
@@ -924,7 +947,7 @@ ${this.getWhereSQL(cls, query.where as WhereClause<T>)}`;
924
947
  }
925
948
  }
926
949
  },
927
- onSimple: async ({ config, path }) => {
950
+ onSimple: async ({ config, path }): Promise<void> => {
928
951
  const top = stack[stack.length - 1];
929
952
  const ids = Object.keys(top);
930
953
  if (ids.length) {
@@ -943,7 +966,7 @@ ${this.getWhereSQL(cls, query.where as WhereClause<T>)}`;
943
966
  /**
944
967
  * Delete all ids
945
968
  */
946
- async deleteByIds(stack: VisitStack[], ids: string[]) {
969
+ async deleteByIds(stack: VisitStack[], ids: string[]): Promise<number> {
947
970
  return this.deleteAndGetCount<ModelType>(stack[stack.length - 1].type, {
948
971
  where: {
949
972
  [stack.length === 1 ? this.idField.name : this.pathField.name]: {
@@ -956,10 +979,10 @@ ${this.getWhereSQL(cls, query.where as WhereClause<T>)}`;
956
979
  /**
957
980
  * Do bulk process
958
981
  */
959
- async bulkProcess(dels: DeleteWrapper[], inserts: InsertWrapper[], upserts: InsertWrapper[], updates: InsertWrapper[]): Promise<BulkResponse> {
982
+ async bulkProcess(deletes: DeleteWrapper[], inserts: InsertWrapper[], upserts: InsertWrapper[], updates: InsertWrapper[]): Promise<BulkResponse> {
960
983
  const out = {
961
984
  counts: {
962
- delete: dels.reduce((acc, el) => acc + el.ids.length, 0),
985
+ delete: deletes.reduce((acc, el) => acc + el.ids.length, 0),
963
986
  error: 0,
964
987
  insert: inserts.filter(x => x.stack.length === 1).reduce((acc, el) => acc + el.records.length, 0),
965
988
  update: updates.filter(x => x.stack.length === 1).reduce((acc, el) => acc + el.records.length, 0),
@@ -970,7 +993,7 @@ ${this.getWhereSQL(cls, query.where as WhereClause<T>)}`;
970
993
  };
971
994
 
972
995
  // Full removals
973
- await Promise.all(dels.map(el => this.deleteByIds(el.stack, el.ids)));
996
+ await Promise.all(deletes.map(el => this.deleteByIds(el.stack, el.ids)));
974
997
 
975
998
  // Adding deletes
976
999
  if (upserts.length || updates.length) {
@@ -978,9 +1001,11 @@ ${this.getWhereSQL(cls, query.where as WhereClause<T>)}`;
978
1001
 
979
1002
  await Promise.all([
980
1003
  ...upserts.filter(x => x.stack.length === 1).map(i =>
1004
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
981
1005
  this.deleteByIds(i.stack, i.records.map(v => (v.value as Record<string, string>)[idx]))
982
1006
  ),
983
1007
  ...updates.filter(x => x.stack.length === 1).map(i =>
1008
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
984
1009
  this.deleteByIds(i.stack, i.records.map(v => (v.value as Record<string, string>)[idx]))
985
1010
  ),
986
1011
  ]);
@@ -24,7 +24,7 @@ export class MySQLConnection extends Connection<mysql.PoolConnection> {
24
24
  this.#config = config;
25
25
  }
26
26
 
27
- async init() {
27
+ async init(): Promise<void> {
28
28
  this.#pool = mysql.createPool({
29
29
  user: this.#config.user,
30
30
  password: this.#config.password,
@@ -43,7 +43,7 @@ export class MySQLConnection extends Connection<mysql.PoolConnection> {
43
43
  /**
44
44
  * Support some basic type support for JSON data
45
45
  */
46
- typeCast(field: Parameters<Exclude<mysql.TypeCast, boolean>>[0], next: () => unknown) {
46
+ typeCast(field: Parameters<Exclude<mysql.TypeCast, boolean>>[0], next: () => unknown): unknown {
47
47
  const res = next();
48
48
  if (typeof res === 'string' && (field.type === 'JSON' || field.type === 'BLOB')) {
49
49
  if (res.charAt(0) === '{' && res.charAt(res.length - 1) === '}') {
@@ -67,19 +67,19 @@ export class MySQLConnection extends Connection<mysql.PoolConnection> {
67
67
  rej(err);
68
68
  }
69
69
  } else {
70
- const records = Array.isArray(results) ? [...results].map(v => ({ ...v })) : [{ ...results }] as T[];
70
+ const records: T[] = Array.isArray(results) ? [...results].map(v => ({ ...v })) : [{ ...results }];
71
71
  res({ records, count: results.affectedRows });
72
72
  }
73
73
  });
74
74
  });
75
75
  }
76
76
 
77
- acquire() {
77
+ acquire(): Promise<mysql.PoolConnection> {
78
78
  return new Promise<mysql.PoolConnection>((res, rej) =>
79
79
  this.#pool.getConnection((err, conn) => err ? rej(err) : res(conn)));
80
80
  }
81
81
 
82
- release(conn: mysql.PoolConnection) {
82
+ release(conn: mysql.PoolConnection): void {
83
83
  conn.release();
84
84
  }
85
85
  }
@@ -52,36 +52,37 @@ export class MySQLDialect extends SQLDialect {
52
52
  /**
53
53
  * Compute hash
54
54
  */
55
- hash(value: string) {
55
+ hash(value: string): string {
56
56
  return `SHA2('${value}', ${this.KEY_LEN * 4})`;
57
57
  }
58
58
 
59
59
  /**
60
60
  * Build identifier
61
61
  */
62
- ident(field: FieldConfig | string) {
62
+ ident(field: FieldConfig | string): string {
63
63
  return `\`${typeof field === 'string' ? field : field.name}\``;
64
64
  }
65
65
 
66
66
  /**
67
67
  * Create table, adding in specific engine options
68
68
  */
69
- override getCreateTableSQL(stack: VisitStack[]) {
69
+ override getCreateTableSQL(stack: VisitStack[]): string {
70
70
  return super.getCreateTableSQL(stack).replace(/;$/, ` ${this.tablePostfix};`);
71
71
  }
72
72
 
73
73
  /**
74
74
  * Define column modification
75
75
  */
76
- getModifyColumnSQL(stack: VisitStack[]) {
77
- const field = stack[stack.length - 1];
78
- return `ALTER TABLE ${this.parentTable(stack)} MODIFY COLUMN ${this.getColumnDefinition(field as FieldConfig)};`;
76
+ getModifyColumnSQL(stack: VisitStack[]): string {
77
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
78
+ const field = stack[stack.length - 1] as FieldConfig;
79
+ return `ALTER TABLE ${this.parentTable(stack)} MODIFY COLUMN ${this.getColumnDefinition(field)};`;
79
80
  }
80
81
 
81
82
  /**
82
83
  * Add root alias to delete clause
83
84
  */
84
- override getDeleteSQL(stack: VisitStack[], where?: WhereClause<unknown>) {
85
+ override getDeleteSQL(stack: VisitStack[], where?: WhereClause<unknown>): string {
85
86
  const sql = super.getDeleteSQL(stack, where);
86
87
  return sql.replace(/\bDELETE\b/g, `DELETE ${this.rootAlias}`);
87
88
  }
@@ -28,7 +28,7 @@ export class PostgreSQLConnection extends Connection<pg.PoolClient> {
28
28
  * Initializes connection and establishes crypto extension for use with hashing
29
29
  */
30
30
  @WithAsyncContext()
31
- async init() {
31
+ async init(): Promise<void> {
32
32
  this.#pool = new pg.Pool({
33
33
  user: this.#config.user,
34
34
  password: this.#config.password,
@@ -53,9 +53,10 @@ export class PostgreSQLConnection extends Connection<pg.PoolClient> {
53
53
  console.debug('Executing query', { query });
54
54
  try {
55
55
  const out = await conn.query(query);
56
- return { count: out.rowCount, records: [...out.rows].map(v => ({ ...v })) as T[] };
56
+ const records: T[] = [...out.rows].map(v => ({ ...v }));
57
+ return { count: out.rowCount, records };
57
58
  } catch (err) {
58
- if (err.message.includes('duplicate key value')) {
59
+ if (err instanceof Error && err.message.includes('duplicate key value')) {
59
60
  throw new ExistsError('query', query);
60
61
  } else {
61
62
  throw err;
@@ -63,11 +64,11 @@ export class PostgreSQLConnection extends Connection<pg.PoolClient> {
63
64
  }
64
65
  }
65
66
 
66
- acquire() {
67
+ acquire(): Promise<pg.PoolClient> {
67
68
  return this.#pool.connect();
68
69
  }
69
70
 
70
- release(conn: pg.PoolClient) {
71
+ release(conn: pg.PoolClient): void {
71
72
  conn.release();
72
73
  }
73
74
  }
@@ -41,20 +41,21 @@ export class PostgreSQLDialect extends SQLDialect {
41
41
  /**
42
42
  * How to hash
43
43
  */
44
- hash(value: string) {
44
+ hash(value: string): string {
45
45
  return `encode(digest('${value}', 'sha1'), 'hex')`;
46
46
  }
47
47
 
48
- ident(field: FieldConfig | string) {
48
+ ident(field: FieldConfig | string): string {
49
49
  return `"${typeof field === 'string' ? field : field.name}"`;
50
50
  }
51
51
 
52
52
  /**
53
53
  * Define column modification
54
54
  */
55
- getModifyColumnSQL(stack: VisitStack[]) {
56
- const field = stack[stack.length - 1];
57
- const type = this.getColumnType(field as FieldConfig);
55
+ getModifyColumnSQL(stack: VisitStack[]): string {
56
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
57
+ const field = stack[stack.length - 1] as FieldConfig;
58
+ const type = this.getColumnType(field);
58
59
  const ident = this.ident(field.name);
59
60
  return `ALTER TABLE ${this.parentTable(stack)} ALTER COLUMN ${ident} TYPE ${type} USING (${ident}::${type});`;
60
61
  }
@@ -29,12 +29,12 @@ export class SqliteConnection extends Connection<sqlite3.Database> {
29
29
  this.#config = config;
30
30
  }
31
31
 
32
- async #withRetries<T>(op: () => Promise<T>, retries = 10, delay = 250) {
32
+ async #withRetries<T>(op: () => Promise<T>, retries = 10, delay = 250): Promise<T> {
33
33
  for (; ;) {
34
34
  try {
35
35
  return await op();
36
36
  } catch (err) {
37
- if (retries > 1 && err.message.includes('database is locked')) {
37
+ if (err instanceof Error && retries > 1 && err.message.includes('database is locked')) {
38
38
  console.error('Failed, and waiting', retries);
39
39
  await Util.wait(delay);
40
40
  retries -= 1;
@@ -49,11 +49,11 @@ export class SqliteConnection extends Connection<sqlite3.Database> {
49
49
  * Initializes connection and establishes crypto extension for use with hashing
50
50
  */
51
51
  @WithAsyncContext()
52
- override async init() {
52
+ override async init(): Promise<void> {
53
53
  this.#pool = pool.createPool({
54
54
  create: () => this.#withRetries(async () => {
55
55
  const db = Db(AppCache.toEntryName('sqlite_db'),
56
- this.#config.options as sqlite3.Options
56
+ this.#config.options
57
57
  );
58
58
  await db.pragma('foreign_keys = ON');
59
59
  await db.pragma('journal_mode = WAL');
@@ -73,21 +73,26 @@ export class SqliteConnection extends Connection<sqlite3.Database> {
73
73
  try {
74
74
  const out = await conn.prepare(query)[query.trim().startsWith('SELECT') ? 'all' : 'run']();
75
75
  if (Array.isArray(out)) {
76
- return { count: out.length, records: [...out].map(v => ({ ...v })) as T[] };
76
+ const records: T[] = [...out].map(v => ({ ...v }));
77
+ return { count: out.length, records };
77
78
  } else {
78
79
  return { count: out.changes, records: [] };
79
80
  }
80
81
  } catch (err) {
81
- throw err.message.includes('UNIQUE constraint failed') ? new ExistsError('query', query) : err;
82
+ if (err instanceof Error && err.message.includes('UNIQUE constraint failed')) {
83
+ throw new ExistsError('query', query);
84
+ } else {
85
+ throw err;
86
+ }
82
87
  }
83
88
  });
84
89
  }
85
90
 
86
- async acquire() {
91
+ async acquire(): Promise<Db.Database> {
87
92
  return this.#pool.acquire();
88
93
  }
89
94
 
90
- async release(db: sqlite3.Database) {
95
+ async release(db: sqlite3.Database): Promise<void> {
91
96
  return this.#pool.release(db);
92
97
  }
93
98
  }
@@ -34,30 +34,31 @@ export class SqliteDialect extends SQLDialect {
34
34
  });
35
35
  }
36
36
 
37
- override resolveDateValue(value: Date) {
37
+ override resolveDateValue(value: Date): string {
38
38
  return `${value.getTime()}`;
39
39
  }
40
40
 
41
41
  /**
42
42
  * How to hash
43
43
  */
44
- hash(value: string) {
44
+ hash(value: string): string {
45
45
  return `hex('${value}')`;
46
46
  }
47
47
 
48
48
  /**
49
49
  * Build identifier
50
50
  */
51
- ident(field: FieldConfig | string) {
51
+ ident(field: FieldConfig | string): string {
52
52
  return `\`${typeof field === 'string' ? field : field.name}\``;
53
53
  }
54
54
 
55
55
  /**
56
56
  * Define column modification
57
57
  */
58
- getModifyColumnSQL(stack: VisitStack[]) {
59
- const field = stack[stack.length - 1];
60
- const type = this.getColumnType(field as FieldConfig);
58
+ getModifyColumnSQL(stack: VisitStack[]): string {
59
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
60
+ const field = stack[stack.length - 1] as FieldConfig;
61
+ const type = this.getColumnType(field);
61
62
  const ident = this.ident(field.name);
62
63
  return `ALTER TABLE ${this.parentTable(stack)} ALTER COLUMN ${ident} TYPE ${type} USING (${ident}::${type});`;
63
64
  }
@@ -65,7 +66,7 @@ export class SqliteDialect extends SQLDialect {
65
66
  /**
66
67
  * Generate truncate SQL
67
68
  */
68
- override getTruncateTableSQL(stack: VisitStack[]) {
69
+ override getTruncateTableSQL(stack: VisitStack[]): string {
69
70
  return `DELETE FROM ${this.table(stack)};`;
70
71
  }
71
72
 
@@ -16,17 +16,19 @@ export type VisitStack = {
16
16
  index?: number;
17
17
  };
18
18
 
19
+ type FieldCacheEntry = {
20
+ local: FieldConfig[];
21
+ localMap: Record<string, FieldConfig>;
22
+ foreign: FieldConfig[];
23
+ foreignMap: Record<string, FieldConfig>;
24
+ };
25
+
19
26
  /**
20
27
  * Utilities for dealing with SQL operations
21
28
  */
22
29
  export class SQLUtil {
23
30
 
24
- static SCHEMA_FIELDS_CACHE = new Map<Class, {
25
- local: FieldConfig[];
26
- localMap: Record<string, FieldConfig>;
27
- foreign: FieldConfig[];
28
- foreignMap: Record<string, FieldConfig>;
29
- }>();
31
+ static SCHEMA_FIELDS_CACHE = new Map<Class, FieldCacheEntry>();
30
32
 
31
33
  /**
32
34
  * Creates a new visitation stack with the class as the root
@@ -44,6 +46,7 @@ export class SQLUtil {
44
46
  if (Array.isArray(o)) {
45
47
  return o.filter(x => x !== null && x !== undefined).map(x => this.cleanResults(dct, x));
46
48
  } else if (!Util.isSimple(o)) {
49
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
47
50
  for (const k of Object.keys(o) as (keyof T)[]) {
48
51
  if (o[k] === null || o[k] === undefined || k === dct.parentPathField.name || k === dct.pathField.name || k === dct.idxField.name) {
49
52
  delete o[k];
@@ -51,8 +54,10 @@ export class SQLUtil {
51
54
  o[k] = this.cleanResults(dct, o[k]);
52
55
  }
53
56
  }
57
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
54
58
  return { ...o } as unknown as U[];
55
59
  } else {
60
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
56
61
  return o as unknown as U;
57
62
  }
58
63
  }
@@ -60,7 +65,7 @@ export class SQLUtil {
60
65
  /**
61
66
  * Get all available fields at current stack path
62
67
  */
63
- static getFieldsByLocation(stack: VisitStack[]) {
68
+ static getFieldsByLocation(stack: VisitStack[]): FieldCacheEntry {
64
69
  const top = stack[stack.length - 1];
65
70
  const cls = SchemaRegistry.get(top.type);
66
71
 
@@ -69,6 +74,7 @@ export class SQLUtil {
69
74
  }
70
75
 
71
76
  if (!cls) { // If a simple type, it is it's own field
77
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
72
78
  const field = { ...top } as FieldConfig;
73
79
  return {
74
80
  local: [field], localMap: { [field.name]: field },
@@ -94,9 +100,9 @@ export class SQLUtil {
94
100
  }
95
101
  }
96
102
 
97
- const ret = {
98
- localMap: {} as Record<string, FieldConfig>,
99
- foreignMap: {} as Record<string, FieldConfig>,
103
+ const ret: FieldCacheEntry = {
104
+ localMap: {},
105
+ foreignMap: {},
100
106
  local: fields.filter(x => !SchemaRegistry.has(x.type) && !x.array),
101
107
  foreign: fields.filter(x => SchemaRegistry.has(x.type) || x.array)
102
108
  };
@@ -112,11 +118,11 @@ export class SQLUtil {
112
118
  /**
113
119
  * Process a schema structure, synchronously
114
120
  */
115
- static visitSchemaSync(config: ClassConfig | FieldConfig, handler: VisitHandler<void>, state: VisitState = { path: [] }) {
121
+ static visitSchemaSync(config: ClassConfig | FieldConfig, handler: VisitHandler<void>, state: VisitState = { path: [] }): void {
116
122
  const path = 'class' in config ? this.classToStack(config.class) : [...state.path, config];
117
123
  const { local: fields, foreign } = this.getFieldsByLocation(path);
118
124
 
119
- const descend = () => {
125
+ const descend = (): void => {
120
126
  for (const field of foreign) {
121
127
  if (SchemaRegistry.has(field.type)) {
122
128
  this.visitSchemaSync(field, handler, { path });
@@ -140,11 +146,11 @@ export class SQLUtil {
140
146
  /**
141
147
  * Visit a Schema structure
142
148
  */
143
- static async visitSchema(config: ClassConfig | FieldConfig, handler: VisitHandler<Promise<void>>, state: VisitState = { path: [] }) {
149
+ static async visitSchema(config: ClassConfig | FieldConfig, handler: VisitHandler<Promise<void>>, state: VisitState = { path: [] }): Promise<void> {
144
150
  const path = 'class' in config ? this.classToStack(config.class) : [...state.path, config];
145
151
  const { local: fields, foreign } = this.getFieldsByLocation(path);
146
152
 
147
- const descend = async () => {
153
+ const descend = async (): Promise<void> => {
148
154
  for (const field of foreign) {
149
155
  if (SchemaRegistry.has(field.type)) {
150
156
  await this.visitSchema(field, handler, { path });
@@ -168,7 +174,7 @@ export class SQLUtil {
168
174
  /**
169
175
  * Process a schema instance by visiting it synchronously. This is synchronous to prevent concurrent calls from breaking
170
176
  */
171
- static visitSchemaInstance<T extends ModelType>(cls: Class<T>, instance: T, handler: VisitHandler<unknown, VisitInstanceNode<unknown>>) {
177
+ static visitSchemaInstance<T extends ModelType>(cls: Class<T>, instance: T, handler: VisitHandler<unknown, VisitInstanceNode<unknown>>): void {
172
178
  const pathObj: unknown[] = [instance];
173
179
  this.visitSchemaSync(SchemaRegistry.get(cls), {
174
180
  onRoot: (config) => {
@@ -179,15 +185,16 @@ export class SQLUtil {
179
185
  },
180
186
  onSub: (config) => {
181
187
  const { config: field } = config;
188
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
182
189
  const topObj = pathObj[pathObj.length - 1] as Record<string, unknown>;
183
190
  const top = config.path[config.path.length - 1];
184
191
 
185
192
  if (field.name in topObj) {
186
193
  const value = topObj[field.name];
187
- const vals = Array.isArray(value) ? value : [value];
194
+ const values = Array.isArray(value) ? value : [value];
188
195
 
189
196
  let i = 0;
190
- for (const val of vals) {
197
+ for (const val of values) {
191
198
  try {
192
199
  pathObj.push(val);
193
200
  config.path[config.path.length - 1] = { ...top, index: i++ };
@@ -202,6 +209,7 @@ export class SQLUtil {
202
209
  },
203
210
  onSimple: (config) => {
204
211
  const { config: field } = config;
212
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
205
213
  const topObj = pathObj[pathObj.length - 1] as Record<string, unknown>;
206
214
  const value = topObj[field.name];
207
215
  return handler.onSimple({ ...config, value });
@@ -214,7 +222,7 @@ export class SQLUtil {
214
222
  */
215
223
  static select<T>(cls: Class<T>, select?: SelectClause<T>): FieldConfig[] {
216
224
  if (!select || Object.keys(select).length === 0) {
217
- return [{ type: cls, name: '*' } as FieldConfig];
225
+ return [{ type: cls, name: '*', owner: cls, array: false }];
218
226
  }
219
227
 
220
228
  const { localMap } = this.getFieldsByLocation(this.classToStack(cls));
@@ -222,7 +230,9 @@ export class SQLUtil {
222
230
  let toGet = new Set<string>();
223
231
 
224
232
  for (const [k, v] of Object.entries(select)) {
225
- if (!Util.isPlainObject(select[k as keyof typeof select]) && localMap[k]) {
233
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
234
+ const typedKey = k as keyof typeof select;
235
+ if (!Util.isPlainObject(select[typedKey]) && localMap[k]) {
226
236
  if (!v) {
227
237
  if (toGet.size === 0) {
228
238
  toGet = new Set(SchemaRegistry.get(cls).views[AllViewⲐ].fields);
@@ -254,6 +264,7 @@ export class SQLUtil {
254
264
  } else {
255
265
  stack.push(field);
256
266
  schema = SchemaRegistry.get(field.type);
267
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
257
268
  cl = val as Record<string, unknown>;
258
269
  }
259
270
  }
@@ -264,18 +275,24 @@ export class SQLUtil {
264
275
  /**
265
276
  * Find all dependent fields via child tables
266
277
  */
267
- static collectDependents<T>(dct: DialectState, parent: unknown, v: T[], field?: FieldConfig) {
278
+ static collectDependents<T>(dct: DialectState, parent: unknown, v: T[], field?: FieldConfig): Record<string, T> {
268
279
  if (field) {
269
280
  const isSimple = SchemaRegistry.has(field.type);
270
281
  for (const el of v) {
271
- const root = (parent as { [k: string]: unknown })[el[dct.parentPathField.name as keyof T] as unknown as string] as unknown as Record<string, unknown>;
282
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
283
+ const parentKey = el[dct.parentPathField.name as keyof T] as unknown as string;
284
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
285
+ const root = (parent as Record<string, Record<string, unknown>>)[parentKey];
272
286
  if (field.array) {
273
287
  if (!root[field.name]) {
288
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
274
289
  root[field.name] = [isSimple ? el : el[field.name as keyof T]];
275
290
  } else {
291
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
276
292
  (root[field.name] as unknown[]).push(isSimple ? el : el[field.name as keyof T]);
277
293
  }
278
294
  } else {
295
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
279
296
  root[field.name] = isSimple ? el : el[field.name as keyof T];
280
297
  }
281
298
  }
@@ -283,6 +300,7 @@ export class SQLUtil {
283
300
 
284
301
  const mapping: Record<string, T> = {};
285
302
  for (const el of v) {
303
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
286
304
  const key = el[dct.pathField.name as keyof T];
287
305
  if (typeof key === 'string') {
288
306
  mapping[key] = el;
@@ -294,7 +312,7 @@ export class SQLUtil {
294
312
  /**
295
313
  * Build table name via stack path
296
314
  */
297
- static buildTable(list: VisitStack[]) {
315
+ static buildTable(list: VisitStack[]): string {
298
316
  const top = list[list.length - 1];
299
317
  if (!top[TableⲐ]) {
300
318
  top[TableⲐ] = list.map((el, i) => i === 0 ? ModelRegistry.getStore(el.type) : el.name).join('_');
@@ -305,7 +323,7 @@ export class SQLUtil {
305
323
  /**
306
324
  * Build property path for a table/field given the current stack
307
325
  */
308
- static buildPath(list: VisitStack[]) {
326
+ static buildPath(list: VisitStack[]): string {
309
327
  return list.map((el, i) => `${el.name}${el.index ? `[${el.index}]` : ''}`).join('.');
310
328
  }
311
329
 
@@ -313,9 +331,9 @@ export class SQLUtil {
313
331
  * Get insert statements for a given class, and its child tables
314
332
  */
315
333
  static async getInserts<T extends ModelType>(cls: Class<T>, els: T[]): Promise<InsertWrapper[]> {
316
- const ins = {} as Record<string, InsertWrapper>;
334
+ const ins: Record<string, InsertWrapper> = {};
317
335
 
318
- const track = (stack: VisitStack[], value: unknown) => {
336
+ const track = (stack: VisitStack[], value: unknown): void => {
319
337
  const key = this.buildTable(stack);
320
338
  (ins[key] = ins[key] ?? { stack, records: [] }).records.push({ stack, value });
321
339
  };
package/src/service.ts CHANGED
@@ -26,6 +26,8 @@ import { Connected, ConnectedIterator, Transactional } from './connection/decora
26
26
  import { SQLUtil } from './internal/util';
27
27
  import { SQLDialect } from './dialect/base';
28
28
  import { TableManager } from './table-manager';
29
+ import { Connection } from './connection/base';
30
+ import { InsertWrapper } from './internal/types';
29
31
 
30
32
  /**
31
33
  * Core for SQL Model Source. Should not have any direct queries,
@@ -45,7 +47,7 @@ export class SQLModelService implements
45
47
 
46
48
  readonly config: SQLModelConfig;
47
49
 
48
- get client() {
50
+ get client(): SQLDialect {
49
51
  return this.#dialect;
50
52
  }
51
53
 
@@ -66,7 +68,7 @@ export class SQLModelService implements
66
68
  cls: Class<T>,
67
69
  addedIds: Map<number, string>,
68
70
  toCheck: Map<string, number>
69
- ) {
71
+ ): Promise<Map<number, string>> {
70
72
  // Get all upsert ids
71
73
  const all = toCheck.size ?
72
74
  (await this.#exec<ModelType>(
@@ -86,20 +88,21 @@ export class SQLModelService implements
86
88
  return addedIds;
87
89
  }
88
90
 
89
- #exec<T = unknown>(sql: string) {
91
+ #exec<T = unknown>(sql: string): Promise<{ records: T[], count: number }> {
90
92
  return this.#dialect.executeSQL<T>(sql);
91
93
  }
92
94
 
93
- async #deleteRaw<T extends ModelType>(cls: Class<T>, id: string, checkExpiry = true) {
95
+ async #deleteRaw<T extends ModelType>(cls: Class<T>, id: string, checkExpiry = true): Promise<void> {
96
+ const where: WhereClauseRaw<ModelType> = { id };
94
97
  const count = await this.#dialect.deleteAndGetCount<ModelType>(cls, {
95
- where: ModelQueryUtil.getWhereClause(cls, { id } as WhereClauseRaw<T>, checkExpiry)
98
+ where: ModelQueryUtil.getWhereClause(cls, where, checkExpiry)
96
99
  });
97
100
  if (count === 0) {
98
101
  throw new NotFoundError(cls, id);
99
102
  }
100
103
  }
101
104
 
102
- async postConstruct() {
105
+ async postConstruct(): Promise<void> {
103
106
  if (this.#dialect) {
104
107
  if (this.#dialect.conn.init) {
105
108
  await this.#dialect.conn.init();
@@ -110,32 +113,32 @@ export class SQLModelService implements
110
113
  }
111
114
  }
112
115
 
113
- get conn() {
116
+ get conn(): Connection {
114
117
  return this.#dialect.conn;
115
118
  }
116
119
 
117
- uuid() {
120
+ uuid(): string {
118
121
  return this.#dialect.generateId();
119
122
  }
120
123
 
121
- async changeSchema(cls: Class, change: SchemaChange) {
124
+ async changeSchema(cls: Class, change: SchemaChange): Promise<void> {
122
125
  await this.#manager.changeSchema(cls, change);
123
126
  }
124
127
 
125
- async createModel(cls: Class) {
128
+ async createModel(cls: Class): Promise<void> {
126
129
  await this.#manager.createTables(cls);
127
130
  }
128
131
 
129
- async deleteModel(cls: Class) {
132
+ async deleteModel(cls: Class): Promise<void> {
130
133
  await this.#manager.dropTables(cls);
131
134
  }
132
135
 
133
- async truncateModel(cls: Class) {
136
+ async truncateModel(cls: Class): Promise<void> {
134
137
  await this.#manager.truncateTables(cls);
135
138
  }
136
139
 
137
- async createStorage() { }
138
- async deleteStorage() { }
140
+ async createStorage(): Promise<void> { }
141
+ async deleteStorage(): Promise<void> { }
139
142
 
140
143
  @Transactional()
141
144
  async create<T extends ModelType>(cls: Class<T>, item: OptionalId<T>): Promise<T> {
@@ -183,7 +186,8 @@ export class SQLModelService implements
183
186
 
184
187
  @Connected()
185
188
  async get<T extends ModelType>(cls: Class<T>, id: string): Promise<T> {
186
- const res = await this.query(cls, { where: { id } as WhereClauseRaw<T> });
189
+ const where: WhereClauseRaw<ModelType> = { id };
190
+ const res = await this.query(cls, { where });
187
191
  if (res.length === 1) {
188
192
  return await ModelCrudUtil.load(cls, res[0]);
189
193
  }
@@ -191,14 +195,14 @@ export class SQLModelService implements
191
195
  }
192
196
 
193
197
  @ConnectedIterator()
194
- async * list<T extends ModelType>(cls: Class<T>) {
198
+ async * list<T extends ModelType>(cls: Class<T>): AsyncIterable<T> {
195
199
  for (const item of await this.query(cls, {})) {
196
200
  yield await ModelCrudUtil.load(cls, item);
197
201
  }
198
202
  }
199
203
 
200
204
  @Transactional()
201
- async delete<T extends ModelType>(cls: Class<T>, id: string) {
205
+ async delete<T extends ModelType>(cls: Class<T>, id: string): Promise<void> {
202
206
  await this.#deleteRaw(cls, id, false);
203
207
  }
204
208
 
@@ -214,8 +218,10 @@ export class SQLModelService implements
214
218
  new Map([...existingUpsertedIds.entries()].map(([k, v]) => [v, k]))
215
219
  );
216
220
 
217
- const get = (k: keyof BulkOp<T>) => operations.map(x => x[k]).filter(x => !!x) as T[];
218
- const getStatements = async (k: keyof BulkOp<T>) => (await SQLUtil.getInserts(cls, get(k))).filter(x => !!x.records.length);
221
+ const get = (k: keyof BulkOp<T>): T[] =>
222
+ operations.map(x => x[k]).filter((x): x is T => !!x);
223
+ const getStatements = async (k: keyof BulkOp<T>): Promise<InsertWrapper[]> =>
224
+ (await SQLUtil.getInserts(cls, get(k))).filter(x => !!x.records.length);
219
225
 
220
226
  const deletes = [{ stack: SQLUtil.classToStack(cls), ids: get('delete').map(x => x.id) }].filter(x => !!x.ids.length);
221
227
 
@@ -232,7 +238,7 @@ export class SQLModelService implements
232
238
 
233
239
  // Expiry
234
240
  @Transactional()
235
- async deleteExpired<T extends ModelType>(cls: Class<T>) {
241
+ async deleteExpired<T extends ModelType>(cls: Class<T>): Promise<number> {
236
242
  return ModelQueryExpiryUtil.deleteExpired(this, cls);
237
243
  }
238
244
 
@@ -284,12 +290,15 @@ export class SQLModelService implements
284
290
  async suggestValues<T extends ModelType>(cls: Class<T>, field: ValidStringFields<T>, prefix?: string, query?: PageableModelQuery<T>): Promise<string[]> {
285
291
  const q = ModelQuerySuggestUtil.getSuggestFieldQuery(cls, field, prefix, query);
286
292
  const results = await this.query(cls, q);
287
- return ModelQuerySuggestUtil.combineSuggestResults(cls, field as 'type', prefix, results, x => x, query?.limit);
293
+
294
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
295
+ const modelTypeField = field as ValidStringFields<ModelType>;
296
+ return ModelQuerySuggestUtil.combineSuggestResults(cls, modelTypeField, prefix, results, x => x, query?.limit);
288
297
  }
289
298
 
290
299
  @Connected()
291
300
  async facet<T extends ModelType>(cls: Class<T>, field: ValidStringFields<T>, query?: ModelQuery<T>): Promise<{ key: string, count: number }[]> {
292
- const col = this.#dialect.ident(field as string);
301
+ const col = this.#dialect.ident(field);
293
302
  const ttl = this.#dialect.ident('count');
294
303
  const key = this.#dialect.ident('key');
295
304
  const q = [
@@ -7,6 +7,7 @@ import { SchemaChange } from '@travetto/schema';
7
7
  import { Connected, Transactional } from './connection/decorator';
8
8
  import { SQLDialect } from './dialect/base';
9
9
  import { SQLUtil, VisitStack } from './internal/util';
10
+ import { Connection } from './connection/base';
10
11
 
11
12
  /**
12
13
  * Manage creation/updating of all tables
@@ -22,7 +23,7 @@ export class TableManager {
22
23
  this.#dialect = dialect;
23
24
  }
24
25
 
25
- #exec<T = unknown>(sql: string) {
26
+ #exec<T = unknown>(sql: string): Promise<{ records: T[], count: number }> {
26
27
  return this.#dialect.executeSQL<T>(sql);
27
28
  }
28
29
 
@@ -41,9 +42,12 @@ export class TableManager {
41
42
  for (const op of this.#dialect.getCreateAllIndicesSQL(cls, indices)) {
42
43
  try {
43
44
  await this.#exec(op);
44
- } catch (e) {
45
- if (!/\bexists|duplicate\b/i.test(e.message)) {
46
- throw e;
45
+ } catch (err) {
46
+ if (!(err instanceof Error)) {
47
+ throw err;
48
+ }
49
+ if (!/\bexists|duplicate\b/i.test(err.message)) {
50
+ throw err;
47
51
  }
48
52
  }
49
53
  }
@@ -78,7 +82,7 @@ export class TableManager {
78
82
  /**
79
83
  * Get a valid connection
80
84
  */
81
- get conn() {
85
+ get conn(): Connection {
82
86
  return this.#dialect.conn;
83
87
  }
84
88
 
@@ -88,24 +92,24 @@ export class TableManager {
88
92
  @WithAsyncContext({})
89
93
  @Transactional()
90
94
  @Connected()
91
- async changeSchema(cls: Class, change: SchemaChange) {
95
+ async changeSchema(cls: Class, change: SchemaChange): Promise<void> {
92
96
  try {
93
97
  const rootStack = SQLUtil.classToStack(cls);
94
98
 
95
- const changes = change.subs.reduce((acc, v) => {
99
+ const changes = change.subs.reduce<Record<ChangeEvent<unknown>['type'], VisitStack[][]>>((acc, v) => {
96
100
  const path = v.path.map(f => ({ ...f }));
97
101
  for (const ev of v.fields) {
98
102
  acc[ev.type].push([...rootStack, ...path, { ...(ev.type === 'removing' ? ev.prev : ev.curr)! }]);
99
103
  }
100
104
  return acc;
101
- }, { added: [], changed: [], removing: [] } as Record<ChangeEvent<unknown>['type'], VisitStack[][]>);
105
+ }, { added: [], changed: [], removing: [] });
102
106
 
103
107
  await Promise.all(changes.added.map(v => this.#dialect.executeSQL(this.#dialect.getAddColumnSQL(v))));
104
108
  await Promise.all(changes.changed.map(v => this.#dialect.executeSQL(this.#dialect.getModifyColumnSQL(v))));
105
109
  await Promise.all(changes.removing.map(v => this.#dialect.executeSQL(this.#dialect.getDropColumnSQL(v))));
106
- } catch (e) {
110
+ } catch (err) {
107
111
  // Failed to change
108
- console.error('Unable to change field', { error: e });
112
+ console.error('Unable to change field', { error: err });
109
113
  }
110
114
  }
111
115
  }