@travetto/model-sql 8.0.0-alpha.3 → 8.0.0-alpha.30
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 +29 -9
- package/__index__.ts +3 -6
- package/package.json +8 -7
- package/src/connection.ts +218 -0
- package/src/dialect.ts +825 -0
- package/src/schema.ts +46 -0
- package/src/service.ts +749 -240
- package/src/types.ts +23 -9
- package/support/test/dialect.ts +157 -0
- package/support/test/query.ts +115 -72
- package/src/config.ts +0 -45
- package/src/connection/base.ts +0 -188
- package/src/connection/decorator.ts +0 -54
- package/src/dialect/base.ts +0 -1102
- package/src/internal/types.ts +0 -64
- package/src/table-manager.ts +0 -162
- package/src/util.ts +0 -331
package/src/types.ts
CHANGED
|
@@ -1,11 +1,25 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { ModelType } from '@travetto/model';
|
|
2
|
+
import type { Class } from '@travetto/runtime';
|
|
3
|
+
import type { SchemaFieldConfig } from '@travetto/schema';
|
|
2
4
|
|
|
3
|
-
export
|
|
5
|
+
export type JSONSqlPathMode = 'orderBy' | 'createIndex' | 'read';
|
|
4
6
|
|
|
5
|
-
export
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ResolvedPathContext {
|
|
20
|
+
sqlPath: string;
|
|
21
|
+
leafField?: SchemaFieldConfig;
|
|
22
|
+
arrayField?: SchemaFieldConfig;
|
|
23
|
+
arrayPath?: string[];
|
|
24
|
+
subPath?: string[];
|
|
25
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import assert from 'node:assert';
|
|
2
|
+
|
|
3
|
+
import { Model, type ModelType } from '@travetto/model';
|
|
4
|
+
import { Registry } from '@travetto/registry';
|
|
5
|
+
import type { Class } from '@travetto/runtime';
|
|
6
|
+
import { Schema } from '@travetto/schema';
|
|
7
|
+
import { BeforeAll, Suite, Test } from '@travetto/test';
|
|
8
|
+
|
|
9
|
+
import { MysqlDialect } from '../../../model-mysql/src/dialect.ts';
|
|
10
|
+
import { PostgresDialect } from '../../../model-postgres/src/dialect.ts';
|
|
11
|
+
import { SqliteDialect } from '../../../model-sqlite/src/dialect.ts';
|
|
12
|
+
import { SQLModelSchemaUtil } from '../../src/schema.ts';
|
|
13
|
+
import type { TableContext } from '../../src/types.ts';
|
|
14
|
+
|
|
15
|
+
@Schema()
|
|
16
|
+
class ChildItem {
|
|
17
|
+
name: string;
|
|
18
|
+
age: number;
|
|
19
|
+
active: boolean;
|
|
20
|
+
createdDate: Date;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@Model()
|
|
24
|
+
class ParentModel {
|
|
25
|
+
id: string;
|
|
26
|
+
title: string;
|
|
27
|
+
child: ChildItem;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getTableContext<T extends ModelType>(modelClass: Class<T>): TableContext<T> {
|
|
31
|
+
return {
|
|
32
|
+
tableName: modelClass.name.toLowerCase(),
|
|
33
|
+
...SQLModelSchemaUtil.getSchemaContext(modelClass)
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@Suite()
|
|
38
|
+
export class SQLDialectGapsTest {
|
|
39
|
+
@BeforeAll()
|
|
40
|
+
async setup() {
|
|
41
|
+
await Registry.init();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@Test()
|
|
45
|
+
async testIndexAndQueryExpressionParity() {
|
|
46
|
+
const tableContext = getTableContext(ParentModel);
|
|
47
|
+
const mysqlDialect = new MysqlDialect();
|
|
48
|
+
const postgresDialect = new PostgresDialect();
|
|
49
|
+
const sqliteDialect = new SqliteDialect();
|
|
50
|
+
|
|
51
|
+
// Verify MySQL path resolution matches index creation
|
|
52
|
+
const mysqlAgeResolved = mysqlDialect.resolvePath(tableContext, ['child', 'age'], 'read');
|
|
53
|
+
assert(mysqlAgeResolved.sqlPath === "CAST(`child`->>'$.age' AS DECIMAL)");
|
|
54
|
+
|
|
55
|
+
const mysqlNameResolved = mysqlDialect.resolvePath(tableContext, ['child', 'name'], 'read');
|
|
56
|
+
assert(mysqlNameResolved.sqlPath === "(CAST(`child`->>'$.name' AS CHAR(255)) COLLATE utf8mb4_bin)");
|
|
57
|
+
|
|
58
|
+
// Verify Postgres path resolution matches index creation
|
|
59
|
+
const postgresAgeResolved = postgresDialect.resolvePath(tableContext, ['child', 'age'], 'read');
|
|
60
|
+
assert(postgresAgeResolved.sqlPath === '((("child"->>\'age\')))::NUMERIC');
|
|
61
|
+
|
|
62
|
+
// Verify SQLite path resolution matches index creation
|
|
63
|
+
const sqliteAgeResolved = sqliteDialect.resolvePath(tableContext, ['child', 'age'], 'read');
|
|
64
|
+
assert(sqliteAgeResolved.sqlPath === 'CAST(json_extract("child", \'$.age\') AS NUMERIC)');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
@Test()
|
|
68
|
+
async testSqliteArraySubObjectPatch() {
|
|
69
|
+
const tableContext = getTableContext(ParentModel);
|
|
70
|
+
const sqliteDialect = new SqliteDialect();
|
|
71
|
+
|
|
72
|
+
const resolvedContext = sqliteDialect.resolvePath(tableContext, ['child', 'name'], 'read');
|
|
73
|
+
const { sql } = sqliteDialect.compileArrayEquals(resolvedContext, '$$1', { name: 'bob' });
|
|
74
|
+
|
|
75
|
+
assert(sql.includes('json_patch('));
|
|
76
|
+
assert(sql.includes('= elem.value'));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
@Test()
|
|
80
|
+
async testCreateIndexes() {
|
|
81
|
+
const tableContext = getTableContext(ParentModel);
|
|
82
|
+
const mysqlDialect = new MysqlDialect();
|
|
83
|
+
const postgresDialect = new PostgresDialect();
|
|
84
|
+
|
|
85
|
+
// Verify create index SQL contains the resolved expressions
|
|
86
|
+
const mysqlCreateIndexSql = mysqlDialect.getCreateIndexSQL(tableContext, {
|
|
87
|
+
type: 'query',
|
|
88
|
+
name: 'child_age',
|
|
89
|
+
fields: [{ 'child.age': 1 }]
|
|
90
|
+
});
|
|
91
|
+
assert(mysqlCreateIndexSql.includes("(CAST(`child`->>'$.age' AS DECIMAL))"));
|
|
92
|
+
|
|
93
|
+
const postgresCreateIndexSql = postgresDialect.getCreateIndexSQL(tableContext, {
|
|
94
|
+
type: 'query',
|
|
95
|
+
name: 'child_age',
|
|
96
|
+
fields: [{ 'child.age': 1 }]
|
|
97
|
+
});
|
|
98
|
+
assert(postgresCreateIndexSql.includes('((("child"->>\'age\')))::NUMERIC)'));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
@Test()
|
|
102
|
+
async testMysqlExistingIndexesParsing() {
|
|
103
|
+
const mysqlDialect = new MysqlDialect();
|
|
104
|
+
const existingIndexRecords = [
|
|
105
|
+
{
|
|
106
|
+
name: 'idx_parentmodel_child_age',
|
|
107
|
+
tableName: 'parentmodel',
|
|
108
|
+
nonUnique: 1,
|
|
109
|
+
indexColumns: "(CAST(`child`->>'$.age' AS DECIMAL))"
|
|
110
|
+
}
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
const parsedIndexes = mysqlDialect.parseExistingIndexes(existingIndexRecords);
|
|
114
|
+
assert(parsedIndexes.size === 1);
|
|
115
|
+
assert(parsedIndexes.has('idx_parentmodel_child_age'));
|
|
116
|
+
|
|
117
|
+
const indexDefinition = parsedIndexes.get('idx_parentmodel_child_age')!;
|
|
118
|
+
assert(indexDefinition.includes('CREATE INDEX `idx_parentmodel_child_age` ON `parentmodel`'));
|
|
119
|
+
assert(indexDefinition.includes("(CAST(`child`->>'$.age' AS DECIMAL))"));
|
|
120
|
+
|
|
121
|
+
const normalizedDefinition = mysqlDialect.normalizeIndexDefinition(indexDefinition);
|
|
122
|
+
assert(normalizedDefinition.length > 0);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
@Test()
|
|
126
|
+
async testMysqlAlterColumnType() {
|
|
127
|
+
const tableContext = getTableContext(ParentModel);
|
|
128
|
+
const mysqlDialect = new MysqlDialect();
|
|
129
|
+
|
|
130
|
+
const alterColumnSql = mysqlDialect.getAlterColumnTypeSQL(tableContext, 'title', 'VARCHAR(255)', 'INT');
|
|
131
|
+
assert(alterColumnSql === 'ALTER TABLE `parentmodel` MODIFY COLUMN `title` VARCHAR(255);');
|
|
132
|
+
|
|
133
|
+
const noopAlterColumnSql = mysqlDialect.getAlterColumnTypeSQL(tableContext, 'title', 'VARCHAR(255)', 'VARCHAR(255)');
|
|
134
|
+
assert(noopAlterColumnSql === undefined);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
@Test()
|
|
138
|
+
async testFormatJsonPathEscaping() {
|
|
139
|
+
const mysqlDialect = new MysqlDialect();
|
|
140
|
+
|
|
141
|
+
const simpleJsonPath = mysqlDialect.formatJsonPath(['child', 'age']);
|
|
142
|
+
assert(simpleJsonPath === 'child.age');
|
|
143
|
+
|
|
144
|
+
const complexJsonPath = mysqlDialect.formatJsonPath(['child', 'first name']);
|
|
145
|
+
assert(complexJsonPath === 'child."first name"');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
@Test()
|
|
149
|
+
async testPartialUpdateSetsWithoutRegex() {
|
|
150
|
+
const tableContext = getTableContext(ParentModel);
|
|
151
|
+
const sqliteDialect = new SqliteDialect();
|
|
152
|
+
|
|
153
|
+
const { sets, values } = sqliteDialect.compilePartialUpdate(tableContext, { title: 'Updated Title' });
|
|
154
|
+
assert.deepStrictEqual(sets, ['"title" = ?']);
|
|
155
|
+
assert.deepStrictEqual(values, ['Updated Title']);
|
|
156
|
+
}
|
|
157
|
+
}
|
package/support/test/query.ts
CHANGED
|
@@ -1,103 +1,146 @@
|
|
|
1
1
|
import assert from 'node:assert';
|
|
2
2
|
|
|
3
|
+
import { Model, type ModelType } from '@travetto/model';
|
|
4
|
+
import { Registry } from '@travetto/registry';
|
|
5
|
+
import type { Class } from '@travetto/runtime';
|
|
3
6
|
import { Schema, type SchemaFieldConfig } from '@travetto/schema';
|
|
4
|
-
import { Suite, Test } from '@travetto/test';
|
|
5
|
-
import { castTo } from '@travetto/runtime';
|
|
6
|
-
import { BaseModelSuite } from '@travetto/model/support/test/base.ts';
|
|
7
|
+
import { BeforeAll, Suite, Test } from '@travetto/test';
|
|
7
8
|
|
|
8
|
-
import
|
|
9
|
-
import
|
|
9
|
+
import { AbstractANSI99Dialect } from '../../src/dialect.ts';
|
|
10
|
+
import { SQLModelSchemaUtil } from '../../src/schema.ts';
|
|
11
|
+
import type { ResolvedPathContext, TableContext } from '../../src/types.ts';
|
|
10
12
|
|
|
11
|
-
@
|
|
13
|
+
@Model()
|
|
12
14
|
class User {
|
|
13
15
|
id: string;
|
|
14
16
|
name: string;
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
@Schema()
|
|
18
|
-
class
|
|
19
|
-
|
|
20
|
+
class Nested {
|
|
21
|
+
value: string;
|
|
20
22
|
}
|
|
21
23
|
|
|
22
|
-
@
|
|
23
|
-
class
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
@Model()
|
|
25
|
+
class WhereType {
|
|
26
|
+
id: string;
|
|
27
|
+
name: string;
|
|
28
|
+
age: number;
|
|
29
|
+
nestedList: Nested[];
|
|
30
|
+
nestedObj: Nested;
|
|
26
31
|
}
|
|
27
32
|
|
|
28
|
-
@
|
|
29
|
-
class
|
|
30
|
-
|
|
31
|
-
}
|
|
33
|
+
// @ts-expect-error
|
|
34
|
+
class MockDialect extends AbstractANSI99Dialect {
|
|
35
|
+
complexColumnType = 'TEXT';
|
|
32
36
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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(context: ResolvedPathContext, identifier: string, value: unknown[]) {
|
|
54
|
+
return { sql: `${context.sqlPath} ALL ${identifier}`, formatted: value };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
compileArrayEquals(context: ResolvedPathContext, identifier: string, values: unknown) {
|
|
58
|
+
return { sql: `${context.sqlPath} EQUALS ${identifier}`, formatted: values };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
compileArrayAny(context: ResolvedPathContext, identifier: string, values: unknown[]) {
|
|
62
|
+
return { sql: `${context.sqlPath} ANY ${identifier}`, formatted: values };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
compileArrayExists(context: ResolvedPathContext, identifier?: string) {
|
|
66
|
+
return { sql: `${context.sqlPath} IS NOT NULL`, formatted: undefined };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
compileArrayRegex(context: ResolvedPathContext, identifier: string, value: RegExp | string) {
|
|
70
|
+
return { sql: `${context.sqlPath} REGEX ${identifier}`, formatted: value };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
getRegexOperator(caseInsensitive: boolean) {
|
|
74
|
+
return caseInsensitive ? '~*' : '~';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
formatRegex(source: string) {
|
|
78
|
+
return source;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
castColumn(sqlPath: string, type: unknown) {
|
|
82
|
+
if (type === Number) {
|
|
83
|
+
return `CAST(${sqlPath} AS NUMERIC)`;
|
|
84
|
+
}
|
|
85
|
+
return sqlPath;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async getTableExists(): Promise<boolean> {
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async getExistingColumns(): Promise<Map<string, string>> {
|
|
93
|
+
return new Map();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async getExistingIndexes(): Promise<Map<string, string>> {
|
|
97
|
+
return new Map();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async dropIndex(): Promise<void> {}
|
|
36
101
|
}
|
|
37
102
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
103
|
+
const mockDialect = new MockDialect();
|
|
104
|
+
|
|
105
|
+
function getMockContext<T extends ModelType>(modelClass: Class<T>): TableContext<T> {
|
|
106
|
+
return {
|
|
107
|
+
tableName: modelClass.name.toLowerCase(),
|
|
108
|
+
...SQLModelSchemaUtil.getSchemaContext(modelClass)
|
|
109
|
+
};
|
|
45
110
|
}
|
|
46
111
|
|
|
47
112
|
@Suite()
|
|
48
|
-
export
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
113
|
+
export class SQLQueryCompilerTest {
|
|
114
|
+
@BeforeAll()
|
|
115
|
+
async setup() {
|
|
116
|
+
await Registry.init();
|
|
52
117
|
}
|
|
53
118
|
|
|
54
119
|
@Test()
|
|
55
|
-
async
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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(qryStr === "(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)");
|
|
120
|
+
async testCompileSimple() {
|
|
121
|
+
const context = getMockContext(User);
|
|
122
|
+
const { whereSQL, parameters } = mockDialect.compileWhere(context, { name: 'john' });
|
|
123
|
+
assert(whereSQL === '"name" = $$1');
|
|
124
|
+
assert.deepStrictEqual(parameters, ['john']);
|
|
77
125
|
}
|
|
78
126
|
|
|
79
127
|
@Test()
|
|
80
|
-
async
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
return `${field.class?.name}.${field.name.toString()}`;
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
const out = dct.getWhereGroupingSQL(User, {
|
|
88
|
-
name: {
|
|
89
|
-
$regex: /google.$/
|
|
90
|
-
}
|
|
128
|
+
async testCompileOperators() {
|
|
129
|
+
const context = getMockContext(WhereType);
|
|
130
|
+
const { whereSQL, parameters } = mockDialect.compileWhere(context, {
|
|
131
|
+
age: { $gt: 18, $lte: 100 }
|
|
91
132
|
});
|
|
133
|
+
assert(whereSQL === '("age" > $$1 AND "age" <= $$2)');
|
|
134
|
+
assert.deepStrictEqual(parameters, [18, 100]);
|
|
135
|
+
}
|
|
92
136
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
137
|
+
@Test()
|
|
138
|
+
async testCompileNested() {
|
|
139
|
+
const context = getMockContext(WhereType);
|
|
140
|
+
const { whereSQL, parameters } = mockDialect.compileWhere(context, {
|
|
141
|
+
nestedObj: { value: 'test' }
|
|
99
142
|
});
|
|
100
|
-
|
|
101
|
-
assert(
|
|
143
|
+
assert(whereSQL === '"nestedObj"->\'value\' = $$1');
|
|
144
|
+
assert.deepStrictEqual(parameters, ['test']);
|
|
102
145
|
}
|
|
103
|
-
}
|
|
146
|
+
}
|
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
|
-
}
|
package/src/connection/base.ts
DELETED
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
import { castTo, Util } from '@travetto/runtime';
|
|
2
|
-
import { type AsyncContext, AsyncContextValue } from '@travetto/context';
|
|
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
|
-
|
|
13
|
-
isolatedTransactions = true;
|
|
14
|
-
nestedTransactions = true;
|
|
15
|
-
|
|
16
|
-
transactionDialect = {
|
|
17
|
-
begin: 'BEGIN;',
|
|
18
|
-
beginNested: 'SAVEPOINT $1;',
|
|
19
|
-
isolate: 'SET TRANSACTION ISOLATION LEVEL READ COMMITTED;',
|
|
20
|
-
rollback: 'ROLLBACK;',
|
|
21
|
-
rollbackNested: 'ROLLBACK TO $1;',
|
|
22
|
-
commit: 'COMMIT;',
|
|
23
|
-
commitNested: 'RELEASE SAVEPOINT $1;'
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
readonly context: AsyncContext;
|
|
27
|
-
|
|
28
|
-
#active = new AsyncContextValue<C>(this);
|
|
29
|
-
#activeTx = new AsyncContextValue<boolean>(this);
|
|
30
|
-
|
|
31
|
-
constructor(context: AsyncContext) {
|
|
32
|
-
this.context = context;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Get active connection
|
|
37
|
-
*/
|
|
38
|
-
get active(): C | undefined {
|
|
39
|
-
return this.#active.get();
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Get active tx state
|
|
44
|
-
*/
|
|
45
|
-
get activeTx(): boolean {
|
|
46
|
-
return !!this.#activeTx.get();
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Initialize connection source
|
|
51
|
-
*/
|
|
52
|
-
init?(): Promise<void> | void;
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Executes a query on the connection
|
|
56
|
-
* @param rawConnection
|
|
57
|
-
* @param query
|
|
58
|
-
*/
|
|
59
|
-
abstract execute<T = unknown>(rawConnection: C, query: string, values?: unknown[]): Promise<{ records: T[], count: number }>;
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Acquire new connection
|
|
63
|
-
*/
|
|
64
|
-
abstract acquire(): Promise<C>;
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Release provided connection
|
|
68
|
-
*/
|
|
69
|
-
abstract release(rawConnection: C): void;
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Run operation with active connection
|
|
73
|
-
* @param context
|
|
74
|
-
* @param operation
|
|
75
|
-
* @param args
|
|
76
|
-
*/
|
|
77
|
-
async runWithActive<R>(operation: () => Promise<R>): Promise<R> {
|
|
78
|
-
if (this.active) {
|
|
79
|
-
return operation();
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return this.context.run(async () => {
|
|
83
|
-
let connection;
|
|
84
|
-
try {
|
|
85
|
-
connection = await this.acquire();
|
|
86
|
-
this.#active.set(connection);
|
|
87
|
-
return await operation();
|
|
88
|
-
} finally {
|
|
89
|
-
if (connection) {
|
|
90
|
-
this.release(connection);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Iterate with active connection
|
|
98
|
-
* @param context
|
|
99
|
-
* @param operation
|
|
100
|
-
* @param args
|
|
101
|
-
*/
|
|
102
|
-
async * iterateWithActive<R>(operation: () => AsyncIterable<R>): AsyncIterable<R> {
|
|
103
|
-
if (this.active) {
|
|
104
|
-
yield* operation();
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const self = castTo<Connection>(this);
|
|
108
|
-
yield* this.context.iterate(async function* () {
|
|
109
|
-
try {
|
|
110
|
-
self.#active.set(await self.acquire());
|
|
111
|
-
yield* operation();
|
|
112
|
-
} finally {
|
|
113
|
-
if (self.active) {
|
|
114
|
-
self.release(self.active);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Run a function within a valid sql transaction. Relies on @travetto/context.
|
|
122
|
-
*/
|
|
123
|
-
async runWithTransaction<R>(mode: TransactionType, operation: () => Promise<R>): Promise<R> {
|
|
124
|
-
if (this.activeTx) {
|
|
125
|
-
if (mode === 'isolated' || mode === 'force') {
|
|
126
|
-
const txId = mode === 'isolated' ? `tx${Util.uuid()}` : undefined;
|
|
127
|
-
try {
|
|
128
|
-
await this.startTx(this.active!, txId);
|
|
129
|
-
const result = await operation();
|
|
130
|
-
await this.commitTx(this.active!, txId);
|
|
131
|
-
return result;
|
|
132
|
-
} catch (error) {
|
|
133
|
-
try { await this.rollbackTx(this.active!, txId); } catch { }
|
|
134
|
-
throw error;
|
|
135
|
-
}
|
|
136
|
-
} else {
|
|
137
|
-
return await operation();
|
|
138
|
-
}
|
|
139
|
-
} else {
|
|
140
|
-
return this.runWithActive(() => {
|
|
141
|
-
this.#activeTx.set(true);
|
|
142
|
-
return this.runWithTransaction('force', operation);
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Start a transaction
|
|
149
|
-
*/
|
|
150
|
-
async startTx(connection: C, transactionId?: string): Promise<void> {
|
|
151
|
-
if (transactionId) {
|
|
152
|
-
if (this.nestedTransactions) {
|
|
153
|
-
await this.execute(connection, this.transactionDialect.beginNested, [transactionId]);
|
|
154
|
-
}
|
|
155
|
-
} else {
|
|
156
|
-
if (this.isolatedTransactions) {
|
|
157
|
-
await this.execute(connection, this.transactionDialect.isolate);
|
|
158
|
-
}
|
|
159
|
-
await this.execute(connection, this.transactionDialect.begin);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Commit active transaction
|
|
165
|
-
*/
|
|
166
|
-
async commitTx(connection: C, transactionId?: string): Promise<void> {
|
|
167
|
-
if (transactionId) {
|
|
168
|
-
if (this.nestedTransactions) {
|
|
169
|
-
await this.execute(connection, this.transactionDialect.commitNested, [transactionId]);
|
|
170
|
-
}
|
|
171
|
-
} else {
|
|
172
|
-
await this.execute(connection, this.transactionDialect.commit);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Rollback active transaction
|
|
178
|
-
*/
|
|
179
|
-
async rollbackTx(connection: C, transactionId?: string): Promise<void> {
|
|
180
|
-
if (transactionId) {
|
|
181
|
-
if (this.isolatedTransactions) {
|
|
182
|
-
await this.execute(connection, this.transactionDialect.rollbackNested, [transactionId]);
|
|
183
|
-
}
|
|
184
|
-
} else {
|
|
185
|
-
await this.execute(connection, this.transactionDialect.rollback);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
}
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import type { AsyncIterableMethodDescriptor, AsyncMethodDescriptor } from '@travetto/runtime';
|
|
2
|
-
import type { Connection, TransactionType } from './base.ts';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Indicating something is aware of connections
|
|
6
|
-
*/
|
|
7
|
-
export interface ConnectionAware<C = unknown> {
|
|
8
|
-
connection: Connection<C>;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Decorator to ensure a method runs with a valid connection
|
|
13
|
-
* @kind decorator
|
|
14
|
-
*/
|
|
15
|
-
export function Connected() {
|
|
16
|
-
return function <T extends { connection?: Connection }>(
|
|
17
|
-
target: T, property: string, descriptor: AsyncMethodDescriptor<T>
|
|
18
|
-
): void {
|
|
19
|
-
const handle = descriptor.value!;
|
|
20
|
-
descriptor.value = function (...args: unknown[]): ReturnType<typeof handle> {
|
|
21
|
-
return this.connection!.runWithActive(() => handle.call(this, ...args));
|
|
22
|
-
};
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Decorator to ensure a method runs with a valid connection
|
|
28
|
-
* @kind decorator
|
|
29
|
-
*/
|
|
30
|
-
export function ConnectedIterator() {
|
|
31
|
-
return function <T extends { connection?: Connection }>(
|
|
32
|
-
target: T, property: string, descriptor: AsyncIterableMethodDescriptor<T>
|
|
33
|
-
): void {
|
|
34
|
-
const handle = descriptor.value!;
|
|
35
|
-
descriptor.value = async function* (...args: unknown[]): ReturnType<typeof handle> {
|
|
36
|
-
yield* this.connection!.iterateWithActive(() => handle.call(this, ...args));
|
|
37
|
-
};
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Decorator to ensure a method runs with a valid transaction
|
|
43
|
-
* @kind decorator
|
|
44
|
-
*/
|
|
45
|
-
export function Transactional(mode: TransactionType = 'required') {
|
|
46
|
-
return function <T extends { connection?: Connection }>(
|
|
47
|
-
target: unknown, property: string, descriptor: AsyncMethodDescriptor<T>
|
|
48
|
-
): void {
|
|
49
|
-
const handle = descriptor.value!;
|
|
50
|
-
descriptor.value = function (...args: unknown[]): ReturnType<typeof handle> {
|
|
51
|
-
return this.connection!.runWithTransaction(mode, () => handle.call(this, ...args));
|
|
52
|
-
};
|
|
53
|
-
};
|
|
54
|
-
}
|