@travetto/model-sql 8.0.0-alpha.24 → 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 +3 -6
- package/package.json +8 -8
- package/src/connection.ts +218 -0
- package/src/dialect.ts +799 -0
- package/src/schema.ts +46 -0
- package/src/service.ts +715 -386
- package/src/types.ts +15 -9
- package/support/test/query.ts +111 -72
- package/src/config.ts +0 -45
- package/src/connection/base.ts +0 -188
- package/src/connection/decorator.ts +0 -54
- package/src/dialect/base.ts +0 -1142
- package/src/internal/types.ts +0 -64
- package/src/table-manager.ts +0 -169
- package/src/util.ts +0 -331
package/src/service.ts
CHANGED
|
@@ -1,501 +1,830 @@
|
|
|
1
|
+
import { Injectable } from '@travetto/di';
|
|
1
2
|
import {
|
|
2
|
-
type
|
|
3
|
-
type
|
|
4
|
-
|
|
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,
|
|
5
13
|
type ModelListOptions,
|
|
14
|
+
ModelRegistryIndex,
|
|
15
|
+
type ModelStorageSupport,
|
|
16
|
+
type ModelType,
|
|
17
|
+
NotFoundError,
|
|
18
|
+
type OptionalId
|
|
6
19
|
} from '@travetto/model';
|
|
7
20
|
import {
|
|
8
|
-
type
|
|
9
|
-
type
|
|
10
|
-
type
|
|
21
|
+
type FullKeyedIndexBody,
|
|
22
|
+
type FullKeyedIndexWithPartialBody,
|
|
23
|
+
type KeyedIndexBody,
|
|
24
|
+
type KeyedIndexSelection,
|
|
25
|
+
ModelIndexedComputedIndex,
|
|
26
|
+
type ModelIndexedSearchOptions,
|
|
27
|
+
type ModelIndexedSupport,
|
|
28
|
+
ModelIndexedUtil,
|
|
29
|
+
type ModelPageOptions,
|
|
30
|
+
type ModelPageResult,
|
|
31
|
+
type SingleItemIndex,
|
|
32
|
+
type SortedIndex,
|
|
33
|
+
type SortedIndexSelection,
|
|
34
|
+
type SortedIndexSelectionType,
|
|
35
|
+
warnIfIndexedUniqueIndex,
|
|
36
|
+
warnIfNonIndexedIndex
|
|
11
37
|
} from '@travetto/model-indexed';
|
|
12
|
-
import { castTo, type Class, JSONUtil } from '@travetto/runtime';
|
|
13
|
-
import { DataUtil } from '@travetto/schema';
|
|
14
|
-
import type { AsyncContext } from '@travetto/context';
|
|
15
|
-
import { Injectable, PostConstruct } from '@travetto/di';
|
|
16
38
|
import {
|
|
17
|
-
type ModelQuery,
|
|
18
|
-
type
|
|
19
|
-
|
|
39
|
+
type ModelQuery,
|
|
40
|
+
type ModelQueryCrudSupport,
|
|
41
|
+
ModelQueryCrudUtil,
|
|
42
|
+
type ModelQueryFacet,
|
|
43
|
+
type ModelQueryFacetSupport,
|
|
44
|
+
type ModelQuerySuggestSupport,
|
|
45
|
+
ModelQuerySuggestUtil,
|
|
46
|
+
type ModelQuerySupport,
|
|
47
|
+
ModelQueryUtil,
|
|
48
|
+
type PageableModelQuery,
|
|
49
|
+
QueryVerifier,
|
|
50
|
+
type ValidStringFields,
|
|
51
|
+
type WhereClause
|
|
20
52
|
} from '@travetto/model-query';
|
|
53
|
+
import { type Class, castTo, JSONUtil } from '@travetto/runtime';
|
|
54
|
+
import { WorkPool } from '@travetto/worker';
|
|
21
55
|
|
|
22
|
-
import type {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import type {
|
|
26
|
-
import { TableManager } from './table-manager.ts';
|
|
27
|
-
import type { Connection } from './connection/base.ts';
|
|
28
|
-
import type { InsertWrapper } from './internal/types.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';
|
|
29
60
|
|
|
30
61
|
/**
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
62
|
+
* Base SQL Model Service.
|
|
63
|
+
* Implements CRUD, Query, Expiry, Bulk, Indexed, and Suggest operations
|
|
64
|
+
* by delegating to connection and dialect components.
|
|
34
65
|
*/
|
|
35
66
|
@Injectable()
|
|
36
|
-
export class
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
readonly
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const allIds = new Set(all.map(type => type.id));
|
|
81
|
-
|
|
82
|
-
for (const [id, idx] of toCheck.entries()) {
|
|
83
|
-
if (!allIds.has(id)) { // If not found
|
|
84
|
-
addedIds.set(idx, id);
|
|
67
|
+
export abstract class BaseSQLModelService<C = unknown>
|
|
68
|
+
implements
|
|
69
|
+
ModelCrudSupport,
|
|
70
|
+
ModelStorageSupport,
|
|
71
|
+
ModelBulkSupport,
|
|
72
|
+
ModelExpirySupport,
|
|
73
|
+
ModelIndexedSupport,
|
|
74
|
+
ModelQuerySupport,
|
|
75
|
+
ModelQueryCrudSupport,
|
|
76
|
+
ModelQueryFacetSupport,
|
|
77
|
+
ModelQuerySuggestSupport
|
|
78
|
+
{
|
|
79
|
+
abstract readonly client: C;
|
|
80
|
+
abstract connection: SQLConnection;
|
|
81
|
+
|
|
82
|
+
idSource = ModelCrudUtil.uuidSource();
|
|
83
|
+
|
|
84
|
+
get dialect(): AbstractANSI99Dialect {
|
|
85
|
+
return this.connection.dialect;
|
|
86
|
+
}
|
|
87
|
+
|
|
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
|
+
}
|
|
95
|
+
|
|
96
|
+
async initialize(): Promise<void> {
|
|
97
|
+
await this.connection.init();
|
|
98
|
+
await this.createStorage();
|
|
99
|
+
ModelExpiryUtil.registerCull(this);
|
|
100
|
+
}
|
|
101
|
+
|
|
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);
|
|
85
110
|
}
|
|
86
111
|
}
|
|
87
|
-
|
|
88
|
-
return addedIds;
|
|
112
|
+
return ModelCrudUtil.load(modelClass, resolvedRecord);
|
|
89
113
|
}
|
|
90
114
|
|
|
91
|
-
|
|
92
|
-
return 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))));
|
|
93
117
|
}
|
|
94
118
|
|
|
95
|
-
async
|
|
96
|
-
|
|
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);
|
|
97
127
|
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
|
|
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 };
|
|
103
138
|
}
|
|
139
|
+
|
|
140
|
+
return result;
|
|
104
141
|
}
|
|
105
142
|
|
|
106
|
-
async
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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;
|
|
165
|
+
}
|
|
166
|
+
|
|
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);
|
|
129
185
|
}
|
|
130
186
|
}
|
|
131
187
|
|
|
132
|
-
|
|
133
|
-
async
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
await ModelStorageUtil.storageInitialization(this);
|
|
138
|
-
ModelExpiryUtil.registerCull(this);
|
|
139
|
-
}
|
|
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 });
|
|
140
193
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
194
|
+
const result = await this.connection.execute<Record<string, unknown>>(sql, parameters);
|
|
195
|
+
|
|
196
|
+
if (result.count === 0) {
|
|
197
|
+
throw new NotFoundError(modelClass, id);
|
|
198
|
+
}
|
|
144
199
|
|
|
145
|
-
|
|
146
|
-
return (await this.#manager.exportTables(cls)).join('\n');
|
|
200
|
+
return this.loadSingle(modelClass, result.records[0]);
|
|
147
201
|
}
|
|
148
202
|
|
|
149
|
-
async
|
|
150
|
-
await this
|
|
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;
|
|
151
212
|
}
|
|
152
213
|
|
|
153
|
-
async
|
|
154
|
-
await this
|
|
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);
|
|
218
|
+
}
|
|
219
|
+
return preppedItem;
|
|
155
220
|
}
|
|
156
221
|
|
|
157
|
-
async
|
|
158
|
-
|
|
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);
|
|
159
224
|
}
|
|
160
225
|
|
|
161
|
-
async
|
|
162
|
-
|
|
226
|
+
async updatePartial<T extends ModelType>(modelClass: Class<T>, item: Partial<T> & { id: string }, view?: string): Promise<T> {
|
|
227
|
+
ModelCrudUtil.ensureNotSubType(modelClass);
|
|
163
228
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
for (const ins of this.#dialect.getAllInsertSQL(cls, prepped)) {
|
|
169
|
-
await this.#exec(ins);
|
|
170
|
-
}
|
|
171
|
-
} catch (error) {
|
|
172
|
-
if (error instanceof ExistsError) {
|
|
173
|
-
throw new ExistsError(cls, prepped.id);
|
|
174
|
-
} else {
|
|
175
|
-
throw error;
|
|
176
|
-
}
|
|
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);
|
|
177
233
|
}
|
|
178
|
-
return prepped;
|
|
179
|
-
}
|
|
180
234
|
|
|
181
|
-
|
|
182
|
-
async update<T extends ModelType>(cls: Class<T>, item: T): Promise<T> {
|
|
183
|
-
await this.#deleteRaw(cls, item.id, {}, true);
|
|
184
|
-
return await this.create(cls, item);
|
|
235
|
+
return this.loadSingle(modelClass, result.records[0]);
|
|
185
236
|
}
|
|
186
237
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
}
|
|
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);
|
|
197
247
|
}
|
|
198
|
-
return await this.create(cls, item);
|
|
199
248
|
}
|
|
200
249
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
const id = item.id;
|
|
204
|
-
const final = await ModelCrudUtil.naivePartialUpdate(cls, () => this.get(cls, id), item, view);
|
|
205
|
-
return this.update(cls, final);
|
|
250
|
+
async *list<T extends ModelType>(modelClass: Class<T>, options?: ModelListOptions): AsyncIterable<T[]> {
|
|
251
|
+
yield* this.listWithOffset(modelClass, options);
|
|
206
252
|
}
|
|
207
253
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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);
|
|
257
|
+
|
|
258
|
+
const limit = options?.limit ?? Number.MAX_SAFE_INTEGER;
|
|
259
|
+
const batchSize = Math.min(options?.batchSizeHint ?? 100, limit);
|
|
260
|
+
|
|
261
|
+
let offset = options?.offset ?? 0;
|
|
262
|
+
let produced = 0;
|
|
263
|
+
|
|
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 });
|
|
267
|
+
|
|
268
|
+
const result = await this.connection.execute(sql, parameters);
|
|
269
|
+
if (result.count === 0) {
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
216
272
|
|
|
217
|
-
|
|
218
|
-
async * list<T extends ModelType>(cls: Class<T>, options?: ModelListOptions): AsyncIterable<T[]> {
|
|
219
|
-
for await (const { items } of this.#scanTable(cls, () => ({}), options)) {
|
|
273
|
+
const items = await this.loadMany(modelClass, result.records);
|
|
220
274
|
yield items;
|
|
275
|
+
produced += items.length;
|
|
276
|
+
offset += items.length;
|
|
221
277
|
}
|
|
222
278
|
}
|
|
223
279
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
await this
|
|
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);
|
|
227
283
|
}
|
|
228
284
|
|
|
229
|
-
|
|
230
|
-
|
|
285
|
+
async dropTable<T extends ModelType>(tableContext: TableContext<T>): Promise<void> {
|
|
286
|
+
const sql = this.dialect.getDropTableSQL(tableContext);
|
|
287
|
+
await this.connection.execute(sql);
|
|
288
|
+
}
|
|
231
289
|
|
|
232
|
-
|
|
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
|
+
}
|
|
233
294
|
|
|
234
|
-
|
|
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);
|
|
235
300
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
);
|
|
301
|
+
if (!tableExists) {
|
|
302
|
+
const createTableSQL = this.dialect.getCreateTableSQL(tableContext);
|
|
303
|
+
await this.connection.execute(createTableSQL);
|
|
240
304
|
|
|
241
|
-
|
|
242
|
-
|
|
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
|
+
}
|
|
243
320
|
|
|
244
|
-
|
|
245
|
-
|
|
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
|
+
}
|
|
246
336
|
|
|
247
|
-
|
|
248
|
-
.
|
|
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);
|
|
249
340
|
|
|
250
|
-
|
|
251
|
-
getStatements('insert'),
|
|
252
|
-
getStatements('upsert'),
|
|
253
|
-
getStatements('update')
|
|
254
|
-
]);
|
|
341
|
+
const modelIndexes = ModelRegistryIndex.getIndices(tableContext.cls) || [];
|
|
255
342
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
+
}
|
|
260
367
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
+
}
|
|
374
|
+
}
|
|
265
375
|
}
|
|
266
376
|
|
|
267
|
-
|
|
268
|
-
async
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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);
|
|
273
384
|
}
|
|
385
|
+
}
|
|
274
386
|
|
|
275
|
-
|
|
276
|
-
|
|
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
|
+
}
|
|
277
392
|
}
|
|
278
393
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
return ModelQueryUtil.verifyGetSingleCounts<T>(cls, failOnMany, results, builder.where);
|
|
394
|
+
async deleteModel(modelClass: Class): Promise<void> {
|
|
395
|
+
const tableContext = this.connection.getContext(modelClass);
|
|
396
|
+
await this.dropTable(tableContext);
|
|
283
397
|
}
|
|
284
398
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
await
|
|
288
|
-
return this.#dialect.getCountForQuery(cls, query);
|
|
399
|
+
async upsertModel(modelClass: Class): Promise<void> {
|
|
400
|
+
const tableContext = this.connection.getContext(modelClass);
|
|
401
|
+
await this.upsertTable(tableContext);
|
|
289
402
|
}
|
|
290
403
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
await QueryVerifier.verify(cls, query);
|
|
295
|
-
const where = ModelQueryUtil.getWhereClause(cls, query.where);
|
|
296
|
-
where.id = item.id;
|
|
297
|
-
await this.#deleteRaw(cls, item.id, where, true);
|
|
298
|
-
return await this.create(cls, item);
|
|
404
|
+
async truncateModel<T extends ModelType>(modelClass: Class<T>): Promise<void> {
|
|
405
|
+
const tableContext = this.connection.getContext(modelClass);
|
|
406
|
+
await this.truncateTable(tableContext);
|
|
299
407
|
}
|
|
300
408
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
const
|
|
307
|
-
|
|
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 }
|
|
507
|
+
);
|
|
508
|
+
|
|
509
|
+
return {
|
|
510
|
+
errors,
|
|
511
|
+
insertedIds: addedIdentifiers,
|
|
512
|
+
counts
|
|
513
|
+
};
|
|
308
514
|
}
|
|
309
515
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
await QueryVerifier.verify(cls, query);
|
|
314
|
-
const { count } = await this.#exec(this.#dialect.getDeleteSQL(SQLModelUtil.classToStack(cls), ModelQueryUtil.getWhereClause(cls, query.where, false)));
|
|
315
|
-
return count;
|
|
516
|
+
// Expiry Support
|
|
517
|
+
async deleteExpired<T extends ModelType>(modelClass: Class<T>): Promise<number> {
|
|
518
|
+
return ModelQueryCrudUtil.deleteExpired(this, modelClass);
|
|
316
519
|
}
|
|
317
520
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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
|
+
}
|
|
324
534
|
}
|
|
325
535
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
536
|
+
async getByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
|
|
537
|
+
modelClass: Class<T>,
|
|
538
|
+
indexConfig: SingleItemIndex<T, K, S>,
|
|
539
|
+
body: FullKeyedIndexBody<T, K, S>
|
|
540
|
+
): Promise<T> {
|
|
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 }));
|
|
331
544
|
|
|
332
|
-
const
|
|
333
|
-
|
|
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]);
|
|
334
553
|
}
|
|
335
554
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
const
|
|
343
|
-
|
|
344
|
-
this.#dialect.getFromSQL(cls),
|
|
345
|
-
];
|
|
346
|
-
sql.push(
|
|
347
|
-
this.#dialect.getWhereSQL(cls, ModelQueryUtil.getWhereClause(cls, query?.where))
|
|
348
|
-
);
|
|
349
|
-
sql.push(
|
|
350
|
-
`GROUP BY ${col}`,
|
|
351
|
-
`ORDER BY ${ttl} DESC`
|
|
352
|
-
);
|
|
555
|
+
async deleteByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
|
|
556
|
+
modelClass: Class<T>,
|
|
557
|
+
indexConfig: SingleItemIndex<T, K, S>,
|
|
558
|
+
body: FullKeyedIndexBody<T, K, S>
|
|
559
|
+
): Promise<void> {
|
|
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 }));
|
|
353
563
|
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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);
|
|
359
570
|
}
|
|
360
571
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
T
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
>(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: FullKeyedIndexBody<T, K, S>): Promise<T> {
|
|
368
|
-
const computed = ModelIndexedComputedIndex.get(idx, body).validate({ sort: true });
|
|
369
|
-
const results = await this.query(cls, castTo({ where: computed.project({ sort: true, includeId: true }) }));
|
|
370
|
-
if (results.length !== 1) {
|
|
371
|
-
throw new NotFoundError(`${cls.name}: ${idx}`, computed.getKey({ sort: true }));
|
|
372
|
-
}
|
|
373
|
-
return results[0];
|
|
572
|
+
async upsertByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
|
|
573
|
+
modelClass: Class<T>,
|
|
574
|
+
indexConfig: SingleItemIndex<T, K, S>,
|
|
575
|
+
body: OptionalId<T>
|
|
576
|
+
): Promise<T> {
|
|
577
|
+
return ModelIndexedUtil.naiveUpsert(this, modelClass, indexConfig, body);
|
|
374
578
|
}
|
|
375
579
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
T
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
const
|
|
385
|
-
if (
|
|
386
|
-
throw new NotFoundError(`${
|
|
580
|
+
async updateByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
|
|
581
|
+
modelClass: Class<T>,
|
|
582
|
+
indexConfig: SingleItemIndex<T, K, S>,
|
|
583
|
+
body: T
|
|
584
|
+
): Promise<T> {
|
|
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());
|
|
387
591
|
}
|
|
592
|
+
return preppedItem;
|
|
388
593
|
}
|
|
389
594
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
T
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
>(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: OptionalId<T>): Promise<T> {
|
|
397
|
-
return ModelIndexedUtil.naiveUpsert(this, cls, idx, body);
|
|
398
|
-
}
|
|
595
|
+
async updatePartialByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
|
|
596
|
+
modelClass: Class<T>,
|
|
597
|
+
indexConfig: SingleItemIndex<T, K, S>,
|
|
598
|
+
body: FullKeyedIndexWithPartialBody<T, K, S>
|
|
599
|
+
): Promise<T> {
|
|
600
|
+
ModelCrudUtil.ensureNotSubType(modelClass);
|
|
399
601
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
updateByIndex<
|
|
403
|
-
T extends ModelType,
|
|
404
|
-
K extends KeyedIndexSelection<T>,
|
|
405
|
-
S extends SortedIndexSelection<T>
|
|
406
|
-
>(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: T): Promise<T> {
|
|
407
|
-
return ModelIndexedUtil.naiveUpdate(this, cls, idx, body);
|
|
408
|
-
}
|
|
602
|
+
const computed = ModelIndexedComputedIndex.get(indexConfig, castTo(body)).validate({ sort: true });
|
|
603
|
+
const where: WhereClause<T> = castTo(computed.project({ sort: true, includeId: true }));
|
|
409
604
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
K extends KeyedIndexSelection<T>,
|
|
415
|
-
S extends SortedIndexSelection<T>
|
|
416
|
-
>(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: FullKeyedIndexWithPartialBody<T, K, S>): Promise<T> {
|
|
417
|
-
const item = await ModelCrudUtil.naivePartialUpdate(cls, () => this.getByIndex(cls, idx, castTo(body)), castTo(body));
|
|
418
|
-
return this.update(cls, item);
|
|
605
|
+
const result = await this.executeUpdatePartial(modelClass, where, castTo(body), true);
|
|
606
|
+
this.validateIndexResult(modelClass, result, indexConfig, computed);
|
|
607
|
+
|
|
608
|
+
return this.loadSingle(modelClass, result.records[0]);
|
|
419
609
|
}
|
|
420
610
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
T
|
|
424
|
-
K extends KeyedIndexSelection<T>,
|
|
425
|
-
S extends SortedIndexSelection<T>
|
|
426
|
-
>(
|
|
427
|
-
cls: Class<T>,
|
|
428
|
-
idx: SortedIndex<T, K, S>,
|
|
611
|
+
async *listByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
|
|
612
|
+
modelClass: Class<T>,
|
|
613
|
+
indexConfig: SortedIndex<T, K, S>,
|
|
429
614
|
body: KeyedIndexBody<T, K>,
|
|
430
|
-
options?:
|
|
431
|
-
):
|
|
432
|
-
const computed = ModelIndexedComputedIndex.get(
|
|
433
|
-
const
|
|
615
|
+
options?: ModelListOptions & { offset?: number }
|
|
616
|
+
): AsyncIterable<T[]> {
|
|
617
|
+
const computed = ModelIndexedComputedIndex.get(indexConfig, body).validate();
|
|
618
|
+
const where: WhereClause<T> = castTo(computed.project());
|
|
434
619
|
|
|
435
|
-
const
|
|
436
|
-
|
|
437
|
-
sort: idx.sortTemplate.map(part => ({ [part.path.join('.')]: part.value })),
|
|
438
|
-
});
|
|
620
|
+
const tableContext = this.connection.getContext(modelClass);
|
|
621
|
+
const sortSQL = this.dialect.buildIndexSort(tableContext, indexConfig);
|
|
439
622
|
|
|
440
|
-
const
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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);
|
|
641
|
+
yield items;
|
|
642
|
+
produced += items.length;
|
|
643
|
+
offset += items.length;
|
|
445
644
|
}
|
|
446
|
-
return { items, nextOffset: nextOffset ? JSONUtil.toBase64(nextOffset) : undefined };
|
|
447
645
|
}
|
|
448
646
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
T
|
|
452
|
-
K extends KeyedIndexSelection<T>,
|
|
453
|
-
S extends SortedIndexSelection<T>
|
|
454
|
-
>(
|
|
455
|
-
cls: Class<T>,
|
|
456
|
-
idx: SortedIndex<T, K, S>,
|
|
647
|
+
async pageByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
|
|
648
|
+
modelClass: Class<T>,
|
|
649
|
+
indexConfig: SortedIndex<T, K, S>,
|
|
457
650
|
body: KeyedIndexBody<T, K>,
|
|
458
|
-
options?:
|
|
459
|
-
):
|
|
460
|
-
const
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
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;
|
|
467
664
|
}
|
|
665
|
+
|
|
666
|
+
return {
|
|
667
|
+
items,
|
|
668
|
+
nextOffset: items.length === options?.limit ? String(nextOffset) : undefined
|
|
669
|
+
};
|
|
468
670
|
}
|
|
469
671
|
|
|
470
|
-
@Connected()
|
|
471
672
|
async suggestByIndex<
|
|
472
673
|
T extends ModelType,
|
|
473
674
|
S extends SortedIndexSelection<T>,
|
|
474
675
|
K extends KeyedIndexSelection<T>,
|
|
475
676
|
B extends SortedIndexSelectionType<T, S> & string
|
|
476
|
-
>(
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
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);
|
|
483
700
|
}
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
const
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
701
|
+
|
|
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
|
|
493
720
|
});
|
|
721
|
+
const result = await this.connection.execute(sql, parameters);
|
|
722
|
+
|
|
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);
|
|
494
737
|
|
|
495
|
-
|
|
496
|
-
|
|
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)}`);
|
|
497
765
|
}
|
|
498
766
|
|
|
499
|
-
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
|
+
}));
|
|
500
829
|
}
|
|
501
|
-
}
|
|
830
|
+
}
|