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