@travetto/model-sql 8.0.0-alpha.25 → 8.0.0-alpha.26

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/src/types.ts CHANGED
@@ -1,11 +1,17 @@
1
+ import type { ModelType } from '@travetto/model';
1
2
  import type { Class } from '@travetto/runtime';
3
+ import type { SchemaFieldConfig } from '@travetto/schema';
2
4
 
3
- export const TableSymbol = Symbol.for('@travetto/model-sql:table');
5
+ export type JSONSqlPathMode = 'orderBy' | 'createIndex' | 'read';
4
6
 
5
- export type VisitStack = {
6
- [TableSymbol]?: string;
7
- array?: boolean;
8
- type: Class;
9
- name: string;
10
- index?: number;
11
- };
7
+ export interface SchemaContext<T> {
8
+ cls: Class<T>;
9
+ simpleFields: Map<string, SchemaFieldConfig>;
10
+ complexFields: Map<string, SchemaFieldConfig>;
11
+ allFields: SchemaFieldConfig[];
12
+ }
13
+
14
+ export interface TableContext<T extends ModelType = ModelType> extends SchemaContext<T> {
15
+ tableName: string;
16
+ database?: string;
17
+ }
@@ -1,106 +1,142 @@
1
1
  import assert from 'node:assert';
2
2
 
3
- import { castTo } from '@travetto/runtime';
3
+ import { Model, type ModelType } from '@travetto/model';
4
+ import { Registry } from '@travetto/registry';
5
+ import type { Class } from '@travetto/runtime';
4
6
  import { Schema, type SchemaFieldConfig } from '@travetto/schema';
5
- import { Suite, Test } from '@travetto/test';
7
+ import { BeforeAll, Suite, Test } from '@travetto/test';
6
8
 
7
- import { BaseModelSuite } from '@travetto/model/support/test/base.ts';
9
+ import { AbstractANSI99Dialect } from '../../src/dialect.ts';
10
+ import { SQLModelSchemaUtil } from '../../src/schema.ts';
11
+ import type { TableContext } from '../../src/types.ts';
8
12
 
9
- import type { SQLModelService } from '../../src/service.ts';
10
- import type { VisitStack } from '../../src/types.ts';
11
-
12
- @Schema()
13
+ @Model()
13
14
  class User {
14
15
  id: string;
15
16
  name: string;
16
17
  }
17
18
 
18
19
  @Schema()
19
- class WhereTypeAB {
20
- c: number;
20
+ class Nested {
21
+ value: string;
21
22
  }
22
23
 
23
- @Schema()
24
- class WhereTypeA {
25
- d: number;
26
- b: WhereTypeAB;
24
+ @Model()
25
+ class WhereType {
26
+ id: string;
27
+ name: string;
28
+ age: number;
29
+ nestedList: Nested[];
30
+ nestedObj: Nested;
27
31
  }
28
32
 
29
- @Schema()
30
- class WhereTypeD {
31
- e: boolean;
32
- }
33
+ // @ts-expect-error
34
+ class MockDialect extends AbstractANSI99Dialect {
35
+ complexColumnType = 'TEXT';
33
36
 
34
- @Schema()
35
- class WhereTypeG {
36
- z: string[];
37
+ getComplexColumnType(field: SchemaFieldConfig): string {
38
+ return 'TEXT';
39
+ }
40
+
41
+ getColumnType() {
42
+ return 'TEXT';
43
+ }
44
+
45
+ compileJsonIndexPath(columnName: string, jsonPath: string[]): string {
46
+ return `${columnName}->'${jsonPath.join("->'")}'`;
47
+ }
48
+
49
+ override getPlaceholder(index: number) {
50
+ return `$$${index}`;
51
+ }
52
+
53
+ compileArrayAll(sqlPath: string, identifier: string, value: unknown[]) {
54
+ return { sql: `${sqlPath} ALL ${identifier}`, formatted: value };
55
+ }
56
+
57
+ compileArrayEquals(sqlPath: string, identifier: string, values: unknown) {
58
+ return { sql: `${sqlPath} EQUALS ${identifier}`, formatted: values };
59
+ }
60
+
61
+ compileArrayAny(sqlPath: string, identifier: string, values: unknown[]) {
62
+ return { sql: `${sqlPath} ANY ${identifier}`, formatted: values };
63
+ }
64
+
65
+ compileArrayExists(sqlPath: string, identifier: string) {
66
+ return { sql: `${sqlPath} IS NOT NULL`, formatted: undefined };
67
+ }
68
+
69
+ getRegexOperator(caseInsensitive: boolean) {
70
+ return caseInsensitive ? '~*' : '~';
71
+ }
72
+
73
+ formatRegex(source: string) {
74
+ return source;
75
+ }
76
+
77
+ castColumn(sqlPath: string, type: unknown) {
78
+ if (type === Number) {
79
+ return `CAST(${sqlPath} AS NUMERIC)`;
80
+ }
81
+ return sqlPath;
82
+ }
83
+
84
+ async getTableExists(): Promise<boolean> {
85
+ return true;
86
+ }
87
+
88
+ async getExistingColumns(): Promise<Map<string, string>> {
89
+ return new Map();
90
+ }
91
+
92
+ async getExistingIndexes(): Promise<Map<string, string>> {
93
+ return new Map();
94
+ }
95
+
96
+ async dropIndex(): Promise<void> {}
37
97
  }
38
98
 
39
- @Schema()
40
- class WhereType {
41
- a: WhereTypeA[];
42
- d: WhereTypeD;
43
- g: WhereTypeG;
44
- name: number;
45
- age: number;
99
+ const mockDialect = new MockDialect();
100
+
101
+ function getMockContext<T extends ModelType>(modelClass: Class<T>): TableContext<T> {
102
+ return {
103
+ tableName: modelClass.name.toLowerCase(),
104
+ ...SQLModelSchemaUtil.getSchemaContext(modelClass)
105
+ };
46
106
  }
47
107
 
48
108
  @Suite()
49
- export abstract class BaseSQLTest extends BaseModelSuite<SQLModelService> {
50
- get dialect() {
51
- return this.service.then(s => s.client);
109
+ export class SQLQueryCompilerTest {
110
+ @BeforeAll()
111
+ async setup() {
112
+ await Registry.init();
52
113
  }
53
114
 
54
115
  @Test()
55
- async validateQuery() {
56
- const qry = {
57
- $and: [
58
- { a: { b: { c: 5 } } },
59
- { d: { e: true } },
60
- {
61
- $or: [{ name: 5 }, { age: 10 }]
62
- },
63
- { g: { z: { $in: ['a', 'b', 'c'] } } },
64
- { a: { d: { $gt: 20 } } }
65
- ]
66
- };
67
-
68
- const dct = await this.dialect;
69
- dct.resolveName = (stack: VisitStack[]) => {
70
- const field: SchemaFieldConfig = castTo(stack.at(-1));
71
- const parent: SchemaFieldConfig = castTo(stack.at(-2));
72
- return `${field.class ? field.class.name.toString() : parent.name.toString()}.${field.name.toString()}`;
73
- };
74
-
75
- const qryStr = dct.getWhereGroupingSQL(WhereType, qry);
76
- assert(
77
- qryStr ===
78
- "(WhereTypeAB.c = 5 AND WhereTypeD.e = TRUE AND (WhereType.name = 5 OR WhereType.age = 10) AND z.z IN ('a','b','c') AND WhereTypeA.d > 20)"
79
- );
116
+ async testCompileSimple() {
117
+ const context = getMockContext(User);
118
+ const { whereSQL, parameters } = mockDialect.compileWhere(context, { name: 'john' });
119
+ assert(whereSQL === '"name" = $$1');
120
+ assert.deepStrictEqual(parameters, ['john']);
80
121
  }
81
122
 
82
123
  @Test()
83
- async testRegEx() {
84
- const dct = await this.dialect;
85
- dct.resolveName = (stack: VisitStack[]) => {
86
- const field: SchemaFieldConfig = castTo(stack.at(-1));
87
- return `${field.class?.name}.${field.name.toString()}`;
88
- };
89
-
90
- const out = dct.getWhereGroupingSQL(User, {
91
- name: {
92
- $regex: /google.$/
93
- }
124
+ async testCompileOperators() {
125
+ const context = getMockContext(WhereType);
126
+ const { whereSQL, parameters } = mockDialect.compileWhere(context, {
127
+ age: { $gt: 18, $lte: 100 }
94
128
  });
129
+ assert(whereSQL === '("age" > $$1 AND "age" <= $$2)');
130
+ assert.deepStrictEqual(parameters, [18, 100]);
131
+ }
95
132
 
96
- assert(out === `User.name ${dct.SQL_OPS.$regex} 'google.$'`);
97
-
98
- const outBoundary = dct.getWhereGroupingSQL(User, {
99
- name: {
100
- $regex: /\bgoogle\b/
101
- }
133
+ @Test()
134
+ async testCompileNested() {
135
+ const context = getMockContext(WhereType);
136
+ const { whereSQL, parameters } = mockDialect.compileWhere(context, {
137
+ nestedObj: { value: 'test' }
102
138
  });
103
-
104
- assert(outBoundary === `User.name ${dct.SQL_OPS.$regex} '${dct.regexWordBoundary}google${dct.regexWordBoundary}'`);
139
+ assert(whereSQL === '"nestedObj"->\'value\' = $$1');
140
+ assert.deepStrictEqual(parameters, ['test']);
105
141
  }
106
142
  }
package/src/config.ts DELETED
@@ -1,45 +0,0 @@
1
- import { Config } from '@travetto/config';
2
- import { asFull, Runtime } from '@travetto/runtime';
3
-
4
- /**
5
- * SQL Model Config
6
- */
7
- @Config('model.sql')
8
- export class SQLModelConfig<T extends {} = {}> {
9
- /**
10
- * Host to connect to
11
- */
12
- host = '127.0.0.1';
13
- /**
14
- * Default port
15
- */
16
- port = 0;
17
- /**
18
- * Username
19
- */
20
- user = Runtime.production ? '' : 'travetto';
21
- /**
22
- * Password
23
- */
24
- password = Runtime.production ? '' : 'travetto';
25
- /**
26
- * Table prefix
27
- */
28
- namespace = '';
29
- /**
30
- * Database name
31
- */
32
- database = 'app';
33
- /**
34
- * Allow storage modification at runtime
35
- */
36
- modifyStorage?: boolean;
37
- /**
38
- * Db version
39
- */
40
- version = '';
41
- /**
42
- * Raw client options
43
- */
44
- options: T = asFull({});
45
- }
@@ -1,189 +0,0 @@
1
- import { type AsyncContext, AsyncContextValue } from '@travetto/context';
2
- import { castTo, Util } from '@travetto/runtime';
3
-
4
- export type TransactionType = 'required' | 'isolated' | 'force';
5
-
6
- /**
7
- * Connection is a common enough pattern, that it can
8
- * be separated out to allow for differences in connection
9
- * vs querying.
10
- */
11
- export abstract class Connection<C = unknown> {
12
- isolatedTransactions = true;
13
- nestedTransactions = true;
14
-
15
- transactionDialect = {
16
- begin: 'BEGIN;',
17
- beginNested: 'SAVEPOINT $1;',
18
- isolate: 'SET TRANSACTION ISOLATION LEVEL READ COMMITTED;',
19
- rollback: 'ROLLBACK;',
20
- rollbackNested: 'ROLLBACK TO $1;',
21
- commit: 'COMMIT;',
22
- commitNested: 'RELEASE SAVEPOINT $1;'
23
- };
24
-
25
- readonly context: AsyncContext;
26
-
27
- #active = new AsyncContextValue<C>(this);
28
- #activeTx = new AsyncContextValue<boolean>(this);
29
-
30
- constructor(context: AsyncContext) {
31
- this.context = context;
32
- }
33
-
34
- /**
35
- * Get active connection
36
- */
37
- get active(): C | undefined {
38
- return this.#active.get();
39
- }
40
-
41
- /**
42
- * Get active tx state
43
- */
44
- get activeTx(): boolean {
45
- return !!this.#activeTx.get();
46
- }
47
-
48
- /**
49
- * Initialize connection source
50
- */
51
- init?(): Promise<void> | void;
52
-
53
- /**
54
- * Executes a query on the connection
55
- * @param rawConnection
56
- * @param query
57
- */
58
- abstract execute<T = unknown>(rawConnection: C, query: string, values?: unknown[]): Promise<{ records: T[]; count: number }>;
59
-
60
- /**
61
- * Acquire new connection
62
- */
63
- abstract acquire(): Promise<C>;
64
-
65
- /**
66
- * Release provided connection
67
- */
68
- abstract release(rawConnection: C): void;
69
-
70
- /**
71
- * Run operation with active connection
72
- * @param context
73
- * @param operation
74
- * @param args
75
- */
76
- async runWithActive<R>(operation: () => Promise<R>): Promise<R> {
77
- if (this.active) {
78
- return operation();
79
- }
80
-
81
- return this.context.run(async () => {
82
- let connection: C | undefined;
83
- try {
84
- connection = await this.acquire();
85
- this.#active.set(connection);
86
- return await operation();
87
- } finally {
88
- if (connection) {
89
- this.release(connection);
90
- }
91
- }
92
- });
93
- }
94
-
95
- /**
96
- * Iterate with active connection
97
- * @param context
98
- * @param operation
99
- * @param args
100
- */
101
- async *iterateWithActive<R>(operation: () => AsyncIterable<R>): AsyncIterable<R> {
102
- if (this.active) {
103
- yield* operation();
104
- }
105
-
106
- const self = castTo<Connection>(this);
107
- yield* this.context.iterate(async function* () {
108
- try {
109
- self.#active.set(await self.acquire());
110
- yield* operation();
111
- } finally {
112
- if (self.active) {
113
- self.release(self.active);
114
- }
115
- }
116
- });
117
- }
118
-
119
- /**
120
- * Run a function within a valid sql transaction. Relies on @travetto/context.
121
- */
122
- async runWithTransaction<R>(mode: TransactionType, operation: () => Promise<R>): Promise<R> {
123
- if (this.activeTx) {
124
- if (mode === 'isolated' || mode === 'force') {
125
- const txId = mode === 'isolated' ? `tx${Util.uuid()}` : undefined;
126
- try {
127
- await this.startTx(this.active!, txId);
128
- const result = await operation();
129
- await this.commitTx(this.active!, txId);
130
- return result;
131
- } catch (error) {
132
- try {
133
- await this.rollbackTx(this.active!, txId);
134
- } catch {}
135
- throw error;
136
- }
137
- } else {
138
- return await operation();
139
- }
140
- } else {
141
- return this.runWithActive(() => {
142
- this.#activeTx.set(true);
143
- return this.runWithTransaction('force', operation);
144
- });
145
- }
146
- }
147
-
148
- /**
149
- * Start a transaction
150
- */
151
- async startTx(connection: C, transactionId?: string): Promise<void> {
152
- if (transactionId) {
153
- if (this.nestedTransactions) {
154
- await this.execute(connection, this.transactionDialect.beginNested, [transactionId]);
155
- }
156
- } else {
157
- if (this.isolatedTransactions) {
158
- await this.execute(connection, this.transactionDialect.isolate);
159
- }
160
- await this.execute(connection, this.transactionDialect.begin);
161
- }
162
- }
163
-
164
- /**
165
- * Commit active transaction
166
- */
167
- async commitTx(connection: C, transactionId?: string): Promise<void> {
168
- if (transactionId) {
169
- if (this.nestedTransactions) {
170
- await this.execute(connection, this.transactionDialect.commitNested, [transactionId]);
171
- }
172
- } else {
173
- await this.execute(connection, this.transactionDialect.commit);
174
- }
175
- }
176
-
177
- /**
178
- * Rollback active transaction
179
- */
180
- async rollbackTx(connection: C, transactionId?: string): Promise<void> {
181
- if (transactionId) {
182
- if (this.isolatedTransactions) {
183
- await this.execute(connection, this.transactionDialect.rollbackNested, [transactionId]);
184
- }
185
- } else {
186
- await this.execute(connection, this.transactionDialect.rollback);
187
- }
188
- }
189
- }
@@ -1,49 +0,0 @@
1
- import type { AsyncIterableMethodDescriptor, AsyncMethodDescriptor } from '@travetto/runtime';
2
-
3
- import type { Connection, TransactionType } from './base.ts';
4
-
5
- /**
6
- * Indicating something is aware of connections
7
- */
8
- export interface ConnectionAware<C = unknown> {
9
- connection: Connection<C>;
10
- }
11
-
12
- /**
13
- * Decorator to ensure a method runs with a valid connection
14
- * @kind decorator
15
- */
16
- export function Connected() {
17
- return function <T extends { connection?: Connection }>(target: T, property: string, descriptor: AsyncMethodDescriptor<T>): void {
18
- const handle = descriptor.value!;
19
- descriptor.value = function (...args: unknown[]): ReturnType<typeof handle> {
20
- return this.connection!.runWithActive(() => handle.call(this, ...args));
21
- };
22
- };
23
- }
24
-
25
- /**
26
- * Decorator to ensure a method runs with a valid connection
27
- * @kind decorator
28
- */
29
- export function ConnectedIterator() {
30
- return function <T extends { connection?: Connection }>(target: T, property: string, descriptor: AsyncIterableMethodDescriptor<T>): void {
31
- const handle = descriptor.value!;
32
- descriptor.value = async function* (...args: unknown[]): ReturnType<typeof handle> {
33
- yield* this.connection!.iterateWithActive(() => handle.call(this, ...args));
34
- };
35
- };
36
- }
37
-
38
- /**
39
- * Decorator to ensure a method runs with a valid transaction
40
- * @kind decorator
41
- */
42
- export function Transactional(mode: TransactionType = 'required') {
43
- return function <T extends { connection?: Connection }>(target: unknown, property: string, descriptor: AsyncMethodDescriptor<T>): void {
44
- const handle = descriptor.value!;
45
- descriptor.value = function (...args: unknown[]): ReturnType<typeof handle> {
46
- return this.connection!.runWithTransaction(mode, () => handle.call(this, ...args));
47
- };
48
- };
49
- }