@travetto/model-sql 8.0.0-alpha.9 → 8.0.0
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/service.ts
CHANGED
|
@@ -1,329 +1,838 @@
|
|
|
1
|
+
import { Injectable } from '@travetto/di';
|
|
1
2
|
import {
|
|
3
|
+
type BulkOperation,
|
|
4
|
+
type BulkResponse,
|
|
5
|
+
type IndexConfig,
|
|
6
|
+
type ModelBulkSupport,
|
|
7
|
+
ModelBulkUtil,
|
|
8
|
+
type ModelCrudProvider,
|
|
9
|
+
type ModelCrudSupport,
|
|
10
|
+
ModelCrudUtil,
|
|
11
|
+
type ModelExpirySupport,
|
|
12
|
+
ModelExpiryUtil,
|
|
13
|
+
type ModelListOptions,
|
|
14
|
+
ModelRegistryIndex,
|
|
15
|
+
type ModelStorageSupport,
|
|
2
16
|
type ModelType,
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
17
|
+
NotFoundError,
|
|
18
|
+
type OptionalId,
|
|
19
|
+
UniqueError
|
|
6
20
|
} from '@travetto/model';
|
|
7
|
-
import { castTo, type Class } from '@travetto/runtime';
|
|
8
|
-
import { DataUtil } from '@travetto/schema';
|
|
9
|
-
import type { AsyncContext } from '@travetto/context';
|
|
10
|
-
import { Injectable, PostConstruct } from '@travetto/di';
|
|
11
21
|
import {
|
|
12
|
-
type
|
|
13
|
-
type
|
|
14
|
-
|
|
22
|
+
type FullKeyedIndexBody,
|
|
23
|
+
type FullKeyedIndexWithPartialBody,
|
|
24
|
+
type KeyedIndexBody,
|
|
25
|
+
type KeyedIndexSelection,
|
|
26
|
+
ModelIndexedComputedIndex,
|
|
27
|
+
type ModelIndexedSearchOptions,
|
|
28
|
+
type ModelIndexedSupport,
|
|
29
|
+
ModelIndexedUtil,
|
|
30
|
+
type ModelPageOptions,
|
|
31
|
+
type ModelPageResult,
|
|
32
|
+
type SingleItemIndex,
|
|
33
|
+
type SortedIndex,
|
|
34
|
+
type SortedIndexSelection,
|
|
35
|
+
type SortedIndexSelectionType,
|
|
36
|
+
warnIfIndexedUniqueIndex,
|
|
37
|
+
warnIfNonIndexedIndex
|
|
38
|
+
} from '@travetto/model-indexed';
|
|
39
|
+
import {
|
|
40
|
+
type ModelQuery,
|
|
41
|
+
type ModelQueryCrudSupport,
|
|
42
|
+
ModelQueryCrudUtil,
|
|
15
43
|
type ModelQueryFacet,
|
|
44
|
+
type ModelQueryFacetSupport,
|
|
45
|
+
type ModelQuerySuggestSupport,
|
|
46
|
+
ModelQuerySuggestUtil,
|
|
47
|
+
type ModelQuerySupport,
|
|
48
|
+
ModelQueryUtil,
|
|
49
|
+
type PageableModelQuery,
|
|
50
|
+
QueryVerifier,
|
|
51
|
+
type ValidStringFields,
|
|
52
|
+
type WhereClause
|
|
16
53
|
} from '@travetto/model-query';
|
|
54
|
+
import { type Class, castTo, JSONUtil } from '@travetto/runtime';
|
|
55
|
+
import { WorkPool } from '@travetto/worker';
|
|
17
56
|
|
|
18
|
-
import type {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import type {
|
|
22
|
-
import { TableManager } from './table-manager.ts';
|
|
23
|
-
import type { Connection } from './connection/base.ts';
|
|
24
|
-
import type { InsertWrapper } from './internal/types.ts';
|
|
57
|
+
import type { SQLConnection } from './connection.ts';
|
|
58
|
+
import type { AbstractANSI99Dialect } from './dialect.ts';
|
|
59
|
+
import { SQLModelSchemaUtil } from './schema.ts';
|
|
60
|
+
import type { TableContext } from './types.ts';
|
|
25
61
|
|
|
26
62
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
63
|
+
* Base SQL Model Service.
|
|
64
|
+
* Implements CRUD, Query, Expiry, Bulk, Indexed, and Suggest operations
|
|
65
|
+
* by delegating to connection and dialect components.
|
|
30
66
|
*/
|
|
31
67
|
@Injectable()
|
|
32
|
-
export class
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
)
|
|
74
|
-
|
|
75
|
-
const allIds = new Set(all.map(type => type.id));
|
|
76
|
-
|
|
77
|
-
for (const [id, idx] of toCheck.entries()) {
|
|
78
|
-
if (!allIds.has(id)) { // If not found
|
|
79
|
-
addedIds.set(idx, id);
|
|
68
|
+
export abstract class BaseSQLModelService<C = unknown>
|
|
69
|
+
implements
|
|
70
|
+
ModelCrudSupport,
|
|
71
|
+
ModelStorageSupport,
|
|
72
|
+
ModelBulkSupport,
|
|
73
|
+
ModelExpirySupport,
|
|
74
|
+
ModelIndexedSupport,
|
|
75
|
+
ModelQuerySupport,
|
|
76
|
+
ModelQueryCrudSupport,
|
|
77
|
+
ModelQueryFacetSupport,
|
|
78
|
+
ModelQuerySuggestSupport
|
|
79
|
+
{
|
|
80
|
+
abstract readonly client: C;
|
|
81
|
+
abstract connection: SQLConnection;
|
|
82
|
+
|
|
83
|
+
idSource = ModelCrudUtil.uuidSource();
|
|
84
|
+
|
|
85
|
+
get dialect(): AbstractANSI99Dialect {
|
|
86
|
+
return this.connection.dialect;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
#whereClause<T extends ModelType>(
|
|
90
|
+
modelClass: Class<T>,
|
|
91
|
+
where?: WhereClause<T>,
|
|
92
|
+
checkExpiry?: boolean
|
|
93
|
+
): { whereSQL?: string; parameters?: unknown[] } {
|
|
94
|
+
return this.dialect.compileWhere(this.connection.getContext(modelClass), ModelQueryUtil.getWhereClause(modelClass, where), checkExpiry);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async initialize(): Promise<void> {
|
|
98
|
+
await this.connection.init();
|
|
99
|
+
await this.createStorage();
|
|
100
|
+
ModelExpiryUtil.registerCull(this);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Record Deserialization Helpers
|
|
104
|
+
async loadSingle<T extends ModelType>(modelClass: Class<T>, record: Record<string, unknown>): Promise<T> {
|
|
105
|
+
const schemaContext = SQLModelSchemaUtil.getSchemaContext(modelClass);
|
|
106
|
+
const resolvedRecord = { ...record };
|
|
107
|
+
for (const complexFieldName of schemaContext.complexFields.keys()) {
|
|
108
|
+
const value = resolvedRecord[complexFieldName];
|
|
109
|
+
if (typeof value === 'string') {
|
|
110
|
+
resolvedRecord[complexFieldName] = JSONUtil.fromUTF8(value);
|
|
80
111
|
}
|
|
81
112
|
}
|
|
113
|
+
return ModelCrudUtil.load(modelClass, resolvedRecord);
|
|
114
|
+
}
|
|
82
115
|
|
|
83
|
-
|
|
116
|
+
async loadMany<T extends ModelType>(modelClass: Class<T>, records: unknown[]): Promise<T[]> {
|
|
117
|
+
return Promise.all(records.map(row => this.loadSingle(modelClass, castTo(row))));
|
|
84
118
|
}
|
|
85
119
|
|
|
86
|
-
|
|
87
|
-
|
|
120
|
+
async executeUpdatePartial<T extends ModelType>(
|
|
121
|
+
modelClass: Class<T>,
|
|
122
|
+
where: WhereClause<T>,
|
|
123
|
+
data: Partial<T>,
|
|
124
|
+
returning: boolean,
|
|
125
|
+
view?: string
|
|
126
|
+
): Promise<{ count: number; records: Record<string, unknown>[] }> {
|
|
127
|
+
const preparedData = await ModelCrudUtil.prePartialUpdate(modelClass, data, view);
|
|
128
|
+
|
|
129
|
+
const tableContext = this.connection.getContext(modelClass);
|
|
130
|
+
const { whereSQL, parameters = [] } = this.#whereClause(modelClass, where);
|
|
131
|
+
const { sql, values } = this.dialect.buildPartialUpdate(tableContext, preparedData, whereSQL, parameters, returning);
|
|
132
|
+
|
|
133
|
+
const result = await this.connection.execute<Record<string, unknown>>(sql, values);
|
|
134
|
+
|
|
135
|
+
if (result.count > 0 && returning && !this.dialect.returningSupport) {
|
|
136
|
+
const selectSQL = this.dialect.buildSelect(tableContext, { whereSQL });
|
|
137
|
+
const selectResult = await this.connection.execute<Record<string, unknown>>(selectSQL, parameters);
|
|
138
|
+
return { count: result.count, records: selectResult.records };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return result;
|
|
88
142
|
}
|
|
89
143
|
|
|
90
|
-
async
|
|
91
|
-
|
|
144
|
+
async executeUpdate<T extends ModelType>(
|
|
145
|
+
modelClass: Class<T>,
|
|
146
|
+
where: WhereClause<T>,
|
|
147
|
+
item: T,
|
|
148
|
+
modelSource?: ModelCrudProvider
|
|
149
|
+
): Promise<T | undefined> {
|
|
150
|
+
ModelCrudUtil.ensureNotSubType(modelClass);
|
|
151
|
+
const preppedItem = await ModelCrudUtil.preStore(modelClass, item, modelSource ?? { idSource: this.idSource });
|
|
152
|
+
const rawItem: Record<string, unknown> = castTo(preppedItem);
|
|
153
|
+
|
|
154
|
+
const tableContext = this.connection.getContext(modelClass);
|
|
155
|
+
const { whereSQL, parameters = [] } = this.#whereClause(modelClass, where);
|
|
156
|
+
const { sql, values } = this.dialect.buildUpdate(tableContext, rawItem, whereSQL, parameters);
|
|
157
|
+
|
|
158
|
+
const result = await this.connection.execute(sql, values);
|
|
159
|
+
if (result.count === 0) {
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
if (result.count > 1) {
|
|
163
|
+
throw new Error(`Multiple items found for update lookup ${modelClass.name}`);
|
|
164
|
+
}
|
|
165
|
+
return preppedItem;
|
|
166
|
+
}
|
|
92
167
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
168
|
+
async executeUpsert<T extends ModelType>(
|
|
169
|
+
modelClass: Class<T>,
|
|
170
|
+
item: OptionalId<T>,
|
|
171
|
+
conflictTarget: string[],
|
|
172
|
+
modelSource?: ModelCrudProvider
|
|
173
|
+
): Promise<T> {
|
|
174
|
+
ModelCrudUtil.ensureNotSubType(modelClass);
|
|
175
|
+
const preppedItem = await ModelCrudUtil.preStore(modelClass, item, modelSource ?? { idSource: this.idSource });
|
|
176
|
+
const rawItem: Record<string, unknown> = castTo(preppedItem);
|
|
177
|
+
const tableContext = this.connection.getContext(modelClass);
|
|
178
|
+
|
|
179
|
+
const { sql, values } = this.dialect.buildUpsert(tableContext, rawItem, conflictTarget);
|
|
180
|
+
|
|
181
|
+
const result = await this.connection.execute<Record<string, unknown>>(sql, values);
|
|
182
|
+
if (result.records.length > 0) {
|
|
183
|
+
return this.loadSingle(modelClass, result.records[0]);
|
|
184
|
+
} else {
|
|
185
|
+
return this.get(modelClass, rawItem.id as string);
|
|
98
186
|
}
|
|
99
187
|
}
|
|
100
188
|
|
|
101
|
-
|
|
102
|
-
async
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
189
|
+
// Crud Support
|
|
190
|
+
async get<T extends ModelType>(modelClass: Class<T>, id: string): Promise<T> {
|
|
191
|
+
const tableContext = this.connection.getContext(modelClass);
|
|
192
|
+
const { whereSQL, parameters } = this.#whereClause(modelClass, castTo({ id }));
|
|
193
|
+
const sql = this.dialect.buildSelect(tableContext, { whereSQL });
|
|
194
|
+
|
|
195
|
+
const result = await this.connection.execute<Record<string, unknown>>(sql, parameters);
|
|
196
|
+
|
|
197
|
+
if (result.count === 0) {
|
|
198
|
+
throw new NotFoundError(modelClass, id);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return this.loadSingle(modelClass, result.records[0]);
|
|
108
202
|
}
|
|
109
203
|
|
|
110
|
-
|
|
111
|
-
|
|
204
|
+
async create<T extends ModelType>(modelClass: Class<T>, item: OptionalId<T>, modelSource?: ModelCrudProvider): Promise<T> {
|
|
205
|
+
const preppedItem = await ModelCrudUtil.preStore(modelClass, item, modelSource ?? { idSource: this.idSource });
|
|
206
|
+
const rawItem: Record<string, unknown> = castTo(preppedItem);
|
|
207
|
+
const tableContext = this.connection.getContext(modelClass);
|
|
208
|
+
|
|
209
|
+
const { sql, values } = this.dialect.buildInsert(tableContext, rawItem);
|
|
210
|
+
|
|
211
|
+
try {
|
|
212
|
+
await this.connection.execute(sql, values);
|
|
213
|
+
} catch (error) {
|
|
214
|
+
if (error instanceof UniqueError && (error.details?.type === 'query' || error.details?.type === 'index')) {
|
|
215
|
+
throw new UniqueError(modelClass, (error.details.constraint as string) ?? 'unknown', error.details);
|
|
216
|
+
}
|
|
217
|
+
throw error;
|
|
218
|
+
}
|
|
219
|
+
return preppedItem;
|
|
112
220
|
}
|
|
113
221
|
|
|
114
|
-
async
|
|
115
|
-
|
|
222
|
+
async update<T extends ModelType>(modelClass: Class<T>, item: T, modelSource?: ModelCrudProvider): Promise<T> {
|
|
223
|
+
const preppedItem = await this.executeUpdate(modelClass, castTo({ id: item.id }), item, modelSource);
|
|
224
|
+
if (!preppedItem) {
|
|
225
|
+
throw new NotFoundError(modelClass, item.id);
|
|
226
|
+
}
|
|
227
|
+
return preppedItem;
|
|
116
228
|
}
|
|
117
229
|
|
|
118
|
-
async
|
|
119
|
-
|
|
230
|
+
async upsert<T extends ModelType>(modelClass: Class<T>, item: OptionalId<T>, modelSource?: ModelCrudProvider): Promise<T> {
|
|
231
|
+
return this.executeUpsert(modelClass, item, [this.dialect.escapeIdentifier('id')], modelSource);
|
|
120
232
|
}
|
|
121
233
|
|
|
122
|
-
async
|
|
123
|
-
|
|
234
|
+
async updatePartial<T extends ModelType>(modelClass: Class<T>, item: Partial<T> & { id: string }, view?: string): Promise<T> {
|
|
235
|
+
ModelCrudUtil.ensureNotSubType(modelClass);
|
|
236
|
+
|
|
237
|
+
const result = await this.executeUpdatePartial(modelClass, castTo({ id: item.id }), item, true, view);
|
|
238
|
+
|
|
239
|
+
if (result.count === 0) {
|
|
240
|
+
throw new NotFoundError(modelClass, item.id);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return this.loadSingle(modelClass, result.records[0]);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async delete<T extends ModelType>(modelClass: Class<T>, id: string): Promise<void> {
|
|
247
|
+
ModelCrudUtil.ensureNotSubType(modelClass);
|
|
248
|
+
const tableContext = this.connection.getContext(modelClass);
|
|
249
|
+
const { whereSQL, parameters } = this.#whereClause(modelClass, castTo({ id }), false);
|
|
250
|
+
const sql = this.dialect.buildDelete(tableContext, whereSQL);
|
|
251
|
+
|
|
252
|
+
const result = await this.connection.execute(sql, parameters);
|
|
253
|
+
if (result.count === 0) {
|
|
254
|
+
throw new NotFoundError(modelClass, id);
|
|
255
|
+
}
|
|
124
256
|
}
|
|
125
257
|
|
|
126
|
-
async
|
|
127
|
-
|
|
258
|
+
async *list<T extends ModelType>(modelClass: Class<T>, options?: ModelListOptions): AsyncIterable<T[]> {
|
|
259
|
+
yield* this.listWithOffset(modelClass, options);
|
|
128
260
|
}
|
|
129
261
|
|
|
130
|
-
async
|
|
131
|
-
|
|
262
|
+
async *listWithOffset<T extends ModelType>(modelClass: Class<T>, options?: ModelListOptions & { offset?: number }): AsyncIterable<T[]> {
|
|
263
|
+
const tableContext = this.connection.getContext(modelClass);
|
|
264
|
+
const { whereSQL, parameters } = this.#whereClause(modelClass, undefined);
|
|
132
265
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
266
|
+
const limit = options?.limit ?? Number.MAX_SAFE_INTEGER;
|
|
267
|
+
const batchSize = Math.min(options?.batchSizeHint ?? 100, limit);
|
|
268
|
+
|
|
269
|
+
let offset = options?.offset ?? 0;
|
|
270
|
+
let produced = 0;
|
|
271
|
+
|
|
272
|
+
while (!options?.abort?.aborted && produced < limit) {
|
|
273
|
+
const batchLimit = Math.min(batchSize, limit - produced);
|
|
274
|
+
const sql = this.dialect.buildSelect(tableContext, { whereSQL, limit: batchLimit, offset });
|
|
275
|
+
|
|
276
|
+
const result = await this.connection.execute(sql, parameters);
|
|
277
|
+
if (result.count === 0) {
|
|
278
|
+
break;
|
|
145
279
|
}
|
|
280
|
+
|
|
281
|
+
const items = await this.loadMany(modelClass, result.records);
|
|
282
|
+
yield items;
|
|
283
|
+
produced += items.length;
|
|
284
|
+
offset += items.length;
|
|
146
285
|
}
|
|
147
|
-
return prepped;
|
|
148
286
|
}
|
|
149
287
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
await this
|
|
153
|
-
return await this.create(cls, item);
|
|
288
|
+
async dropIndex<T extends ModelType>(tableContext: TableContext<T>, indexName: string): Promise<void> {
|
|
289
|
+
const sql = this.dialect.getDropIndexSQL(tableContext, indexName);
|
|
290
|
+
await this.connection.execute(sql);
|
|
154
291
|
}
|
|
155
292
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
293
|
+
async dropTable<T extends ModelType>(tableContext: TableContext<T>): Promise<void> {
|
|
294
|
+
const sql = this.dialect.getDropTableSQL(tableContext);
|
|
295
|
+
await this.connection.execute(sql);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async truncateTable<T extends ModelType>(tableContext: TableContext<T>): Promise<void> {
|
|
299
|
+
const sql = this.dialect.getTruncateTableSQL(tableContext);
|
|
300
|
+
await this.connection.execute(sql);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async upsertTable<T extends ModelType>(tableContext: TableContext<T>): Promise<void> {
|
|
304
|
+
// Storage & Migration Operations
|
|
305
|
+
const query = this.dialect.getTableExistsQuery(tableContext);
|
|
306
|
+
const result = await this.connection.execute(query.sql, query.parameters);
|
|
307
|
+
const tableExists = this.dialect.parseTableExistsResult(result.records);
|
|
308
|
+
|
|
309
|
+
if (!tableExists) {
|
|
310
|
+
const createTableSQL = this.dialect.getCreateTableSQL(tableContext);
|
|
311
|
+
await this.connection.execute(createTableSQL);
|
|
312
|
+
|
|
313
|
+
for (const createIndexSQL of this.dialect.getCreateTableIndexSQLs(tableContext)) {
|
|
314
|
+
await this.connection.execute(createIndexSQL);
|
|
161
315
|
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
|
|
316
|
+
} else {
|
|
317
|
+
const query = this.dialect.getExistingColumnsQuery(tableContext);
|
|
318
|
+
const result = await this.connection.execute(query.sql, query.parameters);
|
|
319
|
+
const existingColumns = this.dialect.parseExistingColumns(result.records);
|
|
320
|
+
|
|
321
|
+
const requestedFieldsMap = new Map<string, string>();
|
|
322
|
+
for (const field of tableContext.simpleFields.values()) {
|
|
323
|
+
requestedFieldsMap.set(field.name, this.dialect.getColumnType(field));
|
|
324
|
+
}
|
|
325
|
+
for (const field of tableContext.complexFields.values()) {
|
|
326
|
+
requestedFieldsMap.set(field.name, this.dialect.getComplexColumnType(field));
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
for (const [columnName, columnType] of requestedFieldsMap.entries()) {
|
|
330
|
+
if (columnName === 'id') {
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
if (!existingColumns.has(columnName)) {
|
|
334
|
+
const addColumnSQL = this.dialect.getAddColumnSQL(tableContext, columnName, columnType);
|
|
335
|
+
await this.connection.execute(addColumnSQL);
|
|
336
|
+
} else if (this.dialect.getAlterColumnTypeSQL) {
|
|
337
|
+
const existingType = existingColumns.get(columnName)!;
|
|
338
|
+
const alterColumnSQL = this.dialect.getAlterColumnTypeSQL(tableContext, columnName, columnType, existingType);
|
|
339
|
+
if (alterColumnSQL) {
|
|
340
|
+
await this.connection.execute(alterColumnSQL);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const indexQuery = this.dialect.getExistingIndexesQuery(tableContext);
|
|
346
|
+
const indexResult = await this.connection.execute(indexQuery.sql, indexQuery.parameters);
|
|
347
|
+
const existingIndexes = this.dialect.parseExistingIndexes(indexResult.records);
|
|
348
|
+
|
|
349
|
+
const modelIndexes = ModelRegistryIndex.getIndices(tableContext.cls) || [];
|
|
350
|
+
|
|
351
|
+
const definedIndexes = new Map<string, IndexConfig>();
|
|
352
|
+
for (const indexConfig of modelIndexes) {
|
|
353
|
+
const indexName = ['idx', tableContext.tableName, indexConfig.name.toLowerCase().replaceAll('-', '_')].join('_');
|
|
354
|
+
definedIndexes.set(indexName, indexConfig);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
for (const [indexName, indexDefinition] of existingIndexes.entries()) {
|
|
358
|
+
if (!definedIndexes.has(indexName)) {
|
|
359
|
+
await this.dropIndex(tableContext, indexName);
|
|
360
|
+
} else {
|
|
361
|
+
const indexConfig = definedIndexes.get(indexName)!;
|
|
362
|
+
const expectedSQL = this.dialect.getCreateIndexSQL(tableContext, indexConfig);
|
|
363
|
+
|
|
364
|
+
if (indexDefinition) {
|
|
365
|
+
const normalizedExisting = this.dialect.normalizeIndexDefinition(indexDefinition);
|
|
366
|
+
const normalizedExpected = this.dialect.normalizeIndexDefinition(expectedSQL);
|
|
367
|
+
|
|
368
|
+
if (normalizedExisting !== normalizedExpected) {
|
|
369
|
+
await this.dropIndex(tableContext, indexName);
|
|
370
|
+
await this.connection.execute(expectedSQL);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
for (const [indexName, indexConfig] of definedIndexes.entries()) {
|
|
377
|
+
if (!existingIndexes.has(indexName)) {
|
|
378
|
+
const createIndexSQL = this.dialect.getCreateIndexSQL(tableContext, indexConfig);
|
|
379
|
+
await this.connection.execute(createIndexSQL);
|
|
380
|
+
}
|
|
165
381
|
}
|
|
166
382
|
}
|
|
167
|
-
return await this.create(cls, item);
|
|
168
383
|
}
|
|
169
384
|
|
|
170
|
-
|
|
171
|
-
async
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
385
|
+
// Storage Support
|
|
386
|
+
async createStorage(): Promise<void> {
|
|
387
|
+
for (const modelClass of ModelRegistryIndex.getClasses()) {
|
|
388
|
+
warnIfIndexedUniqueIndex(this, modelClass, ModelRegistryIndex.getIndices(modelClass));
|
|
389
|
+
warnIfNonIndexedIndex(this, modelClass, ModelRegistryIndex.getIndices(modelClass));
|
|
390
|
+
const tableContext = this.connection.getContext(modelClass);
|
|
391
|
+
await this.upsertTable(tableContext);
|
|
392
|
+
}
|
|
175
393
|
}
|
|
176
394
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
return await ModelCrudUtil.load(cls, result[0]);
|
|
395
|
+
async deleteStorage(): Promise<void> {
|
|
396
|
+
for (const modelClass of ModelRegistryIndex.getClasses()) {
|
|
397
|
+
const tableContext = this.connection.getContext(modelClass);
|
|
398
|
+
await this.dropTable(tableContext);
|
|
182
399
|
}
|
|
183
|
-
throw new NotFoundError(cls, id);
|
|
184
400
|
}
|
|
185
401
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
yield await ModelCrudUtil.load(cls, item);
|
|
190
|
-
}
|
|
402
|
+
async deleteModel(modelClass: Class): Promise<void> {
|
|
403
|
+
const tableContext = this.connection.getContext(modelClass);
|
|
404
|
+
await this.dropTable(tableContext);
|
|
191
405
|
}
|
|
192
406
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
await this
|
|
407
|
+
async upsertModel(modelClass: Class): Promise<void> {
|
|
408
|
+
const tableContext = this.connection.getContext(modelClass);
|
|
409
|
+
await this.upsertTable(tableContext);
|
|
196
410
|
}
|
|
197
411
|
|
|
198
|
-
|
|
199
|
-
|
|
412
|
+
async truncateModel<T extends ModelType>(modelClass: Class<T>): Promise<void> {
|
|
413
|
+
const tableContext = this.connection.getContext(modelClass);
|
|
414
|
+
await this.truncateTable(tableContext);
|
|
415
|
+
}
|
|
200
416
|
|
|
201
|
-
|
|
417
|
+
// Bulk Support
|
|
418
|
+
async processBulk<T extends ModelType>(modelClass: Class<T>, operations: BulkOperation<T>[]): Promise<BulkResponse> {
|
|
419
|
+
const { insertedIds, upsertedIds, operations: preppedOperations } = await ModelBulkUtil.preStore(modelClass, operations, this);
|
|
420
|
+
const addedIdentifiers = new Map([...insertedIds.entries(), ...upsertedIds.entries()]);
|
|
421
|
+
|
|
422
|
+
const counts = {
|
|
423
|
+
update: 0,
|
|
424
|
+
insert: 0,
|
|
425
|
+
upsert: 0,
|
|
426
|
+
delete: 0,
|
|
427
|
+
error: 0
|
|
428
|
+
};
|
|
429
|
+
const errors: unknown[] = [];
|
|
430
|
+
|
|
431
|
+
// Process the inbound bulk request into groups: inserts, deletes, updates, and upserts
|
|
432
|
+
const inserts: OptionalId<T>[] = [];
|
|
433
|
+
const deletes: string[] = [];
|
|
434
|
+
const updates: T[] = [];
|
|
435
|
+
const upserts: { upsert?: OptionalId<T> }[] = [];
|
|
436
|
+
|
|
437
|
+
for (const operation of preppedOperations) {
|
|
438
|
+
if ('insert' in operation && operation.insert) {
|
|
439
|
+
inserts.push(operation.insert);
|
|
440
|
+
} else if ('delete' in operation && operation.delete) {
|
|
441
|
+
deletes.push(operation.delete.id);
|
|
442
|
+
} else if ('update' in operation && operation.update) {
|
|
443
|
+
updates.push(operation.update);
|
|
444
|
+
} else if ('upsert' in operation && operation.upsert) {
|
|
445
|
+
upserts.push(operation);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
202
448
|
|
|
203
|
-
|
|
449
|
+
type SqlCommand = {
|
|
450
|
+
type: 'insert' | 'delete' | 'update' | 'upsert';
|
|
451
|
+
sql: string;
|
|
452
|
+
values: unknown[];
|
|
453
|
+
count: number;
|
|
454
|
+
identifier?: string;
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
const commands: SqlCommand[] = [];
|
|
458
|
+
const batchSize = 100;
|
|
459
|
+
const tableContext = this.connection.getContext(modelClass);
|
|
460
|
+
|
|
461
|
+
// Convert inserts into SQL statements with a fixed batch size
|
|
462
|
+
for (let index = 0; index < inserts.length; index += batchSize) {
|
|
463
|
+
const subBatch = inserts.slice(index, index + batchSize);
|
|
464
|
+
const rawItems: Record<string, unknown>[] = castTo(subBatch);
|
|
465
|
+
const { sql, values } = this.dialect.buildInsertAll(tableContext, rawItems);
|
|
466
|
+
commands.push({ type: 'insert', sql, values, count: subBatch.length });
|
|
467
|
+
}
|
|
204
468
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
469
|
+
// Convert deletes into SQL statements with a fixed batch size
|
|
470
|
+
for (let index = 0; index < deletes.length; index += batchSize) {
|
|
471
|
+
const subBatch = deletes.slice(index, index + batchSize);
|
|
472
|
+
const { whereSQL, parameters = [] } = this.#whereClause(modelClass, castTo({ id: { $in: subBatch } }), false);
|
|
473
|
+
const sql = this.dialect.buildDelete(tableContext, whereSQL);
|
|
474
|
+
commands.push({ type: 'delete', sql, values: parameters, count: subBatch.length });
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Convert updates into SQL statements with a fixed batch size
|
|
478
|
+
for (let index = 0; index < updates.length; index += batchSize) {
|
|
479
|
+
const subBatch = updates.slice(index, index + batchSize);
|
|
480
|
+
const rawItems: Record<string, unknown>[] = castTo(subBatch);
|
|
481
|
+
const { sql, values } = this.dialect.buildUpdateAll(tableContext, rawItems);
|
|
482
|
+
commands.push({ type: 'update', sql, values, count: subBatch.length });
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Generate upsert statements from other operations
|
|
486
|
+
for (const operation of upserts) {
|
|
487
|
+
const rawItem: Record<string, unknown> = castTo(operation.upsert);
|
|
488
|
+
const { sql, values } = this.dialect.buildUpsert(tableContext, rawItem, [this.dialect.escapeIdentifier('id')]);
|
|
489
|
+
commands.push({ type: 'upsert', sql, values, count: 1 });
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Run the final list of SQL commands through a workpool
|
|
493
|
+
await WorkPool.run(
|
|
494
|
+
async command => {
|
|
495
|
+
try {
|
|
496
|
+
const result = await this.connection.execute(command.sql, command.values);
|
|
497
|
+
if (command.type === 'update' && result.count === 0) {
|
|
498
|
+
counts.error += 1;
|
|
499
|
+
errors.push(new NotFoundError(modelClass, command.identifier!));
|
|
500
|
+
} else if (command.type === 'delete' && result.count < command.count) {
|
|
501
|
+
counts.delete += result.count;
|
|
502
|
+
const missingCount = command.count - result.count;
|
|
503
|
+
counts.error += missingCount;
|
|
504
|
+
errors.push(new NotFoundError(modelClass, `Bulk delete missed ${missingCount} record(s)`));
|
|
505
|
+
} else {
|
|
506
|
+
counts[command.type] += command.count;
|
|
507
|
+
}
|
|
508
|
+
} catch (error) {
|
|
509
|
+
counts.error += command.count;
|
|
510
|
+
errors.push(error);
|
|
511
|
+
}
|
|
512
|
+
},
|
|
513
|
+
commands,
|
|
514
|
+
{ max: 8 }
|
|
208
515
|
);
|
|
209
516
|
|
|
210
|
-
|
|
211
|
-
|
|
517
|
+
return {
|
|
518
|
+
errors,
|
|
519
|
+
insertedIds: addedIdentifiers,
|
|
520
|
+
counts
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Expiry Support
|
|
525
|
+
async deleteExpired<T extends ModelType>(modelClass: Class<T>): Promise<number> {
|
|
526
|
+
return ModelQueryCrudUtil.deleteExpired(this, modelClass);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Indexed Support
|
|
530
|
+
validateIndexResult<T extends ModelType>(
|
|
531
|
+
modelClass: Class<T>,
|
|
532
|
+
result: { count: number },
|
|
533
|
+
indexConfig: SingleItemIndex<T>,
|
|
534
|
+
computed: ModelIndexedComputedIndex<T>
|
|
535
|
+
): void {
|
|
536
|
+
if (result.count === 0) {
|
|
537
|
+
throw new NotFoundError(`${modelClass.name} Index=${indexConfig}`, computed.getKey());
|
|
538
|
+
}
|
|
539
|
+
if (result.count > 1) {
|
|
540
|
+
throw new Error(`Multiple items found for index lookup ${modelClass.name} Index=${indexConfig}`);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
212
543
|
|
|
213
|
-
|
|
214
|
-
|
|
544
|
+
async getByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
|
|
545
|
+
modelClass: Class<T>,
|
|
546
|
+
indexConfig: SingleItemIndex<T, K, S>,
|
|
547
|
+
body: FullKeyedIndexBody<T, K, S>
|
|
548
|
+
): Promise<T> {
|
|
549
|
+
ModelCrudUtil.ensureNotSubType(modelClass);
|
|
550
|
+
const computed = ModelIndexedComputedIndex.get(indexConfig, body).validate({ sort: true });
|
|
551
|
+
const where: WhereClause<T> = castTo(computed.project({ sort: true, includeId: true }));
|
|
215
552
|
|
|
216
|
-
const
|
|
217
|
-
|
|
553
|
+
const tableContext = this.connection.getContext(modelClass);
|
|
554
|
+
const { whereSQL, parameters } = this.#whereClause(modelClass, where);
|
|
555
|
+
const sql = this.dialect.buildSelect(tableContext, { whereSQL });
|
|
218
556
|
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
getStatements('upsert'),
|
|
222
|
-
getStatements('update')
|
|
223
|
-
]);
|
|
557
|
+
const result = await this.connection.execute<Record<string, unknown>>(sql, parameters);
|
|
558
|
+
this.validateIndexResult(modelClass, result, indexConfig, computed);
|
|
224
559
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
560
|
+
return this.loadSingle(modelClass, result.records[0]);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
async deleteByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
|
|
564
|
+
modelClass: Class<T>,
|
|
565
|
+
indexConfig: SingleItemIndex<T, K, S>,
|
|
566
|
+
body: FullKeyedIndexBody<T, K, S>
|
|
567
|
+
): Promise<void> {
|
|
568
|
+
ModelCrudUtil.ensureNotSubType(modelClass);
|
|
569
|
+
const computed = ModelIndexedComputedIndex.get(indexConfig, body).validate({ sort: true });
|
|
570
|
+
const where: WhereClause<T> = castTo(computed.project({ sort: true, includeId: true }));
|
|
571
|
+
|
|
572
|
+
const tableContext = this.connection.getContext(modelClass);
|
|
573
|
+
const { whereSQL, parameters } = this.#whereClause(modelClass, where);
|
|
574
|
+
const sql = this.dialect.buildDelete(tableContext, whereSQL);
|
|
575
|
+
|
|
576
|
+
const result = await this.connection.execute(sql, parameters);
|
|
577
|
+
this.validateIndexResult(modelClass, result, indexConfig, computed);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
async upsertByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
|
|
581
|
+
modelClass: Class<T>,
|
|
582
|
+
indexConfig: SingleItemIndex<T, K, S>,
|
|
583
|
+
body: OptionalId<T>
|
|
584
|
+
): Promise<T> {
|
|
585
|
+
return ModelIndexedUtil.naiveUpsert(this, modelClass, indexConfig, body);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
async updateByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
|
|
589
|
+
modelClass: Class<T>,
|
|
590
|
+
indexConfig: SingleItemIndex<T, K, S>,
|
|
591
|
+
body: T
|
|
592
|
+
): Promise<T> {
|
|
593
|
+
const computed = ModelIndexedComputedIndex.get(indexConfig, castTo(body)).validate({ sort: true });
|
|
594
|
+
const where: WhereClause<T> = castTo(computed.project({ sort: true, includeId: true }));
|
|
595
|
+
|
|
596
|
+
const preppedItem = await this.executeUpdate(modelClass, where, body, this);
|
|
597
|
+
if (!preppedItem) {
|
|
598
|
+
throw new NotFoundError(`${modelClass.name} Index=${indexConfig}`, computed.getKey());
|
|
599
|
+
}
|
|
600
|
+
return preppedItem;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
async updatePartialByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
|
|
604
|
+
modelClass: Class<T>,
|
|
605
|
+
indexConfig: SingleItemIndex<T, K, S>,
|
|
606
|
+
body: FullKeyedIndexWithPartialBody<T, K, S>
|
|
607
|
+
): Promise<T> {
|
|
608
|
+
ModelCrudUtil.ensureNotSubType(modelClass);
|
|
609
|
+
|
|
610
|
+
const computed = ModelIndexedComputedIndex.get(indexConfig, castTo(body)).validate({ sort: true });
|
|
611
|
+
const where: WhereClause<T> = castTo(computed.project({ sort: true, includeId: true }));
|
|
612
|
+
|
|
613
|
+
const result = await this.executeUpdatePartial(modelClass, where, castTo(body), true);
|
|
614
|
+
this.validateIndexResult(modelClass, result, indexConfig, computed);
|
|
615
|
+
|
|
616
|
+
return this.loadSingle(modelClass, result.records[0]);
|
|
228
617
|
}
|
|
229
618
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
619
|
+
async *listByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
|
|
620
|
+
modelClass: Class<T>,
|
|
621
|
+
indexConfig: SortedIndex<T, K, S>,
|
|
622
|
+
body: KeyedIndexBody<T, K>,
|
|
623
|
+
options?: ModelListOptions & { offset?: number }
|
|
624
|
+
): AsyncIterable<T[]> {
|
|
625
|
+
const computed = ModelIndexedComputedIndex.get(indexConfig, body).validate();
|
|
626
|
+
const where: WhereClause<T> = castTo(computed.project());
|
|
627
|
+
|
|
628
|
+
const tableContext = this.connection.getContext(modelClass);
|
|
629
|
+
const sortSQL = this.dialect.buildIndexSort(tableContext, indexConfig);
|
|
630
|
+
|
|
631
|
+
const limit = options?.limit ?? Number.MAX_SAFE_INTEGER;
|
|
632
|
+
const batchSize = Math.min(options?.batchSizeHint ?? 100, limit);
|
|
633
|
+
|
|
634
|
+
let offset = options?.offset ?? 0;
|
|
635
|
+
let produced = 0;
|
|
636
|
+
|
|
637
|
+
const { whereSQL, parameters } = this.#whereClause(modelClass, where);
|
|
638
|
+
|
|
639
|
+
while (!options?.abort?.aborted && produced < limit) {
|
|
640
|
+
const batchLimit = Math.min(batchSize, limit - produced);
|
|
641
|
+
const sql = this.dialect.buildSelect(tableContext, { whereSQL, sortSQL, limit: batchLimit, offset });
|
|
642
|
+
|
|
643
|
+
const result = await this.connection.execute(sql, parameters);
|
|
644
|
+
if (result.count === 0) {
|
|
645
|
+
break;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
const items = await this.loadMany(modelClass, result.records);
|
|
649
|
+
yield items;
|
|
650
|
+
produced += items.length;
|
|
651
|
+
offset += items.length;
|
|
652
|
+
}
|
|
234
653
|
}
|
|
235
654
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
655
|
+
async pageByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
|
|
656
|
+
modelClass: Class<T>,
|
|
657
|
+
indexConfig: SortedIndex<T, K, S>,
|
|
658
|
+
body: KeyedIndexBody<T, K>,
|
|
659
|
+
options?: ModelPageOptions
|
|
660
|
+
): Promise<ModelPageResult<T>> {
|
|
661
|
+
const listOptions = {
|
|
662
|
+
limit: options?.limit,
|
|
663
|
+
offset: options?.offset ? Number(options.offset) : 0
|
|
664
|
+
};
|
|
665
|
+
|
|
666
|
+
const items: T[] = [];
|
|
667
|
+
let nextOffset = listOptions.offset ?? 0;
|
|
668
|
+
|
|
669
|
+
for await (const batch of this.listByIndex(modelClass, indexConfig, body, listOptions)) {
|
|
670
|
+
items.push(...batch);
|
|
671
|
+
nextOffset += batch.length;
|
|
242
672
|
}
|
|
243
673
|
|
|
244
|
-
|
|
245
|
-
|
|
674
|
+
return {
|
|
675
|
+
items,
|
|
676
|
+
nextOffset: items.length === options?.limit ? String(nextOffset) : undefined
|
|
677
|
+
};
|
|
246
678
|
}
|
|
247
679
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
680
|
+
async suggestByIndex<
|
|
681
|
+
T extends ModelType,
|
|
682
|
+
S extends SortedIndexSelection<T>,
|
|
683
|
+
K extends KeyedIndexSelection<T>,
|
|
684
|
+
B extends SortedIndexSelectionType<T, S> & string
|
|
685
|
+
>(
|
|
686
|
+
modelClass: Class<T>,
|
|
687
|
+
indexConfig: SortedIndex<T, K, S>,
|
|
688
|
+
body: KeyedIndexBody<T, K>,
|
|
689
|
+
prefix: B,
|
|
690
|
+
options?: ModelIndexedSearchOptions
|
|
691
|
+
): Promise<T[]> {
|
|
692
|
+
const computed = ModelIndexedComputedIndex.get(indexConfig, body).validate();
|
|
693
|
+
const where: WhereClause<T> = castTo(computed.project());
|
|
694
|
+
|
|
695
|
+
const tableContext = this.connection.getContext(modelClass);
|
|
696
|
+
const { whereSQL, parameters = [] } = this.#whereClause(modelClass, where);
|
|
697
|
+
|
|
698
|
+
const prefixFieldPath = indexConfig.sortTemplate[0].path;
|
|
699
|
+
const { sqlPath } = this.dialect.resolvePath(tableContext, prefixFieldPath, 'read');
|
|
700
|
+
|
|
701
|
+
const placeholder = this.dialect.getPlaceholder(parameters.length + 1);
|
|
702
|
+
parameters.push(`${prefix}%`);
|
|
703
|
+
|
|
704
|
+
const likeOp = this.dialect.suggestLikeOperator ?? 'LIKE';
|
|
705
|
+
const conditions = [`${sqlPath} ${likeOp} ${placeholder}`];
|
|
706
|
+
if (whereSQL) {
|
|
707
|
+
conditions.push(whereSQL);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
const sql = this.dialect.buildSelect(tableContext, { whereSQL: conditions.join(' AND '), limit: options?.limit ?? 10 });
|
|
711
|
+
const result = await this.connection.execute(sql, parameters);
|
|
712
|
+
|
|
713
|
+
return this.loadMany(modelClass, result.records);
|
|
252
714
|
}
|
|
253
715
|
|
|
254
|
-
|
|
255
|
-
async
|
|
256
|
-
await QueryVerifier.verify(
|
|
257
|
-
|
|
716
|
+
// Query Support
|
|
717
|
+
async query<T extends ModelType>(modelClass: Class<T>, query: PageableModelQuery<T>): Promise<T[]> {
|
|
718
|
+
await QueryVerifier.verify(modelClass, query);
|
|
719
|
+
const tableContext = this.connection.getContext(modelClass);
|
|
720
|
+
const { whereSQL, parameters = [] } = this.#whereClause(modelClass, query.where);
|
|
721
|
+
const sortSQL = this.dialect.compileSort(tableContext, query.sort);
|
|
722
|
+
|
|
723
|
+
const sql = this.dialect.buildSelect(tableContext, {
|
|
724
|
+
whereSQL,
|
|
725
|
+
sortSQL,
|
|
726
|
+
limit: query.limit,
|
|
727
|
+
offset: query.offset
|
|
728
|
+
});
|
|
729
|
+
const result = await this.connection.execute(sql, parameters);
|
|
730
|
+
|
|
731
|
+
return this.loadMany(modelClass, result.records);
|
|
258
732
|
}
|
|
259
733
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
const where = ModelQueryUtil.getWhereClause(cls, query.where);
|
|
265
|
-
where.id = item.id;
|
|
266
|
-
await this.#deleteRaw(cls, item.id, where, true);
|
|
267
|
-
return await this.create(cls, item);
|
|
734
|
+
async queryOne<T extends ModelType>(modelClass: Class<T>, query: ModelQuery<T>, failOnMany = true): Promise<T> {
|
|
735
|
+
const limit = failOnMany ? 2 : 1;
|
|
736
|
+
const items = await this.query<T>(modelClass, { ...query, limit });
|
|
737
|
+
return ModelQueryUtil.verifyGetSingleCounts<T>(modelClass, failOnMany, items, query.where);
|
|
268
738
|
}
|
|
269
739
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
740
|
+
async queryCount<T extends ModelType>(modelClass: Class<T>, query: ModelQuery<T>): Promise<number> {
|
|
741
|
+
await QueryVerifier.verify(modelClass, query);
|
|
742
|
+
const tableContext = this.connection.getContext(modelClass);
|
|
743
|
+
const { whereSQL, parameters = [] } = this.#whereClause(modelClass, query.where);
|
|
744
|
+
const sql = this.dialect.buildCount(tableContext, whereSQL);
|
|
745
|
+
|
|
746
|
+
const result = await this.connection.execute<{ total: string | number }>(sql, parameters);
|
|
747
|
+
return Number(result.records[0]?.total ?? 0);
|
|
277
748
|
}
|
|
278
749
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
750
|
+
// Query Crud Support
|
|
751
|
+
async updateByQuery<T extends ModelType>(
|
|
752
|
+
modelClass: Class<T>,
|
|
753
|
+
item: T,
|
|
754
|
+
query: ModelQuery<T>,
|
|
755
|
+
modelSource?: ModelCrudProvider
|
|
756
|
+
): Promise<T> {
|
|
757
|
+
await QueryVerifier.verify(modelClass, query);
|
|
758
|
+
ModelCrudUtil.ensureNotSubType(modelClass);
|
|
759
|
+
const preppedItem = await ModelCrudUtil.preStore(modelClass, item, modelSource ?? { idSource: this.idSource });
|
|
760
|
+
const rawItem: Record<string, unknown> = castTo(preppedItem);
|
|
761
|
+
|
|
762
|
+
const tableContext = this.connection.getContext(modelClass);
|
|
763
|
+
const combinedWhere: WhereClause<T> = castTo({
|
|
764
|
+
$and: [{ id: preppedItem.id }, ...(query.where ? [query.where] : [])]
|
|
765
|
+
});
|
|
766
|
+
const { whereSQL, parameters = [] } = this.#whereClause(modelClass, combinedWhere);
|
|
767
|
+
|
|
768
|
+
const { sql, values } = this.dialect.buildUpdate(tableContext, rawItem, whereSQL, parameters);
|
|
769
|
+
|
|
770
|
+
const result = await this.connection.execute(sql, values);
|
|
771
|
+
if (result.count === 0) {
|
|
772
|
+
throw new NotFoundError(modelClass, `Query: ${JSONUtil.toUTF8(query.where)}`);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
return preppedItem;
|
|
285
776
|
}
|
|
286
777
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
await
|
|
290
|
-
|
|
291
|
-
const results = await this.query<T>(cls, resolvedQuery);
|
|
292
|
-
return ModelQuerySuggestUtil.combineSuggestResults(cls, field, prefix, results, (a, b) => b, query?.limit);
|
|
778
|
+
async updatePartialByQuery<T extends ModelType>(modelClass: Class<T>, query: ModelQuery<T>, data: Partial<T>): Promise<number> {
|
|
779
|
+
await QueryVerifier.verify(modelClass, query);
|
|
780
|
+
const result = await this.executeUpdatePartial(modelClass, query.where!, data, false);
|
|
781
|
+
return result.count;
|
|
293
782
|
}
|
|
294
783
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
const
|
|
299
|
-
const results = await this.query(cls, resolvedQuery);
|
|
784
|
+
async deleteByQuery<T extends ModelType>(modelClass: Class<T>, query: ModelQuery<T>): Promise<number> {
|
|
785
|
+
await QueryVerifier.verify(modelClass, query);
|
|
786
|
+
const tableContext = this.connection.getContext(modelClass);
|
|
787
|
+
const { whereSQL, parameters = [] } = this.#whereClause(modelClass, query.where, false);
|
|
300
788
|
|
|
301
|
-
const
|
|
302
|
-
|
|
789
|
+
const sql = this.dialect.buildDelete(tableContext, whereSQL);
|
|
790
|
+
|
|
791
|
+
const result = await this.connection.execute(sql, parameters);
|
|
792
|
+
return result.count;
|
|
303
793
|
}
|
|
304
794
|
|
|
305
|
-
|
|
306
|
-
async
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
this.#dialect.getWhereSQL(cls, ModelQueryUtil.getWhereClause(cls, query?.where))
|
|
317
|
-
);
|
|
318
|
-
sql.push(
|
|
319
|
-
`GROUP BY ${col}`,
|
|
320
|
-
`ORDER BY ${ttl} DESC`
|
|
321
|
-
);
|
|
795
|
+
// Suggest Support
|
|
796
|
+
async suggestValuesByQuery<T extends ModelType>(
|
|
797
|
+
modelClass: Class<T>,
|
|
798
|
+
field: ValidStringFields<T>,
|
|
799
|
+
prefix?: string,
|
|
800
|
+
query?: PageableModelQuery<T>
|
|
801
|
+
): Promise<string[]> {
|
|
802
|
+
const resolvedQuery = ModelQuerySuggestUtil.getSuggestFieldQuery<T>(modelClass, field, prefix, query);
|
|
803
|
+
const results = await this.query<T>(modelClass, resolvedQuery);
|
|
804
|
+
return ModelQuerySuggestUtil.combineSuggestResults<T, string>(modelClass, field, prefix, results, value => value, query?.limit);
|
|
805
|
+
}
|
|
322
806
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
807
|
+
async suggestByQuery<T extends ModelType>(
|
|
808
|
+
modelClass: Class<T>,
|
|
809
|
+
field: ValidStringFields<T>,
|
|
810
|
+
prefix?: string,
|
|
811
|
+
query?: PageableModelQuery<T>
|
|
812
|
+
): Promise<T[]> {
|
|
813
|
+
const resolvedQuery = ModelQuerySuggestUtil.getSuggestQuery<T>(modelClass, field, prefix, query);
|
|
814
|
+
const results = await this.query<T>(modelClass, resolvedQuery);
|
|
815
|
+
return ModelQuerySuggestUtil.combineSuggestResults<T, T>(modelClass, field, prefix, results, (_, value) => value, query?.limit);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// Facet Support
|
|
819
|
+
async facetByQuery<T extends ModelType>(
|
|
820
|
+
modelClass: Class<T>,
|
|
821
|
+
field: ValidStringFields<T>,
|
|
822
|
+
query?: ModelQuery<T>
|
|
823
|
+
): Promise<ModelQueryFacet[]> {
|
|
824
|
+
await QueryVerifier.verify(modelClass, query);
|
|
825
|
+
const tableContext = this.connection.getContext(modelClass);
|
|
826
|
+
const { whereSQL, parameters } = this.#whereClause(modelClass, query?.where);
|
|
827
|
+
const { sqlPath } = this.dialect.resolvePath(tableContext, String(field).split('.'), 'read');
|
|
828
|
+
|
|
829
|
+
const sql = this.dialect.buildFacet(tableContext, sqlPath, whereSQL);
|
|
830
|
+
|
|
831
|
+
const result = await this.connection.execute<{ key: string; count: string | number }>(sql, parameters);
|
|
832
|
+
|
|
833
|
+
return result.records.map(record => ({
|
|
834
|
+
key: record.key,
|
|
835
|
+
count: Number(record.count)
|
|
836
|
+
}));
|
|
328
837
|
}
|
|
329
|
-
}
|
|
838
|
+
}
|