@travetto/model-sql 2.1.5 → 2.2.2
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 +2 -2
- package/package.json +6 -6
- package/src/connection/base.ts +7 -7
- package/src/connection/decorator.ts +12 -12
- package/src/dialect/base.ts +72 -49
- package/src/dialect/mysql/connection.ts +5 -5
- package/src/dialect/mysql/dialect.ts +8 -7
- package/src/dialect/postgresql/connection.ts +6 -5
- package/src/dialect/postgresql/dialect.ts +6 -5
- package/src/dialect/sqlite/connection.ts +13 -8
- package/src/dialect/sqlite/dialect.ts +8 -7
- package/src/internal/util.ts +43 -26
- package/src/service.ts +31 -22
- package/src/table-manager.ts +14 -10
|
@@ -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
|
-
|
|
78
|
-
|
|
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
|
-
|
|
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
|
-
|
|
57
|
-
const
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
60
|
-
const
|
|
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
|
|
package/src/internal/util.ts
CHANGED
|
@@ -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,15 +46,17 @@ 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)) {
|
|
47
|
-
for (const k of Object.keys(o)
|
|
49
|
+
for (const k of Object.keys(o)) {
|
|
48
50
|
if (o[k] === null || o[k] === undefined || k === dct.parentPathField.name || k === dct.pathField.name || k === dct.idxField.name) {
|
|
49
51
|
delete o[k];
|
|
50
52
|
} else {
|
|
51
53
|
o[k] = this.cleanResults(dct, o[k]);
|
|
52
54
|
}
|
|
53
55
|
}
|
|
56
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
54
57
|
return { ...o } as unknown as U[];
|
|
55
58
|
} else {
|
|
59
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
56
60
|
return o as unknown as U;
|
|
57
61
|
}
|
|
58
62
|
}
|
|
@@ -60,7 +64,7 @@ export class SQLUtil {
|
|
|
60
64
|
/**
|
|
61
65
|
* Get all available fields at current stack path
|
|
62
66
|
*/
|
|
63
|
-
static getFieldsByLocation(stack: VisitStack[]) {
|
|
67
|
+
static getFieldsByLocation(stack: VisitStack[]): FieldCacheEntry {
|
|
64
68
|
const top = stack[stack.length - 1];
|
|
65
69
|
const cls = SchemaRegistry.get(top.type);
|
|
66
70
|
|
|
@@ -69,6 +73,7 @@ export class SQLUtil {
|
|
|
69
73
|
}
|
|
70
74
|
|
|
71
75
|
if (!cls) { // If a simple type, it is it's own field
|
|
76
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
72
77
|
const field = { ...top } as FieldConfig;
|
|
73
78
|
return {
|
|
74
79
|
local: [field], localMap: { [field.name]: field },
|
|
@@ -94,9 +99,9 @@ export class SQLUtil {
|
|
|
94
99
|
}
|
|
95
100
|
}
|
|
96
101
|
|
|
97
|
-
const ret = {
|
|
98
|
-
localMap: {}
|
|
99
|
-
foreignMap: {}
|
|
102
|
+
const ret: FieldCacheEntry = {
|
|
103
|
+
localMap: {},
|
|
104
|
+
foreignMap: {},
|
|
100
105
|
local: fields.filter(x => !SchemaRegistry.has(x.type) && !x.array),
|
|
101
106
|
foreign: fields.filter(x => SchemaRegistry.has(x.type) || x.array)
|
|
102
107
|
};
|
|
@@ -112,11 +117,11 @@ export class SQLUtil {
|
|
|
112
117
|
/**
|
|
113
118
|
* Process a schema structure, synchronously
|
|
114
119
|
*/
|
|
115
|
-
static visitSchemaSync(config: ClassConfig | FieldConfig, handler: VisitHandler<void>, state: VisitState = { path: [] }) {
|
|
120
|
+
static visitSchemaSync(config: ClassConfig | FieldConfig, handler: VisitHandler<void>, state: VisitState = { path: [] }): void {
|
|
116
121
|
const path = 'class' in config ? this.classToStack(config.class) : [...state.path, config];
|
|
117
122
|
const { local: fields, foreign } = this.getFieldsByLocation(path);
|
|
118
123
|
|
|
119
|
-
const descend = () => {
|
|
124
|
+
const descend = (): void => {
|
|
120
125
|
for (const field of foreign) {
|
|
121
126
|
if (SchemaRegistry.has(field.type)) {
|
|
122
127
|
this.visitSchemaSync(field, handler, { path });
|
|
@@ -140,11 +145,11 @@ export class SQLUtil {
|
|
|
140
145
|
/**
|
|
141
146
|
* Visit a Schema structure
|
|
142
147
|
*/
|
|
143
|
-
static async visitSchema(config: ClassConfig | FieldConfig, handler: VisitHandler<Promise<void>>, state: VisitState = { path: [] }) {
|
|
148
|
+
static async visitSchema(config: ClassConfig | FieldConfig, handler: VisitHandler<Promise<void>>, state: VisitState = { path: [] }): Promise<void> {
|
|
144
149
|
const path = 'class' in config ? this.classToStack(config.class) : [...state.path, config];
|
|
145
150
|
const { local: fields, foreign } = this.getFieldsByLocation(path);
|
|
146
151
|
|
|
147
|
-
const descend = async () => {
|
|
152
|
+
const descend = async (): Promise<void> => {
|
|
148
153
|
for (const field of foreign) {
|
|
149
154
|
if (SchemaRegistry.has(field.type)) {
|
|
150
155
|
await this.visitSchema(field, handler, { path });
|
|
@@ -168,7 +173,7 @@ export class SQLUtil {
|
|
|
168
173
|
/**
|
|
169
174
|
* Process a schema instance by visiting it synchronously. This is synchronous to prevent concurrent calls from breaking
|
|
170
175
|
*/
|
|
171
|
-
static visitSchemaInstance<T extends ModelType>(cls: Class<T>, instance: T, handler: VisitHandler<unknown, VisitInstanceNode<unknown>>) {
|
|
176
|
+
static visitSchemaInstance<T extends ModelType>(cls: Class<T>, instance: T, handler: VisitHandler<unknown, VisitInstanceNode<unknown>>): void {
|
|
172
177
|
const pathObj: unknown[] = [instance];
|
|
173
178
|
this.visitSchemaSync(SchemaRegistry.get(cls), {
|
|
174
179
|
onRoot: (config) => {
|
|
@@ -179,15 +184,16 @@ export class SQLUtil {
|
|
|
179
184
|
},
|
|
180
185
|
onSub: (config) => {
|
|
181
186
|
const { config: field } = config;
|
|
187
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
182
188
|
const topObj = pathObj[pathObj.length - 1] as Record<string, unknown>;
|
|
183
189
|
const top = config.path[config.path.length - 1];
|
|
184
190
|
|
|
185
191
|
if (field.name in topObj) {
|
|
186
192
|
const value = topObj[field.name];
|
|
187
|
-
const
|
|
193
|
+
const values = Array.isArray(value) ? value : [value];
|
|
188
194
|
|
|
189
195
|
let i = 0;
|
|
190
|
-
for (const val of
|
|
196
|
+
for (const val of values) {
|
|
191
197
|
try {
|
|
192
198
|
pathObj.push(val);
|
|
193
199
|
config.path[config.path.length - 1] = { ...top, index: i++ };
|
|
@@ -202,6 +208,7 @@ export class SQLUtil {
|
|
|
202
208
|
},
|
|
203
209
|
onSimple: (config) => {
|
|
204
210
|
const { config: field } = config;
|
|
211
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
205
212
|
const topObj = pathObj[pathObj.length - 1] as Record<string, unknown>;
|
|
206
213
|
const value = topObj[field.name];
|
|
207
214
|
return handler.onSimple({ ...config, value });
|
|
@@ -214,7 +221,7 @@ export class SQLUtil {
|
|
|
214
221
|
*/
|
|
215
222
|
static select<T>(cls: Class<T>, select?: SelectClause<T>): FieldConfig[] {
|
|
216
223
|
if (!select || Object.keys(select).length === 0) {
|
|
217
|
-
return [{ type: cls, name: '*'
|
|
224
|
+
return [{ type: cls, name: '*', owner: cls, array: false }];
|
|
218
225
|
}
|
|
219
226
|
|
|
220
227
|
const { localMap } = this.getFieldsByLocation(this.classToStack(cls));
|
|
@@ -222,7 +229,9 @@ export class SQLUtil {
|
|
|
222
229
|
let toGet = new Set<string>();
|
|
223
230
|
|
|
224
231
|
for (const [k, v] of Object.entries(select)) {
|
|
225
|
-
|
|
232
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
233
|
+
const typedKey = k as keyof typeof select;
|
|
234
|
+
if (!Util.isPlainObject(select[typedKey]) && localMap[k]) {
|
|
226
235
|
if (!v) {
|
|
227
236
|
if (toGet.size === 0) {
|
|
228
237
|
toGet = new Set(SchemaRegistry.get(cls).views[AllViewⲐ].fields);
|
|
@@ -254,6 +263,7 @@ export class SQLUtil {
|
|
|
254
263
|
} else {
|
|
255
264
|
stack.push(field);
|
|
256
265
|
schema = SchemaRegistry.get(field.type);
|
|
266
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
257
267
|
cl = val as Record<string, unknown>;
|
|
258
268
|
}
|
|
259
269
|
}
|
|
@@ -264,18 +274,24 @@ export class SQLUtil {
|
|
|
264
274
|
/**
|
|
265
275
|
* Find all dependent fields via child tables
|
|
266
276
|
*/
|
|
267
|
-
static collectDependents<T>(dct: DialectState, parent: unknown, v: T[], field?: FieldConfig) {
|
|
277
|
+
static collectDependents<T>(dct: DialectState, parent: unknown, v: T[], field?: FieldConfig): Record<string, T> {
|
|
268
278
|
if (field) {
|
|
269
279
|
const isSimple = SchemaRegistry.has(field.type);
|
|
270
280
|
for (const el of v) {
|
|
271
|
-
|
|
281
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
282
|
+
const parentKey = el[dct.parentPathField.name as keyof T] as unknown as string;
|
|
283
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
284
|
+
const root = (parent as Record<string, Record<string, unknown>>)[parentKey];
|
|
272
285
|
if (field.array) {
|
|
273
286
|
if (!root[field.name]) {
|
|
287
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
274
288
|
root[field.name] = [isSimple ? el : el[field.name as keyof T]];
|
|
275
289
|
} else {
|
|
290
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
276
291
|
(root[field.name] as unknown[]).push(isSimple ? el : el[field.name as keyof T]);
|
|
277
292
|
}
|
|
278
293
|
} else {
|
|
294
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
279
295
|
root[field.name] = isSimple ? el : el[field.name as keyof T];
|
|
280
296
|
}
|
|
281
297
|
}
|
|
@@ -283,6 +299,7 @@ export class SQLUtil {
|
|
|
283
299
|
|
|
284
300
|
const mapping: Record<string, T> = {};
|
|
285
301
|
for (const el of v) {
|
|
302
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
286
303
|
const key = el[dct.pathField.name as keyof T];
|
|
287
304
|
if (typeof key === 'string') {
|
|
288
305
|
mapping[key] = el;
|
|
@@ -294,7 +311,7 @@ export class SQLUtil {
|
|
|
294
311
|
/**
|
|
295
312
|
* Build table name via stack path
|
|
296
313
|
*/
|
|
297
|
-
static buildTable(list: VisitStack[]) {
|
|
314
|
+
static buildTable(list: VisitStack[]): string {
|
|
298
315
|
const top = list[list.length - 1];
|
|
299
316
|
if (!top[TableⲐ]) {
|
|
300
317
|
top[TableⲐ] = list.map((el, i) => i === 0 ? ModelRegistry.getStore(el.type) : el.name).join('_');
|
|
@@ -305,7 +322,7 @@ export class SQLUtil {
|
|
|
305
322
|
/**
|
|
306
323
|
* Build property path for a table/field given the current stack
|
|
307
324
|
*/
|
|
308
|
-
static buildPath(list: VisitStack[]) {
|
|
325
|
+
static buildPath(list: VisitStack[]): string {
|
|
309
326
|
return list.map((el, i) => `${el.name}${el.index ? `[${el.index}]` : ''}`).join('.');
|
|
310
327
|
}
|
|
311
328
|
|
|
@@ -313,9 +330,9 @@ export class SQLUtil {
|
|
|
313
330
|
* Get insert statements for a given class, and its child tables
|
|
314
331
|
*/
|
|
315
332
|
static async getInserts<T extends ModelType>(cls: Class<T>, els: T[]): Promise<InsertWrapper[]> {
|
|
316
|
-
const ins
|
|
333
|
+
const ins: Record<string, InsertWrapper> = {};
|
|
317
334
|
|
|
318
|
-
const track = (stack: VisitStack[], value: unknown) => {
|
|
335
|
+
const track = (stack: VisitStack[], value: unknown): void => {
|
|
319
336
|
const key = this.buildTable(stack);
|
|
320
337
|
(ins[key] = ins[key] ?? { stack, records: [] }).records.push({ stack, value });
|
|
321
338
|
};
|
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,
|
|
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
|
|
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>)
|
|
218
|
-
|
|
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
|
-
|
|
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
|
|
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 = [
|
package/src/table-manager.ts
CHANGED
|
@@ -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 (
|
|
45
|
-
if (
|
|
46
|
-
throw
|
|
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: [] }
|
|
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 (
|
|
110
|
+
} catch (err) {
|
|
107
111
|
// Failed to change
|
|
108
|
-
console.error('Unable to change field', { error:
|
|
112
|
+
console.error('Unable to change field', { error: err });
|
|
109
113
|
}
|
|
110
114
|
}
|
|
111
115
|
}
|