@travetto/model-sql 8.0.0-alpha.3 → 8.0.0-alpha.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/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
- type BulkOperation, type BulkResponse, type ModelCrudSupport, type ModelStorageSupport, type ModelBulkSupport,
4
- NotFoundError, ModelRegistryIndex, ExistsError, type OptionalId, type ModelIdSource,
5
- ModelExpiryUtil, ModelCrudUtil, ModelStorageUtil, ModelBulkUtil,
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 ModelQuery, type ModelQueryCrudSupport, type ModelQueryFacetSupport, type ModelQuerySupport,
13
- type PageableModelQuery, type ValidStringFields, type WhereClauseRaw, QueryVerifier, type ModelQuerySuggestSupport,
14
- ModelQueryUtil, ModelQuerySuggestUtil, ModelQueryCrudUtil,
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 { SQLModelConfig } from './config.ts';
19
- import { Connected, ConnectedIterator, Transactional } from './connection/decorator.ts';
20
- import { SQLModelUtil } from './util.ts';
21
- import type { SQLDialect } from './dialect/base.ts';
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
- * Core for SQL Model Source. Should not have any direct queries,
28
- * but should offload all of that to the dialect, so it can be overridden
29
- * as needed.
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 SQLModelService implements
33
- ModelCrudSupport, ModelStorageSupport,
34
- ModelBulkSupport, ModelQuerySupport,
35
- ModelQueryCrudSupport, ModelQueryFacetSupport,
36
- ModelQuerySuggestSupport {
37
-
38
- #manager: TableManager;
39
- #context: AsyncContext;
40
- #dialect: SQLDialect;
41
- idSource: ModelIdSource;
42
-
43
- readonly config: SQLModelConfig;
44
-
45
- get client(): SQLDialect {
46
- return this.#dialect;
47
- }
48
-
49
- constructor(
50
- context: AsyncContext,
51
- config: SQLModelConfig,
52
- dialect: SQLDialect
53
- ) {
54
- this.#context = context;
55
- this.#dialect = dialect;
56
- this.config = config;
57
- }
58
-
59
- /**
60
- * Verify upserted ids for bulk operations
61
- */
62
- async #checkUpsertedIds<T extends ModelType>(
63
- cls: Class<T>,
64
- addedIds: Map<number, string>,
65
- toCheck: Map<string, number>
66
- ): Promise<Map<number, string>> {
67
- // Get all upsert ids
68
- const all = toCheck.size ?
69
- (await this.#exec<ModelType>(
70
- this.#dialect.getSelectRowsByIdsSQL(
71
- SQLModelUtil.classToStack(cls), [...toCheck.keys()], [this.#dialect.idField]
72
- )
73
- )).records : [];
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
- return addedIds;
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
- #exec<T = unknown>(sql: string): Promise<{ records: T[], count: number }> {
87
- return this.#dialect.executeSQL<T>(sql);
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 #deleteRaw<T extends ModelType>(cls: Class<T>, id: string, where?: WhereClauseRaw<T>, checkExpiry = true): Promise<void> {
91
- castTo<WhereClauseRaw<ModelType>>(where ??= {}).id = id;
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
- const count = await this.#dialect.deleteAndGetCount<ModelType>(cls, {
94
- where: ModelQueryUtil.getWhereClause(cls, where, checkExpiry)
95
- });
96
- if (count === 0) {
97
- throw new NotFoundError(cls, id);
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
- @PostConstruct()
102
- async initializeClient(): Promise<void> {
103
- await this.#dialect.connection.init?.();
104
- this.idSource = ModelCrudUtil.uuidSource(this.#dialect.ID_LENGTH);
105
- this.#manager = new TableManager(this.#context, this.#dialect);
106
- await ModelStorageUtil.storageInitialization(this);
107
- ModelExpiryUtil.registerCull(this);
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
- get connection(): Connection {
111
- return this.#dialect.connection;
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 exportModel<T extends ModelType>(cls: Class<T>): Promise<string> {
115
- return (await this.#manager.exportTables(cls)).join('\n');
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 upsertModel(cls: Class): Promise<void> {
119
- await this.#manager.upsertTables(cls);
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 deleteModel(cls: Class): Promise<void> {
123
- await this.#manager.dropTables(cls);
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 truncateModel(cls: Class): Promise<void> {
127
- await this.#manager.truncateTables(cls);
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 createStorage(): Promise<void> { }
131
- async deleteStorage(): Promise<void> { }
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
- @Transactional()
134
- async create<T extends ModelType>(cls: Class<T>, item: OptionalId<T>): Promise<T> {
135
- const prepped = await ModelCrudUtil.preStore(cls, item, this);
136
- try {
137
- for (const ins of this.#dialect.getAllInsertSQL(cls, prepped)) {
138
- await this.#exec(ins);
139
- }
140
- } catch (error) {
141
- if (error instanceof ExistsError) {
142
- throw new ExistsError(cls, prepped.id);
143
- } else {
144
- throw error;
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
- @Transactional()
151
- async update<T extends ModelType>(cls: Class<T>, item: T): Promise<T> {
152
- await this.#deleteRaw(cls, item.id, {}, true);
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
- @Transactional()
157
- async upsert<T extends ModelType>(cls: Class<T>, item: OptionalId<T>): Promise<T> {
158
- try {
159
- if (item.id) {
160
- await this.#deleteRaw(cls, item.id, {}, false);
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
- } catch (error) {
163
- if (!(error instanceof NotFoundError)) {
164
- throw error;
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
- @Transactional()
171
- async updatePartial<T extends ModelType>(cls: Class<T>, item: Partial<T> & { id: string }, view?: string): Promise<T> {
172
- const id = item.id;
173
- const final = await ModelCrudUtil.naivePartialUpdate(cls, () => this.get(cls, id), item, view);
174
- return this.update(cls, final);
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
- @Connected()
178
- async get<T extends ModelType>(cls: Class<T>, id: string): Promise<T> {
179
- const result = await this.query(cls, { where: castTo({ id }) });
180
- if (result.length === 1) {
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
- @ConnectedIterator()
187
- async * list<T extends ModelType>(cls: Class<T>): AsyncIterable<T> {
188
- for (const item of await this.query(cls, {})) {
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
- @Transactional()
194
- async delete<T extends ModelType>(cls: Class<T>, id: string): Promise<void> {
195
- await this.#deleteRaw(cls, id, {}, false);
407
+ async upsertModel(modelClass: Class): Promise<void> {
408
+ const tableContext = this.connection.getContext(modelClass);
409
+ await this.upsertTable(tableContext);
196
410
  }
197
411
 
198
- @Transactional()
199
- async processBulk<T extends ModelType>(cls: Class<T>, operations: BulkOperation<T>[]): Promise<BulkResponse> {
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
- const { insertedIds, upsertedIds, existingUpsertedIds } = await ModelBulkUtil.preStore(cls, operations, this);
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
- const addedIds = new Map([...insertedIds.entries(), ...upsertedIds.entries()]);
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
- await this.#checkUpsertedIds(cls,
206
- addedIds,
207
- new Map([...existingUpsertedIds.entries()].map(([key, value]) => [value, key]))
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
- const get = <K extends keyof BulkOperation<T>>(key: K): Required<BulkOperation<T>>[K][] =>
211
- operations.map(item => item[key]).filter((item): item is Required<BulkOperation<T>>[K] => !!item);
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
- const getStatements = async (key: keyof BulkOperation<T>): Promise<InsertWrapper[]> =>
214
- (await SQLModelUtil.getInserts(cls, get(key))).filter(wrapper => !!wrapper.records.length);
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 deletes = [{ stack: SQLModelUtil.classToStack(cls), ids: get('delete').map(wrapper => wrapper.id) }]
217
- .filter(wrapper => !!wrapper.ids.length);
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 [inserts, upserts, updates] = await Promise.all([
220
- getStatements('insert'),
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
- const result = await this.#dialect.bulkProcess(deletes, inserts, upserts, updates);
226
- result.insertedIds = addedIds;
227
- return result;
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
- // Expiry
231
- @Transactional()
232
- async deleteExpired<T extends ModelType>(cls: Class<T>): Promise<number> {
233
- return ModelQueryCrudUtil.deleteExpired(this, cls);
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
- @Connected()
237
- async query<T extends ModelType>(cls: Class<T>, query: PageableModelQuery<T>): Promise<T[]> {
238
- await QueryVerifier.verify(cls, query);
239
- const { records } = await this.#exec<T>(this.#dialect.getQuerySQL(cls, query, ModelQueryUtil.getWhereClause(cls, query.where)));
240
- if (ModelRegistryIndex.has(cls)) {
241
- await this.#dialect.fetchDependents(cls, records, query && query.select);
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
- const cleaned = SQLModelUtil.cleanResults<T>(this.#dialect, records);
245
- return await Promise.all(cleaned.map(item => ModelCrudUtil.load(cls, item)));
674
+ return {
675
+ items,
676
+ nextOffset: items.length === options?.limit ? String(nextOffset) : undefined
677
+ };
246
678
  }
247
679
 
248
- @Connected()
249
- async queryOne<T extends ModelType>(cls: Class<T>, builder: ModelQuery<T>, failOnMany = true): Promise<T> {
250
- const results = await this.query<T>(cls, { ...builder, limit: failOnMany ? 2 : 1 });
251
- return ModelQueryUtil.verifyGetSingleCounts<T>(cls, failOnMany, results, builder.where);
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
- @Connected()
255
- async queryCount<T extends ModelType>(cls: Class<T>, query: ModelQuery<T>): Promise<number> {
256
- await QueryVerifier.verify(cls, query);
257
- return this.#dialect.getCountForQuery(cls, query);
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
- @Connected()
261
- @Transactional()
262
- async updateByQuery<T extends ModelType>(cls: Class<T>, item: T, query: ModelQuery<T>): Promise<T> {
263
- await QueryVerifier.verify(cls, query);
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
- @Connected()
271
- @Transactional()
272
- async updatePartialByQuery<T extends ModelType>(cls: Class<T>, query: ModelQuery<T>, data: Partial<T>): Promise<number> {
273
- await QueryVerifier.verify(cls, query);
274
- const item = await ModelCrudUtil.prePartialUpdate(cls, data);
275
- const { count } = await this.#exec(this.#dialect.getUpdateSQL(SQLModelUtil.classToStack(cls), item, ModelQueryUtil.getWhereClause(cls, query.where)));
276
- return count;
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
- @Connected()
280
- @Transactional()
281
- async deleteByQuery<T extends ModelType>(cls: Class<T>, query: ModelQuery<T>): Promise<number> {
282
- await QueryVerifier.verify(cls, query);
283
- const { count } = await this.#exec(this.#dialect.getDeleteSQL(SQLModelUtil.classToStack(cls), ModelQueryUtil.getWhereClause(cls, query.where, false)));
284
- return count;
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
- @Connected()
288
- async suggest<T extends ModelType>(cls: Class<T>, field: ValidStringFields<T>, prefix?: string, query?: PageableModelQuery<T>): Promise<T[]> {
289
- await QueryVerifier.verify(cls, query);
290
- const resolvedQuery = ModelQuerySuggestUtil.getSuggestQuery<T>(cls, field, prefix, query);
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
- @Connected()
296
- async suggestValues<T extends ModelType>(cls: Class<T>, field: ValidStringFields<T>, prefix?: string, query?: PageableModelQuery<T>): Promise<string[]> {
297
- await QueryVerifier.verify(cls, query);
298
- const resolvedQuery = ModelQuerySuggestUtil.getSuggestFieldQuery(cls, field, prefix, query);
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 modelTypeField: ValidStringFields<ModelType> = castTo(field);
302
- return ModelQuerySuggestUtil.combineSuggestResults(cls, modelTypeField, prefix, results, result => result, query?.limit);
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
- @Connected()
306
- async facet<T extends ModelType>(cls: Class<T>, field: ValidStringFields<T>, query?: ModelQuery<T>): Promise<ModelQueryFacet[]> {
307
- await QueryVerifier.verify(cls, query);
308
- const col = this.#dialect.identifier(field);
309
- const ttl = this.#dialect.identifier('count');
310
- const key = this.#dialect.identifier('key');
311
- const sql = [
312
- `SELECT ${col} as ${key}, COUNT(${col}) as ${ttl}`,
313
- this.#dialect.getFromSQL(cls),
314
- ];
315
- sql.push(
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
- const results = await this.#exec<{ key: string, count: number }>(sql.join('\n'));
324
- return results.records.map(result => {
325
- result.count = DataUtil.coerceType(result.count, Number);
326
- return result;
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
+ }