@travetto/model-sql 8.0.0-alpha.9 → 8.0.1
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 +4 -6
- package/package.json +19 -18
- package/src/connection.ts +218 -0
- package/src/dialect.ts +847 -0
- package/src/schema.ts +61 -0
- package/src/service.ts +749 -240
- package/src/types.ts +23 -9
- package/support/test/dialect.ts +125 -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,125 @@
|
|
|
1
|
+
import assert from 'node:assert';
|
|
2
|
+
|
|
3
|
+
import { Model, type ModelType, TransientField } from '@travetto/model';
|
|
4
|
+
import { Registry } from '@travetto/registry';
|
|
5
|
+
import type { Class } from '@travetto/runtime';
|
|
6
|
+
import { DiscriminatorField, Required, Schema } from '@travetto/schema';
|
|
7
|
+
import { BeforeAll, Suite, Test } from '@travetto/test';
|
|
8
|
+
|
|
9
|
+
import type { AbstractANSI99Dialect } from '../../src/dialect.ts';
|
|
10
|
+
import { SQLModelSchemaUtil } from '../../src/schema.ts';
|
|
11
|
+
import type { TableContext } from '../../src/types.ts';
|
|
12
|
+
|
|
13
|
+
@Schema()
|
|
14
|
+
class ChildItem {
|
|
15
|
+
name: string;
|
|
16
|
+
age: number;
|
|
17
|
+
active: boolean;
|
|
18
|
+
createdDate: Date;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@Model('dialect_gap_parent')
|
|
22
|
+
class ParentModel {
|
|
23
|
+
id: string;
|
|
24
|
+
title: string;
|
|
25
|
+
child: ChildItem;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@Schema()
|
|
29
|
+
@Model('dialect_gap_simple')
|
|
30
|
+
class SimpleModel {
|
|
31
|
+
id: string;
|
|
32
|
+
@Required()
|
|
33
|
+
requiredField: string;
|
|
34
|
+
optionalField?: string;
|
|
35
|
+
@TransientField()
|
|
36
|
+
transientField: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@Schema()
|
|
40
|
+
@Model('dialect_gap_base_poly')
|
|
41
|
+
abstract class BasePolymorphic {
|
|
42
|
+
id: string;
|
|
43
|
+
@DiscriminatorField()
|
|
44
|
+
type: string;
|
|
45
|
+
@Required()
|
|
46
|
+
sharedRequired: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@Schema()
|
|
50
|
+
@Model('dialect_gap_sub_a')
|
|
51
|
+
class SubTypeA extends BasePolymorphic {
|
|
52
|
+
@Required()
|
|
53
|
+
subTypeAOnlyRequired: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function getTableContext<T extends ModelType>(modelClass: Class<T>): TableContext<T> {
|
|
57
|
+
return {
|
|
58
|
+
tableName: modelClass.name.toLowerCase(),
|
|
59
|
+
...SQLModelSchemaUtil.getSchemaContext(modelClass)
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@Suite({ skip: true })
|
|
64
|
+
export abstract class BaseSQLDialectSuite {
|
|
65
|
+
abstract dialect: AbstractANSI99Dialect;
|
|
66
|
+
|
|
67
|
+
@BeforeAll()
|
|
68
|
+
async setup() {
|
|
69
|
+
await Registry.init();
|
|
70
|
+
SQLModelSchemaUtil.SCHEMA_CACHE.clear();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@Test('Verify DDL column nullability')
|
|
74
|
+
async testDDLNullability() {
|
|
75
|
+
const dialect = this.dialect;
|
|
76
|
+
const quote = dialect.escapeIdentifier('').substring(0, 1) || '"';
|
|
77
|
+
|
|
78
|
+
const simpleContext = getTableContext(SimpleModel);
|
|
79
|
+
const simpleSQL = dialect.getCreateTableSQL(simpleContext);
|
|
80
|
+
|
|
81
|
+
const requiredLine = simpleSQL.split('\n').find(line => line.includes(`${quote}requiredField${quote}`));
|
|
82
|
+
assert(requiredLine?.includes('NOT NULL'));
|
|
83
|
+
|
|
84
|
+
const optionalLine = simpleSQL.split('\n').find(line => line.includes(`${quote}optionalField${quote}`));
|
|
85
|
+
if (optionalLine) {
|
|
86
|
+
assert(!optionalLine.includes('NOT NULL'));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const transientLine = simpleSQL.split('\n').find(line => line.includes(`${quote}transientField${quote}`));
|
|
90
|
+
if (transientLine) {
|
|
91
|
+
assert(!transientLine.includes('NOT NULL'));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const polyContext = getTableContext(BasePolymorphic);
|
|
95
|
+
const polySQL = dialect.getCreateTableSQL(polyContext);
|
|
96
|
+
|
|
97
|
+
const sharedRequiredLine = polySQL.split('\n').find(line => line.includes(`${quote}sharedRequired${quote}`));
|
|
98
|
+
assert(sharedRequiredLine?.includes('NOT NULL'));
|
|
99
|
+
|
|
100
|
+
const subTypeLine = polySQL.split('\n').find(line => line.includes(`${quote}subTypeAOnlyRequired${quote}`));
|
|
101
|
+
if (subTypeLine) {
|
|
102
|
+
assert(!subTypeLine.includes('NOT NULL'));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
@Test()
|
|
107
|
+
async testFormatJsonPathEscaping() {
|
|
108
|
+
const simpleJsonPath = this.dialect.formatJsonPath(['child', 'age']);
|
|
109
|
+
assert(simpleJsonPath === 'child.age');
|
|
110
|
+
|
|
111
|
+
const complexJsonPath = this.dialect.formatJsonPath(['child', 'first name']);
|
|
112
|
+
assert(complexJsonPath === 'child."first name"');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
@Test()
|
|
116
|
+
async testPartialUpdateSetsWithoutRegex() {
|
|
117
|
+
const tableContext = getTableContext(ParentModel);
|
|
118
|
+
const quote = this.dialect.escapeIdentifier('').substring(0, 1) || '"';
|
|
119
|
+
const placeholder = this.dialect.getPlaceholder(1);
|
|
120
|
+
|
|
121
|
+
const { sets, values } = this.dialect.compilePartialUpdate(tableContext, { title: 'Updated Title' });
|
|
122
|
+
assert.deepStrictEqual(sets, [`${quote}title${quote} = ${placeholder}`]);
|
|
123
|
+
assert.deepStrictEqual(values, ['Updated Title']);
|
|
124
|
+
}
|
|
125
|
+
}
|
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
|
-
}
|