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