@travetto/model-sql 8.0.0-alpha.26 → 8.0.0-alpha.27
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/package.json +8 -8
- package/src/dialect.ts +67 -75
- package/src/service.ts +11 -3
- package/src/types.ts +8 -0
- package/support/test/query.ts +9 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/model-sql",
|
|
3
|
-
"version": "8.0.0-alpha.
|
|
3
|
+
"version": "8.0.0-alpha.27",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "SQL backing for the travetto model module, with real-time modeling support for SQL schemas.",
|
|
6
6
|
"keywords": [
|
|
@@ -28,15 +28,15 @@
|
|
|
28
28
|
"directory": "module/model-sql"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@travetto/config": "^8.0.0-alpha.
|
|
32
|
-
"@travetto/context": "^8.0.0-alpha.
|
|
33
|
-
"@travetto/model": "^8.0.0-alpha.
|
|
34
|
-
"@travetto/model-indexed": "^8.0.0-alpha.
|
|
35
|
-
"@travetto/model-query": "^8.0.0-alpha.
|
|
31
|
+
"@travetto/config": "^8.0.0-alpha.23",
|
|
32
|
+
"@travetto/context": "^8.0.0-alpha.21",
|
|
33
|
+
"@travetto/model": "^8.0.0-alpha.24",
|
|
34
|
+
"@travetto/model-indexed": "^8.0.0-alpha.26",
|
|
35
|
+
"@travetto/model-query": "^8.0.0-alpha.25"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
|
-
"@travetto/cli": "^8.0.0-alpha.
|
|
39
|
-
"@travetto/test": "^8.0.0-alpha.
|
|
38
|
+
"@travetto/cli": "^8.0.0-alpha.29",
|
|
39
|
+
"@travetto/test": "^8.0.0-alpha.22"
|
|
40
40
|
},
|
|
41
41
|
"peerDependenciesMeta": {
|
|
42
42
|
"@travetto/cli": {
|
package/src/dialect.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { isModelQueryIndex, ModelQueryUtil, type SortClause, type WhereClause }
|
|
|
5
5
|
import { type Class, castTo, JSONUtil, RuntimeError } from '@travetto/runtime';
|
|
6
6
|
import { DataUtil, type SchemaFieldConfig, SchemaRegistryIndex } from '@travetto/schema';
|
|
7
7
|
|
|
8
|
-
import type { JSONSqlPathMode, SchemaContext, TableContext } from './types.ts';
|
|
8
|
+
import type { JSONSqlPathMode, ResolvedPathContext, SchemaContext, TableContext } from './types.ts';
|
|
9
9
|
|
|
10
10
|
export interface TransactionStatements {
|
|
11
11
|
begin: string;
|
|
@@ -64,28 +64,10 @@ export abstract class AbstractANSI99Dialect {
|
|
|
64
64
|
|
|
65
65
|
abstract getColumnType(fieldConfiguration: SchemaFieldConfig): string;
|
|
66
66
|
abstract compileJsonIndexPath(columnName: string, jsonPath: string[], mode: JSONSqlPathMode): string;
|
|
67
|
-
abstract compileArrayAll(
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
field: SchemaFieldConfig,
|
|
72
|
-
topLevel?: boolean
|
|
73
|
-
): { sql: string; formatted: unknown };
|
|
74
|
-
abstract compileArrayEquals(
|
|
75
|
-
sqlPath: string,
|
|
76
|
-
identifier: string,
|
|
77
|
-
values: unknown,
|
|
78
|
-
field: SchemaFieldConfig,
|
|
79
|
-
topLevel?: boolean
|
|
80
|
-
): { sql: string; formatted: unknown };
|
|
81
|
-
abstract compileArrayAny(
|
|
82
|
-
sqlPath: string,
|
|
83
|
-
identifier: string,
|
|
84
|
-
values: unknown[],
|
|
85
|
-
field: SchemaFieldConfig,
|
|
86
|
-
topLevel?: boolean
|
|
87
|
-
): { sql: string; formatted: unknown };
|
|
88
|
-
abstract compileArrayExists(sqlPath: string, identifier: string, field: SchemaFieldConfig, topLevel?: boolean): { sql: string };
|
|
67
|
+
abstract compileArrayAll(context: ResolvedPathContext, identifier: string, value: unknown[]): { sql: string; formatted: unknown };
|
|
68
|
+
abstract compileArrayEquals(context: ResolvedPathContext, identifier: string, values: unknown): { sql: string; formatted: unknown };
|
|
69
|
+
abstract compileArrayAny(context: ResolvedPathContext, identifier: string, values: unknown[]): { sql: string; formatted: unknown };
|
|
70
|
+
abstract compileArrayExists(context: ResolvedPathContext, identifier?: string): { sql: string };
|
|
89
71
|
|
|
90
72
|
abstract getRegexOperator(caseInsensitive: boolean): string;
|
|
91
73
|
abstract formatRegex(source: string, caseInsensitive: boolean): string;
|
|
@@ -94,12 +76,17 @@ export abstract class AbstractANSI99Dialect {
|
|
|
94
76
|
compileJsonEquality?(sqlPath: string, identifier: string): string;
|
|
95
77
|
shiftPlaceholders?(whereSQL: string, offset: number): string;
|
|
96
78
|
|
|
97
|
-
|
|
79
|
+
buildSqlPath<T extends ModelType>(tableContext: TableContext<T>, path: string[], mode: JSONSqlPathMode): string {
|
|
98
80
|
const firstSegment = path[0];
|
|
99
81
|
const escapedFirst = this.escapeIdentifier(firstSegment);
|
|
100
|
-
if (
|
|
82
|
+
if (tableContext.simpleFields.has(firstSegment)) {
|
|
101
83
|
if (path.length > 1) {
|
|
102
|
-
throw new RuntimeError(
|
|
84
|
+
throw new RuntimeError(
|
|
85
|
+
`Cannot traverse nested properties under simple column "${firstSegment}" in table "${tableContext.tableName}"`,
|
|
86
|
+
{
|
|
87
|
+
category: 'data'
|
|
88
|
+
}
|
|
89
|
+
);
|
|
103
90
|
}
|
|
104
91
|
return escapedFirst;
|
|
105
92
|
} else {
|
|
@@ -111,6 +98,10 @@ export abstract class AbstractANSI99Dialect {
|
|
|
111
98
|
}
|
|
112
99
|
}
|
|
113
100
|
|
|
101
|
+
compileIndexPath(context: TableContext, path: string[], mode: JSONSqlPathMode): string {
|
|
102
|
+
return this.buildSqlPath(context, path, mode);
|
|
103
|
+
}
|
|
104
|
+
|
|
114
105
|
getCreateIndexSQL(context: TableContext, indexConfig: IndexConfig): string {
|
|
115
106
|
const { tableName, cls: modelClass } = context;
|
|
116
107
|
const indexName = ['idx', tableName, indexConfig.name.toLowerCase().replaceAll('-', '_')].join('_');
|
|
@@ -272,11 +263,14 @@ CREATE TABLE ${this.escapeIdentifier(context.tableName)} (
|
|
|
272
263
|
return sortClauses.length ? `ORDER BY ${sortClauses.join(', ')}` : '';
|
|
273
264
|
}
|
|
274
265
|
|
|
275
|
-
|
|
266
|
+
#resolveSchemaPath<T extends ModelType>(
|
|
276
267
|
tableContext: TableContext<T>,
|
|
277
|
-
path: string[]
|
|
278
|
-
|
|
279
|
-
|
|
268
|
+
path: string[]
|
|
269
|
+
): {
|
|
270
|
+
leafField?: SchemaFieldConfig;
|
|
271
|
+
arrayField?: SchemaFieldConfig;
|
|
272
|
+
arraySegmentIndex?: number;
|
|
273
|
+
} {
|
|
280
274
|
const firstSegment = path[0];
|
|
281
275
|
|
|
282
276
|
if (tableContext.simpleFields.has(firstSegment)) {
|
|
@@ -288,34 +282,55 @@ CREATE TABLE ${this.escapeIdentifier(context.tableName)} (
|
|
|
288
282
|
}
|
|
289
283
|
);
|
|
290
284
|
}
|
|
291
|
-
|
|
292
|
-
return { sqlPath: this.escapeIdentifier(firstSegment), leafField };
|
|
285
|
+
return { leafField: tableContext.simpleFields.get(firstSegment) };
|
|
293
286
|
}
|
|
294
287
|
|
|
295
288
|
let currentField: SchemaFieldConfig | undefined = tableContext.complexFields.get(firstSegment);
|
|
289
|
+
let arrayField: SchemaFieldConfig | undefined = currentField?.array ? currentField : undefined;
|
|
290
|
+
let arraySegmentIndex: number | undefined = currentField?.array ? 0 : undefined;
|
|
296
291
|
let currentClass = currentField?.type;
|
|
297
|
-
const jsonPath = path.slice(1);
|
|
298
292
|
|
|
299
|
-
for (let
|
|
300
|
-
const segment =
|
|
293
|
+
for (let pathIndex = 1; pathIndex < path.length; pathIndex += 1) {
|
|
294
|
+
const segment = path[pathIndex];
|
|
301
295
|
const subclassConfiguration = SchemaRegistryIndex.getOptional(currentClass!)?.get();
|
|
302
296
|
currentField = subclassConfiguration?.fields[segment];
|
|
297
|
+
if (currentField?.array && !arrayField) {
|
|
298
|
+
arrayField = currentField;
|
|
299
|
+
arraySegmentIndex = pathIndex;
|
|
300
|
+
}
|
|
303
301
|
currentClass = currentField?.type;
|
|
304
302
|
}
|
|
305
303
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
}
|
|
304
|
+
return {
|
|
305
|
+
leafField: currentField,
|
|
306
|
+
arrayField,
|
|
307
|
+
arraySegmentIndex
|
|
308
|
+
};
|
|
309
|
+
}
|
|
311
310
|
|
|
312
|
-
|
|
311
|
+
resolvePath<T extends ModelType>(tableContext: TableContext<T>, path: string[], mode: JSONSqlPathMode): ResolvedPathContext {
|
|
312
|
+
const firstSegment = path[0];
|
|
313
313
|
|
|
314
|
-
if (
|
|
315
|
-
|
|
314
|
+
if (tableContext.simpleFields.has(firstSegment)) {
|
|
315
|
+
const { leafField } = this.#resolveSchemaPath(tableContext, path);
|
|
316
|
+
return { sqlPath: this.buildSqlPath(tableContext, path, mode), leafField };
|
|
316
317
|
}
|
|
317
318
|
|
|
318
|
-
|
|
319
|
+
const { leafField, arrayField, arraySegmentIndex } = this.#resolveSchemaPath(tableContext, path);
|
|
320
|
+
const sqlPath = this.buildSqlPath(tableContext, path, mode);
|
|
321
|
+
|
|
322
|
+
const finalSqlPath = leafField && !leafField.array ? this.castColumn(sqlPath, leafField.type) : sqlPath;
|
|
323
|
+
|
|
324
|
+
const arrayPath = arraySegmentIndex !== undefined ? path.slice(0, arraySegmentIndex + 1) : undefined;
|
|
325
|
+
const subPath = arraySegmentIndex !== undefined ? path.slice(arraySegmentIndex + 1) : undefined;
|
|
326
|
+
|
|
327
|
+
return {
|
|
328
|
+
sqlPath: finalSqlPath,
|
|
329
|
+
leafField,
|
|
330
|
+
arrayField,
|
|
331
|
+
arrayPath,
|
|
332
|
+
subPath
|
|
333
|
+
};
|
|
319
334
|
}
|
|
320
335
|
|
|
321
336
|
#compileClause<T extends ModelType>(
|
|
@@ -349,28 +364,6 @@ CREATE TABLE ${this.escapeIdentifier(context.tableName)} (
|
|
|
349
364
|
}
|
|
350
365
|
}
|
|
351
366
|
|
|
352
|
-
#buildJsonTemplate(queryObject: Record<string, unknown>): Record<string, unknown> {
|
|
353
|
-
const template: Record<string, unknown> = {};
|
|
354
|
-
for (const [key, value] of Object.entries(queryObject)) {
|
|
355
|
-
if (DataUtil.isPlainObject(value)) {
|
|
356
|
-
const firstKey = Object.keys(value)[0];
|
|
357
|
-
const valueObject = castTo<Record<string, unknown>>(value);
|
|
358
|
-
if (firstKey === '$eq') {
|
|
359
|
-
template[key] = valueObject.$eq;
|
|
360
|
-
} else if (valueObject.$eq !== undefined) {
|
|
361
|
-
template[key] = valueObject.$eq;
|
|
362
|
-
} else if (!firstKey.startsWith('$')) {
|
|
363
|
-
template[key] = this.#buildJsonTemplate(valueObject);
|
|
364
|
-
} else {
|
|
365
|
-
throw new RuntimeError(`Unsupported operator ${firstKey} in nested array query`, { category: 'data' });
|
|
366
|
-
}
|
|
367
|
-
} else {
|
|
368
|
-
template[key] = value;
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
return template;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
367
|
#compileSimple<T extends ModelType>(
|
|
375
368
|
tableContext: TableContext<T>,
|
|
376
369
|
item: Record<string, unknown>,
|
|
@@ -389,14 +382,11 @@ CREATE TABLE ${this.escapeIdentifier(context.tableName)} (
|
|
|
389
382
|
const isPlainObject = DataUtil.isPlainObject(value);
|
|
390
383
|
const firstKey = isPlainObject ? Object.keys(value)[0] : '';
|
|
391
384
|
|
|
392
|
-
const { leafField } = this.resolvePath(tableContext, currentPath, 'read');
|
|
393
385
|
const nextIdentificationPath = `${identificationPath}__${index}`;
|
|
394
386
|
|
|
395
387
|
if (isPlainObject) {
|
|
396
388
|
if (firstKey.startsWith('$')) {
|
|
397
389
|
clauses.push(this.#compileOperator(tableContext, currentPath, value as Record<string, unknown>, nextIdentificationPath));
|
|
398
|
-
} else if (leafField?.array) {
|
|
399
|
-
clauses.push(this.#compileOperator(tableContext, currentPath, { $eq: value }, nextIdentificationPath));
|
|
400
390
|
} else {
|
|
401
391
|
clauses.push(this.#compileSimple(tableContext, value as Record<string, unknown>, currentPath, nextIdentificationPath));
|
|
402
392
|
}
|
|
@@ -414,7 +404,9 @@ CREATE TABLE ${this.escapeIdentifier(context.tableName)} (
|
|
|
414
404
|
operation: Record<string, unknown>,
|
|
415
405
|
identificationPath: IdentificationPath = ''
|
|
416
406
|
): QueryClause {
|
|
417
|
-
const
|
|
407
|
+
const resolvedContext = this.resolvePath(tableContext, path, 'read');
|
|
408
|
+
const { sqlPath, leafField, arrayField } = resolvedContext;
|
|
409
|
+
const effectiveArrayField = leafField?.array ? leafField : arrayField;
|
|
418
410
|
const clauses: QueryClause[] = [];
|
|
419
411
|
|
|
420
412
|
let index = 0;
|
|
@@ -432,16 +424,16 @@ CREATE TABLE ${this.escapeIdentifier(context.tableName)} (
|
|
|
432
424
|
|
|
433
425
|
let clause: QueryClause;
|
|
434
426
|
|
|
435
|
-
if (
|
|
427
|
+
if (effectiveArrayField) {
|
|
436
428
|
if (operator === '$eq' || operator === '$ne') {
|
|
437
|
-
const { sql, formatted } = this.compileArrayEquals(
|
|
429
|
+
const { sql, formatted } = this.compileArrayEquals(resolvedContext, identifier, value);
|
|
438
430
|
const finalSql = operator === '$ne' ? `NOT(${sql})` : sql;
|
|
439
431
|
clause = { parameters: { [identifier]: formatted }, sql: finalSql };
|
|
440
432
|
} else if (operator === '$in' || operator === '$nin') {
|
|
441
433
|
if (!Array.isArray(value) || value.length === 0) {
|
|
442
434
|
clause = operator === '$in' ? { sql: '1=0' } : {};
|
|
443
435
|
} else {
|
|
444
|
-
const { sql, formatted } = this.compileArrayAny(
|
|
436
|
+
const { sql, formatted } = this.compileArrayAny(resolvedContext, identifier, value);
|
|
445
437
|
const finalSql = operator === '$nin' ? `NOT(${sql})` : sql;
|
|
446
438
|
clause = { sql: finalSql, parameters: { [identifier]: formatted } };
|
|
447
439
|
}
|
|
@@ -449,11 +441,11 @@ CREATE TABLE ${this.escapeIdentifier(context.tableName)} (
|
|
|
449
441
|
if (!Array.isArray(value) || value.length === 0) {
|
|
450
442
|
clause = { sql: '1=0' };
|
|
451
443
|
} else {
|
|
452
|
-
const { sql, formatted } = this.compileArrayAll(
|
|
444
|
+
const { sql, formatted } = this.compileArrayAll(resolvedContext, identifier, value);
|
|
453
445
|
clause = { sql, parameters: { [identifier]: formatted } };
|
|
454
446
|
}
|
|
455
447
|
} else if (operator === '$exists') {
|
|
456
|
-
const { sql } = this.compileArrayExists(
|
|
448
|
+
const { sql } = this.compileArrayExists(resolvedContext, identifier);
|
|
457
449
|
const finalSql = !value ? `NOT(${sql})` : sql;
|
|
458
450
|
clause = { sql: finalSql };
|
|
459
451
|
} else {
|
package/src/service.ts
CHANGED
|
@@ -15,7 +15,8 @@ import {
|
|
|
15
15
|
type ModelStorageSupport,
|
|
16
16
|
type ModelType,
|
|
17
17
|
NotFoundError,
|
|
18
|
-
type OptionalId
|
|
18
|
+
type OptionalId,
|
|
19
|
+
UniqueError
|
|
19
20
|
} from '@travetto/model';
|
|
20
21
|
import {
|
|
21
22
|
type FullKeyedIndexBody,
|
|
@@ -207,7 +208,14 @@ export abstract class BaseSQLModelService<C = unknown>
|
|
|
207
208
|
|
|
208
209
|
const { sql, values } = this.dialect.buildInsert(tableContext, rawItem);
|
|
209
210
|
|
|
210
|
-
|
|
211
|
+
try {
|
|
212
|
+
await this.connection.execute(sql, values);
|
|
213
|
+
} catch (error) {
|
|
214
|
+
if (error instanceof UniqueError && (error.details?.type === 'query' || error.details?.type === 'index')) {
|
|
215
|
+
throw new UniqueError(modelClass, (error.details.constraint as string) ?? 'unknown', error.details);
|
|
216
|
+
}
|
|
217
|
+
throw error;
|
|
218
|
+
}
|
|
211
219
|
return preppedItem;
|
|
212
220
|
}
|
|
213
221
|
|
|
@@ -487,7 +495,7 @@ export abstract class BaseSQLModelService<C = unknown>
|
|
|
487
495
|
try {
|
|
488
496
|
const result = await this.connection.execute(command.sql, command.values);
|
|
489
497
|
if (command.type === 'update' && result.count === 0) {
|
|
490
|
-
counts.error
|
|
498
|
+
counts.error += 1;
|
|
491
499
|
errors.push(new NotFoundError(modelClass, command.identifier!));
|
|
492
500
|
} else if (command.type === 'delete' && result.count < command.count) {
|
|
493
501
|
counts.delete += result.count;
|
package/src/types.ts
CHANGED
|
@@ -15,3 +15,11 @@ export interface TableContext<T extends ModelType = ModelType> extends SchemaCon
|
|
|
15
15
|
tableName: string;
|
|
16
16
|
database?: string;
|
|
17
17
|
}
|
|
18
|
+
|
|
19
|
+
export interface ResolvedPathContext {
|
|
20
|
+
sqlPath: string;
|
|
21
|
+
leafField?: SchemaFieldConfig;
|
|
22
|
+
arrayField?: SchemaFieldConfig;
|
|
23
|
+
arrayPath?: string[];
|
|
24
|
+
subPath?: string[];
|
|
25
|
+
}
|
package/support/test/query.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { BeforeAll, Suite, Test } from '@travetto/test';
|
|
|
8
8
|
|
|
9
9
|
import { AbstractANSI99Dialect } from '../../src/dialect.ts';
|
|
10
10
|
import { SQLModelSchemaUtil } from '../../src/schema.ts';
|
|
11
|
-
import type { TableContext } from '../../src/types.ts';
|
|
11
|
+
import type { ResolvedPathContext, TableContext } from '../../src/types.ts';
|
|
12
12
|
|
|
13
13
|
@Model()
|
|
14
14
|
class User {
|
|
@@ -50,20 +50,20 @@ class MockDialect extends AbstractANSI99Dialect {
|
|
|
50
50
|
return `$$${index}`;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
compileArrayAll(
|
|
54
|
-
return { sql: `${sqlPath} ALL ${identifier}`, formatted: value };
|
|
53
|
+
compileArrayAll(context: ResolvedPathContext, identifier: string, value: unknown[]) {
|
|
54
|
+
return { sql: `${context.sqlPath} ALL ${identifier}`, formatted: value };
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
compileArrayEquals(
|
|
58
|
-
return { sql: `${sqlPath} EQUALS ${identifier}`, formatted: values };
|
|
57
|
+
compileArrayEquals(context: ResolvedPathContext, identifier: string, values: unknown) {
|
|
58
|
+
return { sql: `${context.sqlPath} EQUALS ${identifier}`, formatted: values };
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
compileArrayAny(
|
|
62
|
-
return { sql: `${sqlPath} ANY ${identifier}`, formatted: values };
|
|
61
|
+
compileArrayAny(context: ResolvedPathContext, identifier: string, values: unknown[]) {
|
|
62
|
+
return { sql: `${context.sqlPath} ANY ${identifier}`, formatted: values };
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
compileArrayExists(
|
|
66
|
-
return { sql: `${sqlPath} IS NOT NULL`, formatted: undefined };
|
|
65
|
+
compileArrayExists(context: ResolvedPathContext, identifier?: string) {
|
|
66
|
+
return { sql: `${context.sqlPath} IS NOT NULL`, formatted: undefined };
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
getRegexOperator(caseInsensitive: boolean) {
|