@jskit-ai/crud-core 0.1.53 → 0.1.55
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/package.descriptor.mjs +2 -2
- package/package.json +9 -10
- package/src/server/lookupHydration.js +20 -20
- package/src/server/lookupPathSupport.js +5 -5
- package/src/server/{lookupProviders.js → lookups.js} +14 -14
- package/src/server/repositorySupport.js +76 -6
- package/src/server/resourceRuntime/index.js +1413 -0
- package/src/server/resourceRuntime/lookupHydration.js +685 -0
- package/src/server/serviceEvents.js +1 -1
- package/src/server/serviceMethods.js +32 -31
- package/src/shared/crudFieldMetaSupport.js +64 -1
- package/test/createCrudServiceFromResource.test.js +58 -74
- package/test/lookups.test.js +135 -0
- package/test/repositorySupport.test.js +160 -6
- package/test/resourceRuntime.test.js +916 -0
- package/test/serviceEvents.test.js +3 -3
- package/test/serviceMethods.test.js +14 -38
- package/src/server/createCrudRepositoryFromResource.js +0 -66
- package/src/server/repositoryMethods.js +0 -1485
- package/test/createCrudRepositoryFromResource.test.js +0 -2086
- package/test/lookupProviders.test.js +0 -135
|
@@ -0,0 +1,1413 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createWithTransaction,
|
|
3
|
+
resolveInsertedRecordId,
|
|
4
|
+
toInsertDateTime
|
|
5
|
+
} from "@jskit-ai/database-runtime/shared";
|
|
6
|
+
import { applyVisibility, applyVisibilityOwners } from "@jskit-ai/database-runtime/shared/visibility";
|
|
7
|
+
import { AppError, createValidationError } from "@jskit-ai/kernel/server/runtime/errors";
|
|
8
|
+
import { isRecord, normalizeRecordId, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
9
|
+
import { Check, Errors } from "typebox/value";
|
|
10
|
+
import {
|
|
11
|
+
DEFAULT_LIST_LIMIT,
|
|
12
|
+
MAX_LIST_LIMIT,
|
|
13
|
+
buildRepositoryColumnMetadata,
|
|
14
|
+
deriveRepositoryMappingFromResource,
|
|
15
|
+
normalizeCrudListLimit,
|
|
16
|
+
requireCrudTableName,
|
|
17
|
+
applyCrudListQueryFilters,
|
|
18
|
+
buildWritePayload,
|
|
19
|
+
mapRecordRow,
|
|
20
|
+
resolveColumnName,
|
|
21
|
+
resolveCrudIdColumn
|
|
22
|
+
} from "../repositorySupport.js";
|
|
23
|
+
import {
|
|
24
|
+
createCrudLookupRuntime,
|
|
25
|
+
hydrateCrudLookupRecords
|
|
26
|
+
} from "./lookupHydration.js";
|
|
27
|
+
import { CRUD_FIELD_REPOSITORY_STORAGE_COLUMN } from "../../shared/crudFieldMetaSupport.js";
|
|
28
|
+
|
|
29
|
+
const LIST_ORDER_DIRECTION_ASC = "asc";
|
|
30
|
+
const LIST_ORDER_DIRECTION_DESC = "desc";
|
|
31
|
+
const LIST_ORDER_NULLS_FIRST = "first";
|
|
32
|
+
const LIST_ORDER_NULLS_LAST = "last";
|
|
33
|
+
const ORDERED_LIST_CURSOR_VALUE_TYPE_KEY = "__jskitCursorValueType";
|
|
34
|
+
const ORDERED_LIST_CURSOR_VALUE_KEY = "value";
|
|
35
|
+
const ORDERED_LIST_CURSOR_VALUE_TYPE_DATE = "date";
|
|
36
|
+
const REPOSITORY_OPERATION_KEYS = Object.freeze([
|
|
37
|
+
"read",
|
|
38
|
+
"list",
|
|
39
|
+
"findById",
|
|
40
|
+
"listByIds",
|
|
41
|
+
"create",
|
|
42
|
+
"updateById",
|
|
43
|
+
"deleteById"
|
|
44
|
+
]);
|
|
45
|
+
const REPOSITORY_OPERATION_STAGE_KEYS = Object.freeze({
|
|
46
|
+
read: ["applyQuery"],
|
|
47
|
+
list: ["applyQuery"],
|
|
48
|
+
findById: ["applyQuery"],
|
|
49
|
+
listByIds: ["applyQuery"],
|
|
50
|
+
create: ["preparePayload", "prepareInsertPayload", "applyQuery"],
|
|
51
|
+
updateById: ["preparePatch", "applyQuery"],
|
|
52
|
+
deleteById: ["applyQuery"]
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
function requireCrudRecordId(value, { context = "crudRepository" } = {}) {
|
|
56
|
+
const recordId = normalizeRecordId(value, { fallback: null });
|
|
57
|
+
if (!recordId) {
|
|
58
|
+
throw new TypeError(`${context} requires recordId.`);
|
|
59
|
+
}
|
|
60
|
+
return recordId;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function requireCrudRepositoryOptions(value = {}, { context = "crudRepository" } = {}) {
|
|
64
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
65
|
+
throw new TypeError(`${context} requires repository options object when provided.`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function resolveRepositoryDefaults(resource = {}, repositoryMapping = {}, { context = "crudRepository" } = {}) {
|
|
72
|
+
const resourceName = normalizeText(resource.namespace);
|
|
73
|
+
const tableName = normalizeText(resource.tableName) || resourceName;
|
|
74
|
+
if (!tableName) {
|
|
75
|
+
throw new TypeError(`${context} requires resource.tableName or resource.namespace.`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const idColumn = normalizeText(resource.idColumn) || resolveColumnName("id", repositoryMapping.columnOverrides) || "id";
|
|
79
|
+
const createdAtColumn = repositoryMapping.outputKeys.includes("createdAt") &&
|
|
80
|
+
repositoryMapping.fieldStorageByKey?.createdAt === CRUD_FIELD_REPOSITORY_STORAGE_COLUMN
|
|
81
|
+
? resolveColumnName("createdAt", repositoryMapping.columnOverrides)
|
|
82
|
+
: "";
|
|
83
|
+
const updatedAtColumn = repositoryMapping.outputKeys.includes("updatedAt") &&
|
|
84
|
+
repositoryMapping.fieldStorageByKey?.updatedAt === CRUD_FIELD_REPOSITORY_STORAGE_COLUMN
|
|
85
|
+
? resolveColumnName("updatedAt", repositoryMapping.columnOverrides)
|
|
86
|
+
: "";
|
|
87
|
+
|
|
88
|
+
return Object.freeze({
|
|
89
|
+
tableName,
|
|
90
|
+
idColumn,
|
|
91
|
+
createdAtColumn,
|
|
92
|
+
updatedAtColumn
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function normalizeCrudVirtualFieldHandlers(
|
|
97
|
+
virtualFields = {},
|
|
98
|
+
repositoryMapping = {},
|
|
99
|
+
{ context = "crudRepository" } = {}
|
|
100
|
+
) {
|
|
101
|
+
const expectedKeys = new Set(
|
|
102
|
+
(Array.isArray(repositoryMapping?.virtualOutputKeys) ? repositoryMapping.virtualOutputKeys : [])
|
|
103
|
+
.map((key) => normalizeText(key))
|
|
104
|
+
.filter(Boolean)
|
|
105
|
+
);
|
|
106
|
+
if (expectedKeys.size < 1) {
|
|
107
|
+
if (virtualFields === null || virtualFields === undefined) {
|
|
108
|
+
return Object.freeze([]);
|
|
109
|
+
}
|
|
110
|
+
if (!virtualFields || typeof virtualFields !== "object" || Array.isArray(virtualFields)) {
|
|
111
|
+
throw new TypeError(`${context} virtualFields must be an object when provided.`);
|
|
112
|
+
}
|
|
113
|
+
if (Object.keys(virtualFields).length > 0) {
|
|
114
|
+
throw new Error(
|
|
115
|
+
`${context} virtualFields contains registrations, but the resource does not declare any repository.storage "virtual" fields.`
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
return Object.freeze([]);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (!virtualFields || typeof virtualFields !== "object" || Array.isArray(virtualFields)) {
|
|
122
|
+
throw new TypeError(`${context} virtualFields must be an object.`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const normalizedHandlers = [];
|
|
126
|
+
const seenKeys = new Set();
|
|
127
|
+
for (const [rawKey, handlerConfig] of Object.entries(virtualFields)) {
|
|
128
|
+
const key = normalizeText(rawKey);
|
|
129
|
+
if (!key || seenKeys.has(key)) {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (!expectedKeys.has(key)) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
`${context} virtualFields["${key}"] is unknown; declare the field in resource.fieldMeta with repository.storage "virtual".`
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
if (!handlerConfig || typeof handlerConfig !== "object" || Array.isArray(handlerConfig)) {
|
|
138
|
+
throw new TypeError(`${context} virtualFields["${key}"] must be an object.`);
|
|
139
|
+
}
|
|
140
|
+
if (typeof handlerConfig.applyProjection !== "function") {
|
|
141
|
+
throw new TypeError(`${context} virtualFields["${key}"].applyProjection must be a function.`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
seenKeys.add(key);
|
|
145
|
+
normalizedHandlers.push(Object.freeze({
|
|
146
|
+
key,
|
|
147
|
+
alias: resolveColumnName(key, repositoryMapping.columnOverrides),
|
|
148
|
+
applyProjection: handlerConfig.applyProjection
|
|
149
|
+
}));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
for (const key of expectedKeys) {
|
|
153
|
+
if (!seenKeys.has(key)) {
|
|
154
|
+
throw new Error(
|
|
155
|
+
`${context} resource output field "${key}" is virtual but no repository runtime projection was registered.`
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return Object.freeze(normalizedHandlers);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function applyCrudRepositoryVirtualProjections(
|
|
164
|
+
dbQuery,
|
|
165
|
+
runtime = {},
|
|
166
|
+
{ knex, tableName } = {}
|
|
167
|
+
) {
|
|
168
|
+
const virtualFields = Array.isArray(runtime?.virtualFields) ? runtime.virtualFields : [];
|
|
169
|
+
if (virtualFields.length < 1) {
|
|
170
|
+
return dbQuery;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
for (const virtualField of virtualFields) {
|
|
174
|
+
virtualField.applyProjection(dbQuery, {
|
|
175
|
+
knex,
|
|
176
|
+
tableName,
|
|
177
|
+
alias: virtualField.alias,
|
|
178
|
+
fieldKey: virtualField.key
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return dbQuery;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function normalizeSearchColumns(searchColumns = [], fallbackColumns = []) {
|
|
186
|
+
const normalizedConfiguredColumns = (Array.isArray(searchColumns) ? searchColumns : [])
|
|
187
|
+
.map((columnName) => String(columnName || "").trim())
|
|
188
|
+
.filter(Boolean);
|
|
189
|
+
|
|
190
|
+
if (normalizedConfiguredColumns.length > 0) {
|
|
191
|
+
return Object.freeze([...new Set(normalizedConfiguredColumns)]);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return Object.freeze(
|
|
195
|
+
(Array.isArray(fallbackColumns) ? fallbackColumns : [])
|
|
196
|
+
.map((columnName) => String(columnName || "").trim())
|
|
197
|
+
.filter(Boolean)
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function normalizeListOrderDirection(value = LIST_ORDER_DIRECTION_ASC) {
|
|
202
|
+
const normalized = normalizeText(value).toLowerCase();
|
|
203
|
+
if (!normalized) {
|
|
204
|
+
return LIST_ORDER_DIRECTION_ASC;
|
|
205
|
+
}
|
|
206
|
+
if (normalized === LIST_ORDER_DIRECTION_ASC || normalized === LIST_ORDER_DIRECTION_DESC) {
|
|
207
|
+
return normalized;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
throw new TypeError(`crudRepository list.orderBy direction must be "${LIST_ORDER_DIRECTION_ASC}" or "${LIST_ORDER_DIRECTION_DESC}".`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function normalizeListOrderNulls(value = LIST_ORDER_NULLS_LAST) {
|
|
214
|
+
const normalized = normalizeText(value).toLowerCase();
|
|
215
|
+
if (!normalized) {
|
|
216
|
+
return LIST_ORDER_NULLS_LAST;
|
|
217
|
+
}
|
|
218
|
+
if (normalized === LIST_ORDER_NULLS_FIRST || normalized === LIST_ORDER_NULLS_LAST) {
|
|
219
|
+
return normalized;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
throw new TypeError(`crudRepository list.orderBy nulls must be "${LIST_ORDER_NULLS_FIRST}" or "${LIST_ORDER_NULLS_LAST}".`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function normalizeListOrderBy(orderBy = [], { idColumn = "id" } = {}) {
|
|
226
|
+
const sourceEntries = Array.isArray(orderBy)
|
|
227
|
+
? orderBy
|
|
228
|
+
: orderBy === null || orderBy === undefined
|
|
229
|
+
? []
|
|
230
|
+
: [orderBy];
|
|
231
|
+
const normalizedIdColumn = normalizeText(idColumn) || "id";
|
|
232
|
+
const normalizedOrderBy = [];
|
|
233
|
+
const seenColumns = new Set();
|
|
234
|
+
|
|
235
|
+
for (const rawEntry of sourceEntries) {
|
|
236
|
+
const sourceEntry = typeof rawEntry === "string"
|
|
237
|
+
? { column: rawEntry }
|
|
238
|
+
: rawEntry;
|
|
239
|
+
if (!sourceEntry || typeof sourceEntry !== "object" || Array.isArray(sourceEntry)) {
|
|
240
|
+
throw new TypeError("crudRepository list.orderBy entries must be objects or column strings.");
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const column = normalizeText(sourceEntry.column);
|
|
244
|
+
if (!column) {
|
|
245
|
+
throw new TypeError("crudRepository list.orderBy entries require column.");
|
|
246
|
+
}
|
|
247
|
+
if (seenColumns.has(column)) {
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
seenColumns.add(column);
|
|
252
|
+
normalizedOrderBy.push(
|
|
253
|
+
Object.freeze({
|
|
254
|
+
column,
|
|
255
|
+
direction: normalizeListOrderDirection(sourceEntry.direction),
|
|
256
|
+
nulls: normalizeListOrderNulls(sourceEntry.nulls)
|
|
257
|
+
})
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (normalizedOrderBy.length > 0 && !seenColumns.has(normalizedIdColumn)) {
|
|
262
|
+
normalizedOrderBy.push(
|
|
263
|
+
Object.freeze({
|
|
264
|
+
column: normalizedIdColumn,
|
|
265
|
+
direction: normalizedOrderBy[normalizedOrderBy.length - 1].direction,
|
|
266
|
+
nulls: LIST_ORDER_NULLS_LAST
|
|
267
|
+
})
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return Object.freeze(normalizedOrderBy);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function resolveListRuntimeConfig(list = {}, fallbackSearchColumns = [], { idColumn = "id" } = {}) {
|
|
275
|
+
const parsedMaxLimit = Number(list?.maxLimit);
|
|
276
|
+
const normalizedMaxLimit = Number.isInteger(parsedMaxLimit) && parsedMaxLimit > 0
|
|
277
|
+
? parsedMaxLimit
|
|
278
|
+
: MAX_LIST_LIMIT;
|
|
279
|
+
const normalizedDefaultLimit = normalizeCrudListLimit(list?.defaultLimit, {
|
|
280
|
+
fallback: DEFAULT_LIST_LIMIT,
|
|
281
|
+
max: normalizedMaxLimit
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
return Object.freeze({
|
|
285
|
+
defaultLimit: normalizedDefaultLimit,
|
|
286
|
+
maxLimit: normalizedMaxLimit,
|
|
287
|
+
searchColumns: normalizeSearchColumns(list?.searchColumns, fallbackSearchColumns),
|
|
288
|
+
orderBy: normalizeListOrderBy(list?.orderBy, { idColumn })
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function formatOutputValidationError(issue = {}) {
|
|
293
|
+
const path = Array.isArray(issue.path) ? issue.path.join(".") : "";
|
|
294
|
+
const value = issue.value;
|
|
295
|
+
const message = normalizeText(issue.message) || "Invalid value";
|
|
296
|
+
if (path) {
|
|
297
|
+
return `${path}: ${message}`;
|
|
298
|
+
}
|
|
299
|
+
if (value !== undefined) {
|
|
300
|
+
return `${message} (${JSON.stringify(value)})`;
|
|
301
|
+
}
|
|
302
|
+
return message;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function resolveRecordOutputValidator(resource = {}, { context = "crudRepository" } = {}) {
|
|
306
|
+
const outputValidator = resource?.operations?.view?.outputValidator;
|
|
307
|
+
if (!outputValidator || typeof outputValidator !== "object" || Array.isArray(outputValidator)) {
|
|
308
|
+
throw new TypeError(`${context} requires operations.view.outputValidator.`);
|
|
309
|
+
}
|
|
310
|
+
if (!outputValidator.schema || typeof outputValidator.schema !== "object" || Array.isArray(outputValidator.schema)) {
|
|
311
|
+
throw new TypeError(`${context} requires operations.view.outputValidator.schema.`);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return Object.freeze({
|
|
315
|
+
schema: outputValidator.schema,
|
|
316
|
+
normalize: typeof outputValidator.normalize === "function" ? outputValidator.normalize : null
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function resolveOperationBodyValidator(resource = {}, operationKey = "", { context = "crudRepository" } = {}) {
|
|
321
|
+
const bodyValidator = resource?.operations?.[operationKey]?.bodyValidator;
|
|
322
|
+
if (bodyValidator == null) {
|
|
323
|
+
return Object.freeze({
|
|
324
|
+
normalize: null
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
if (!bodyValidator || typeof bodyValidator !== "object" || Array.isArray(bodyValidator)) {
|
|
328
|
+
throw new TypeError(`${context} operations.${operationKey}.bodyValidator must be an object when provided.`);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return Object.freeze({
|
|
332
|
+
normalize: typeof bodyValidator.normalize === "function" ? bodyValidator.normalize : null
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function extractExplicitFieldErrors(error) {
|
|
337
|
+
if (isRecord(error?.fieldErrors)) {
|
|
338
|
+
return error.fieldErrors;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (isRecord(error?.details?.fieldErrors)) {
|
|
342
|
+
return error.details.fieldErrors;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async function normalizeRepositoryInputPayload(
|
|
349
|
+
runtime = {},
|
|
350
|
+
payload = {},
|
|
351
|
+
{
|
|
352
|
+
operationKey = "create",
|
|
353
|
+
phase = "crudCreate",
|
|
354
|
+
action = "create",
|
|
355
|
+
recordId = null,
|
|
356
|
+
existingRecord = null,
|
|
357
|
+
actionContextBase = {}
|
|
358
|
+
} = {}
|
|
359
|
+
) {
|
|
360
|
+
const inputValidator = operationKey === "patch"
|
|
361
|
+
? runtime.input?.patch
|
|
362
|
+
: runtime.input?.create;
|
|
363
|
+
const normalizedPayload = normalizeCrudRepositoryObjectInput(payload);
|
|
364
|
+
|
|
365
|
+
if (typeof inputValidator?.normalize !== "function") {
|
|
366
|
+
return normalizedPayload;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
try {
|
|
370
|
+
const nextPayload = await inputValidator.normalize(normalizedPayload, {
|
|
371
|
+
phase,
|
|
372
|
+
action,
|
|
373
|
+
recordId,
|
|
374
|
+
existingRecord,
|
|
375
|
+
context: actionContextBase?.callOptions?.context,
|
|
376
|
+
callOptions: actionContextBase?.callOptions,
|
|
377
|
+
repositoryOptions: actionContextBase?.repositoryOptions
|
|
378
|
+
});
|
|
379
|
+
if (nextPayload === undefined) {
|
|
380
|
+
return normalizedPayload;
|
|
381
|
+
}
|
|
382
|
+
if (!nextPayload || typeof nextPayload !== "object" || Array.isArray(nextPayload)) {
|
|
383
|
+
throw new TypeError(
|
|
384
|
+
`${runtime?.context || "crudRepository"} operations.${operationKey}.bodyValidator.normalize must return an object when it returns a value.`
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
return nextPayload;
|
|
388
|
+
} catch (error) {
|
|
389
|
+
const explicitFieldErrors = extractExplicitFieldErrors(error);
|
|
390
|
+
if (explicitFieldErrors) {
|
|
391
|
+
throw createValidationError(explicitFieldErrors);
|
|
392
|
+
}
|
|
393
|
+
throw error;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
async function normalizeRepositoryOutputRecord(runtime = {}, record = {}, { operation = "list" } = {}) {
|
|
398
|
+
const outputRuntime = runtime.output;
|
|
399
|
+
let normalizedRecord = record;
|
|
400
|
+
if (typeof outputRuntime.normalize === "function") {
|
|
401
|
+
normalizedRecord = await outputRuntime.normalize(record, {
|
|
402
|
+
phase: "crudRepositoryOutput",
|
|
403
|
+
operation
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (Check(outputRuntime.schema, normalizedRecord)) {
|
|
408
|
+
return normalizedRecord;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const issues = [...Errors(outputRuntime.schema, normalizedRecord)];
|
|
412
|
+
const formattedIssue = formatOutputValidationError(issues[0]);
|
|
413
|
+
throw new TypeError(
|
|
414
|
+
`${runtime?.context || "crudRepository"} ${operation} output validation failed: ${formattedIssue}.`
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function encodeOrderedListCursorValue(value = null) {
|
|
419
|
+
if (value instanceof Date) {
|
|
420
|
+
return {
|
|
421
|
+
[ORDERED_LIST_CURSOR_VALUE_TYPE_KEY]: ORDERED_LIST_CURSOR_VALUE_TYPE_DATE,
|
|
422
|
+
[ORDERED_LIST_CURSOR_VALUE_KEY]: value.toISOString()
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return value === undefined ? null : value;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function decodeOrderedListCursorValue(value = null) {
|
|
430
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
431
|
+
return value === undefined ? null : value;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const valueType = normalizeText(value[ORDERED_LIST_CURSOR_VALUE_TYPE_KEY]).toLowerCase();
|
|
435
|
+
if (!valueType) {
|
|
436
|
+
return value;
|
|
437
|
+
}
|
|
438
|
+
if (valueType !== ORDERED_LIST_CURSOR_VALUE_TYPE_DATE) {
|
|
439
|
+
return value;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const normalizedValue = normalizeText(value[ORDERED_LIST_CURSOR_VALUE_KEY]);
|
|
443
|
+
if (!normalizedValue) {
|
|
444
|
+
throw new TypeError("Ordered list cursor date values require a non-empty value.");
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const date = new Date(normalizedValue);
|
|
448
|
+
if (Number.isNaN(date.getTime())) {
|
|
449
|
+
throw new TypeError("Ordered list cursor date values must be valid dates.");
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return date;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function encodeOrderedListCursor(row = null, orderBy = []) {
|
|
456
|
+
if (!row || typeof row !== "object" || Array.isArray(row)) {
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const normalizedOrderBy = Array.isArray(orderBy) ? orderBy : [];
|
|
461
|
+
if (normalizedOrderBy.length < 1) {
|
|
462
|
+
return null;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const values = normalizedOrderBy.map(({ column }) => (
|
|
466
|
+
Object.hasOwn(row, column) && row[column] !== undefined
|
|
467
|
+
? encodeOrderedListCursorValue(row[column])
|
|
468
|
+
: null
|
|
469
|
+
));
|
|
470
|
+
|
|
471
|
+
return Buffer.from(JSON.stringify({ values }), "utf8").toString("base64url");
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function decodeOrderedListCursor(cursor = "", orderBy = []) {
|
|
475
|
+
const normalizedCursor = normalizeText(cursor);
|
|
476
|
+
const normalizedOrderBy = Array.isArray(orderBy) ? orderBy : [];
|
|
477
|
+
if (!normalizedCursor || normalizedOrderBy.length < 1) {
|
|
478
|
+
return null;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
try {
|
|
482
|
+
const decoded = Buffer.from(normalizedCursor, "base64url").toString("utf8");
|
|
483
|
+
const payload = JSON.parse(decoded);
|
|
484
|
+
const values = Array.isArray(payload?.values) ? payload.values : null;
|
|
485
|
+
if (!values || values.length !== normalizedOrderBy.length) {
|
|
486
|
+
throw new AppError(400, "Invalid cursor.", {
|
|
487
|
+
code: "INVALID_CURSOR"
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
return values.map((value) => decodeOrderedListCursorValue(value));
|
|
492
|
+
} catch (error) {
|
|
493
|
+
if (error instanceof AppError) {
|
|
494
|
+
throw error;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
throw new AppError(400, "Invalid cursor.", {
|
|
498
|
+
code: "INVALID_CURSOR"
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function applyOrderedListCursorEquality(query, descriptor = {}, value = null) {
|
|
504
|
+
if (value === null) {
|
|
505
|
+
query.whereNull(descriptor.column);
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
query.where(descriptor.column, value);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function applyOrderedListCursorAfterBranch(query, descriptor = {}, value = null) {
|
|
513
|
+
const operator = descriptor.direction === LIST_ORDER_DIRECTION_DESC ? "<" : ">";
|
|
514
|
+
|
|
515
|
+
if (value === null) {
|
|
516
|
+
if (descriptor.nulls === LIST_ORDER_NULLS_FIRST) {
|
|
517
|
+
query.whereNotNull(descriptor.column);
|
|
518
|
+
return true;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
return false;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (descriptor.nulls === LIST_ORDER_NULLS_LAST) {
|
|
525
|
+
query.where((branchQuery) => {
|
|
526
|
+
branchQuery.where(descriptor.column, operator, value);
|
|
527
|
+
branchQuery.orWhereNull(descriptor.column);
|
|
528
|
+
});
|
|
529
|
+
return true;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
query.where(descriptor.column, operator, value);
|
|
533
|
+
return true;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function canApplyOrderedListCursorAfterBranch(descriptor = {}, value = null) {
|
|
537
|
+
return !(value === null && descriptor.nulls === LIST_ORDER_NULLS_LAST);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function appendOrderedListCursorBranches(query, orderBy = [], cursorValues = [], index = 0, { useOr = false } = {}) {
|
|
541
|
+
const descriptor = orderBy[index];
|
|
542
|
+
if (!descriptor) {
|
|
543
|
+
return false;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
let addedBranch = false;
|
|
547
|
+
const currentValue = cursorValues[index] ?? null;
|
|
548
|
+
|
|
549
|
+
if (canApplyOrderedListCursorAfterBranch(descriptor, currentValue)) {
|
|
550
|
+
const afterMethod = useOr === true ? "orWhere" : "where";
|
|
551
|
+
query[afterMethod]((afterQuery) => {
|
|
552
|
+
applyOrderedListCursorAfterBranch(afterQuery, descriptor, currentValue);
|
|
553
|
+
});
|
|
554
|
+
addedBranch = true;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
if (index >= orderBy.length - 1) {
|
|
558
|
+
return addedBranch;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const equalityMethod = useOr === true || addedBranch === true ? "orWhere" : "where";
|
|
562
|
+
query[equalityMethod]((equalQuery) => {
|
|
563
|
+
applyOrderedListCursorEquality(equalQuery, descriptor, currentValue);
|
|
564
|
+
equalQuery.where((nestedQuery) => {
|
|
565
|
+
appendOrderedListCursorBranches(nestedQuery, orderBy, cursorValues, index + 1);
|
|
566
|
+
});
|
|
567
|
+
});
|
|
568
|
+
return true;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function applyOrderedListCursorFilter(query, { orderBy = [], cursor = "" } = {}) {
|
|
572
|
+
const normalizedOrderBy = Array.isArray(orderBy) ? orderBy : [];
|
|
573
|
+
const cursorValues = decodeOrderedListCursor(cursor, normalizedOrderBy);
|
|
574
|
+
if (!cursorValues) {
|
|
575
|
+
return query;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
return query.where((cursorQuery) => {
|
|
579
|
+
appendOrderedListCursorBranches(cursorQuery, normalizedOrderBy, cursorValues);
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function normalizeCrudRepositoryOperationStage(stage = null, stageLabel = "", { context = "crudRepository" } = {}) {
|
|
584
|
+
if (stage === undefined || stage === null) {
|
|
585
|
+
return null;
|
|
586
|
+
}
|
|
587
|
+
if (typeof stage !== "function") {
|
|
588
|
+
throw new TypeError(`${context} ${stageLabel} must be a function when provided.`);
|
|
589
|
+
}
|
|
590
|
+
return stage;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function normalizeCrudRepositoryOperationConfig(config = {}, operationKey = "", { context = "crudRepository" } = {}) {
|
|
594
|
+
if (config === undefined || config === null) {
|
|
595
|
+
return Object.freeze({});
|
|
596
|
+
}
|
|
597
|
+
if (!config || typeof config !== "object" || Array.isArray(config)) {
|
|
598
|
+
throw new TypeError(`${context} operations.${operationKey} must be an object when provided.`);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const allowedKeys = REPOSITORY_OPERATION_STAGE_KEYS[operationKey] || [];
|
|
602
|
+
const normalized = {};
|
|
603
|
+
for (const [rawKey, rawValue] of Object.entries(config)) {
|
|
604
|
+
const key = normalizeText(rawKey);
|
|
605
|
+
if (!key) {
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
608
|
+
if (!allowedKeys.includes(key)) {
|
|
609
|
+
throw new TypeError(
|
|
610
|
+
`${context} operations.${operationKey}.${key} is not supported. Allowed keys: ${allowedKeys.join(", ")}.`
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
normalized[key] = normalizeCrudRepositoryOperationStage(
|
|
614
|
+
rawValue,
|
|
615
|
+
`operations.${operationKey}.${key}`,
|
|
616
|
+
{ context }
|
|
617
|
+
);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
return Object.freeze(normalized);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function normalizeCrudRepositoryOperations(sourceOperations = {}, { context = "crudRepository" } = {}) {
|
|
624
|
+
if (sourceOperations === undefined || sourceOperations === null) {
|
|
625
|
+
return Object.freeze({});
|
|
626
|
+
}
|
|
627
|
+
if (!sourceOperations || typeof sourceOperations !== "object" || Array.isArray(sourceOperations)) {
|
|
628
|
+
throw new TypeError(`${context} operations must be an object when provided.`);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const normalized = {};
|
|
632
|
+
for (const operationKey of REPOSITORY_OPERATION_KEYS) {
|
|
633
|
+
normalized[operationKey] = normalizeCrudRepositoryOperationConfig(
|
|
634
|
+
sourceOperations[operationKey],
|
|
635
|
+
operationKey,
|
|
636
|
+
{ context }
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
for (const rawKey of Object.keys(sourceOperations)) {
|
|
641
|
+
const key = normalizeText(rawKey);
|
|
642
|
+
if (key && !REPOSITORY_OPERATION_KEYS.includes(key)) {
|
|
643
|
+
throw new TypeError(
|
|
644
|
+
`${context} operations.${key} is not supported. Allowed keys: ${REPOSITORY_OPERATION_KEYS.join(", ")}.`
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
return Object.freeze(normalized);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function applyConfiguredQueryStage(
|
|
653
|
+
dbQuery,
|
|
654
|
+
stage = null,
|
|
655
|
+
stageContext = {},
|
|
656
|
+
{ context = "crudRepository", stageKey = "applyQuery" } = {}
|
|
657
|
+
) {
|
|
658
|
+
if (typeof stage !== "function") {
|
|
659
|
+
return {
|
|
660
|
+
queryBuilder: dbQuery
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const directResult = stage(dbQuery, stageContext);
|
|
665
|
+
if (directResult === undefined || directResult === dbQuery) {
|
|
666
|
+
return {
|
|
667
|
+
queryBuilder: dbQuery
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
if (isCrudRepositoryQueryBuilder(directResult)) {
|
|
672
|
+
return {
|
|
673
|
+
queryBuilder: directResult
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
if (directResult && typeof directResult.then === "function") {
|
|
678
|
+
return Promise.resolve(directResult).then((awaitedResult) => {
|
|
679
|
+
if (awaitedResult === undefined || awaitedResult === dbQuery) {
|
|
680
|
+
return {
|
|
681
|
+
queryBuilder: dbQuery
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
if (isCrudRepositoryQueryBuilder(awaitedResult)) {
|
|
686
|
+
throw new TypeError(
|
|
687
|
+
`${context} ${stageKey} cannot return a query builder asynchronously. Mutate the provided builder and return undefined instead.`
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
throw new TypeError(
|
|
692
|
+
`${context} ${stageKey} must return undefined, the provided query builder, or another query builder synchronously.`
|
|
693
|
+
);
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
return Promise.reject(new TypeError(
|
|
698
|
+
`${context} ${stageKey} must return undefined, the provided query builder, or another query builder synchronously.`
|
|
699
|
+
));
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function isCrudRepositoryQueryBuilder(value) {
|
|
703
|
+
if (!value || (typeof value !== "object" && typeof value !== "function") || Array.isArray(value)) {
|
|
704
|
+
return false;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
return [
|
|
708
|
+
"where",
|
|
709
|
+
"whereIn",
|
|
710
|
+
"select",
|
|
711
|
+
"insert",
|
|
712
|
+
"update",
|
|
713
|
+
"delete",
|
|
714
|
+
"del",
|
|
715
|
+
"first",
|
|
716
|
+
"modify"
|
|
717
|
+
].some((key) => typeof value[key] === "function");
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function normalizeCrudRepositoryObjectInput(value = {}) {
|
|
721
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
722
|
+
return {};
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
return value;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
async function applyConfiguredObjectStage(
|
|
729
|
+
payload = {},
|
|
730
|
+
stage = null,
|
|
731
|
+
stageContext = {},
|
|
732
|
+
{ context = "crudRepository", stageKey = "preparePayload" } = {}
|
|
733
|
+
) {
|
|
734
|
+
const normalizedPayload = normalizeCrudRepositoryObjectInput(payload);
|
|
735
|
+
if (typeof stage !== "function") {
|
|
736
|
+
return normalizedPayload;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
const nextPayload = await stage(normalizedPayload, stageContext);
|
|
740
|
+
if (nextPayload === undefined) {
|
|
741
|
+
return normalizedPayload;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
if (!nextPayload || typeof nextPayload !== "object" || Array.isArray(nextPayload)) {
|
|
745
|
+
throw new TypeError(`${context} ${stageKey} must return an object when it returns a value.`);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
return nextPayload;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function applyOrderedListControls(dbQuery, orderBy = []) {
|
|
752
|
+
let nextQuery = dbQuery;
|
|
753
|
+
for (const descriptor of Array.isArray(orderBy) ? orderBy : []) {
|
|
754
|
+
if (typeof nextQuery.orderByRaw === "function") {
|
|
755
|
+
nextQuery = nextQuery.orderByRaw(
|
|
756
|
+
descriptor.nulls === LIST_ORDER_NULLS_FIRST
|
|
757
|
+
? "?? is null desc"
|
|
758
|
+
: "?? is null asc",
|
|
759
|
+
[descriptor.column]
|
|
760
|
+
);
|
|
761
|
+
}
|
|
762
|
+
nextQuery = nextQuery.orderBy(descriptor.column, descriptor.direction);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
return nextQuery;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
function enforceCrudRepositoryListControls(
|
|
769
|
+
dbQuery,
|
|
770
|
+
{
|
|
771
|
+
idColumn = "id",
|
|
772
|
+
limit = DEFAULT_LIST_LIMIT + 1,
|
|
773
|
+
orderBy = []
|
|
774
|
+
} = {}
|
|
775
|
+
) {
|
|
776
|
+
let nextQuery = dbQuery;
|
|
777
|
+
if (typeof nextQuery.clearOrder === "function") {
|
|
778
|
+
nextQuery = nextQuery.clearOrder();
|
|
779
|
+
}
|
|
780
|
+
if (typeof nextQuery.clear === "function") {
|
|
781
|
+
nextQuery = nextQuery.clear("limit");
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
const normalizedOrderBy = Array.isArray(orderBy) ? orderBy : [];
|
|
785
|
+
if (normalizedOrderBy.length > 0) {
|
|
786
|
+
return applyOrderedListControls(nextQuery, normalizedOrderBy)
|
|
787
|
+
.limit(limit);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
return nextQuery
|
|
791
|
+
.orderBy(idColumn, "asc")
|
|
792
|
+
.limit(limit);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function createCrudRepositoryActionContextBase(runtime, callOptions = {}) {
|
|
796
|
+
return {
|
|
797
|
+
runtime,
|
|
798
|
+
repositoryOptions: runtime.repositoryOptions,
|
|
799
|
+
callOptions,
|
|
800
|
+
state: {}
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
function createCompiledCrudRepositoryRuntime(resource = {}, repositoryOptions = {}) {
|
|
805
|
+
const sourceOptions = requireCrudRepositoryOptions(repositoryOptions, {
|
|
806
|
+
context: "createCrudResourceRuntime"
|
|
807
|
+
});
|
|
808
|
+
const context = normalizeText(sourceOptions.context) || "crudRepository";
|
|
809
|
+
const repositoryMapping = deriveRepositoryMappingFromResource(resource, { context });
|
|
810
|
+
const defaults = resolveRepositoryDefaults(resource, repositoryMapping, { context });
|
|
811
|
+
const output = resolveRecordOutputValidator(resource, { context });
|
|
812
|
+
const lookupRuntime = createCrudLookupRuntime(resource, {
|
|
813
|
+
outputKeys: repositoryMapping.outputKeys
|
|
814
|
+
});
|
|
815
|
+
const listRuntime = resolveListRuntimeConfig(sourceOptions.list, repositoryMapping.listSearchColumns, {
|
|
816
|
+
idColumn: defaults.idColumn
|
|
817
|
+
});
|
|
818
|
+
const { selectColumns } = buildRepositoryColumnMetadata({
|
|
819
|
+
outputKeys: repositoryMapping.columnBackedOutputKeys,
|
|
820
|
+
writeKeys: repositoryMapping.writeKeys,
|
|
821
|
+
columnOverrides: repositoryMapping.columnOverrides,
|
|
822
|
+
fieldStorageByKey: repositoryMapping.fieldStorageByKey
|
|
823
|
+
});
|
|
824
|
+
const normalizedSelectColumns = Object.freeze(
|
|
825
|
+
[...new Set([
|
|
826
|
+
...selectColumns,
|
|
827
|
+
...listRuntime.orderBy.map(({ column }) => column)
|
|
828
|
+
])]
|
|
829
|
+
);
|
|
830
|
+
|
|
831
|
+
return Object.freeze({
|
|
832
|
+
context,
|
|
833
|
+
resource,
|
|
834
|
+
repositoryOptions: sourceOptions,
|
|
835
|
+
defaults,
|
|
836
|
+
input: Object.freeze({
|
|
837
|
+
create: resolveOperationBodyValidator(resource, "create", { context }),
|
|
838
|
+
patch: resolveOperationBodyValidator(resource, "patch", { context })
|
|
839
|
+
}),
|
|
840
|
+
selectColumns: normalizedSelectColumns,
|
|
841
|
+
output,
|
|
842
|
+
list: listRuntime,
|
|
843
|
+
lookup: lookupRuntime,
|
|
844
|
+
mapping: repositoryMapping,
|
|
845
|
+
operations: normalizeCrudRepositoryOperations(sourceOptions.operations, { context }),
|
|
846
|
+
virtualFields: normalizeCrudVirtualFieldHandlers(sourceOptions.virtualFields, repositoryMapping, { context })
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function resolveCrudRepositoryCall(runtime, knex, callOptions = {}) {
|
|
851
|
+
const client = callOptions?.trx || knex;
|
|
852
|
+
const tableName = requireCrudTableName(runtime.repositoryOptions?.tableName ?? runtime.defaults?.tableName, {
|
|
853
|
+
context: runtime.context || "crudRepository"
|
|
854
|
+
});
|
|
855
|
+
const idColumn = resolveCrudIdColumn(runtime.repositoryOptions?.idColumn, {
|
|
856
|
+
fallback: runtime.defaults?.idColumn || "id"
|
|
857
|
+
});
|
|
858
|
+
const visible = (queryBuilder) => applyVisibility(queryBuilder, callOptions.visibilityContext);
|
|
859
|
+
|
|
860
|
+
return Object.freeze({
|
|
861
|
+
client,
|
|
862
|
+
tableName,
|
|
863
|
+
idColumn,
|
|
864
|
+
visible
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
function applyCrudRepositoryReadLock(dbQuery, callOptions = {}) {
|
|
869
|
+
if (callOptions?.forUpdate === true && typeof dbQuery.forUpdate === "function") {
|
|
870
|
+
return dbQuery.forUpdate();
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
return dbQuery;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
async function listRecords(runtime, knex, query = {}, callOptions = {}) {
|
|
877
|
+
const { client, tableName, idColumn, visible } = resolveCrudRepositoryCall(runtime, knex, callOptions);
|
|
878
|
+
const normalizedLimit = normalizeCrudListLimit(query?.limit, {
|
|
879
|
+
fallback: runtime.list.defaultLimit,
|
|
880
|
+
max: runtime.list.maxLimit
|
|
881
|
+
});
|
|
882
|
+
const actionContextBase = createCrudRepositoryActionContextBase(runtime, callOptions);
|
|
883
|
+
const usesOrderedListCursor = runtime.list.orderBy.length > 0;
|
|
884
|
+
let dbQuery = client(tableName)
|
|
885
|
+
.select(...runtime.selectColumns);
|
|
886
|
+
dbQuery = applyCrudRepositoryVirtualProjections(dbQuery, runtime, {
|
|
887
|
+
knex: client,
|
|
888
|
+
tableName
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
dbQuery = applyCrudListQueryFilters(dbQuery, {
|
|
892
|
+
idColumn,
|
|
893
|
+
cursor: query?.cursor,
|
|
894
|
+
applyCursor: usesOrderedListCursor !== true,
|
|
895
|
+
q: query?.q,
|
|
896
|
+
searchColumns: runtime.list.searchColumns,
|
|
897
|
+
parentFilters: query,
|
|
898
|
+
parentFilterColumns: runtime.mapping.parentFilterColumns
|
|
899
|
+
});
|
|
900
|
+
if (usesOrderedListCursor) {
|
|
901
|
+
dbQuery = applyOrderedListCursorFilter(dbQuery, {
|
|
902
|
+
orderBy: runtime.list.orderBy,
|
|
903
|
+
cursor: query?.cursor
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
const readListStageResult = await applyConfiguredQueryStage(
|
|
908
|
+
dbQuery,
|
|
909
|
+
runtime.operations?.read?.applyQuery,
|
|
910
|
+
{
|
|
911
|
+
action: "list",
|
|
912
|
+
query,
|
|
913
|
+
...actionContextBase
|
|
914
|
+
},
|
|
915
|
+
{
|
|
916
|
+
context: runtime.context,
|
|
917
|
+
stageKey: "operations.read.applyQuery"
|
|
918
|
+
}
|
|
919
|
+
);
|
|
920
|
+
dbQuery = readListStageResult.queryBuilder;
|
|
921
|
+
const listStageResult = await applyConfiguredQueryStage(
|
|
922
|
+
dbQuery,
|
|
923
|
+
runtime.operations?.list?.applyQuery,
|
|
924
|
+
{
|
|
925
|
+
action: "list",
|
|
926
|
+
query,
|
|
927
|
+
...actionContextBase
|
|
928
|
+
},
|
|
929
|
+
{
|
|
930
|
+
context: runtime.context,
|
|
931
|
+
stageKey: "operations.list.applyQuery"
|
|
932
|
+
}
|
|
933
|
+
);
|
|
934
|
+
dbQuery = listStageResult.queryBuilder;
|
|
935
|
+
|
|
936
|
+
dbQuery = dbQuery.where(visible);
|
|
937
|
+
dbQuery = applyCrudRepositoryReadLock(dbQuery, callOptions);
|
|
938
|
+
dbQuery = enforceCrudRepositoryListControls(dbQuery, {
|
|
939
|
+
idColumn,
|
|
940
|
+
limit: normalizedLimit + 1,
|
|
941
|
+
orderBy: runtime.list.orderBy
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
const rows = await dbQuery;
|
|
945
|
+
const hasMore = rows.length > normalizedLimit;
|
|
946
|
+
const pageRows = hasMore ? rows.slice(0, normalizedLimit) : rows;
|
|
947
|
+
const items = [];
|
|
948
|
+
for (const row of pageRows) {
|
|
949
|
+
const mappedRecord = mapRecordRow(
|
|
950
|
+
row,
|
|
951
|
+
runtime.mapping.outputKeys,
|
|
952
|
+
runtime.mapping.columnOverrides,
|
|
953
|
+
{ recordIdKeys: runtime.mapping.outputRecordIdKeys }
|
|
954
|
+
);
|
|
955
|
+
if (!mappedRecord) {
|
|
956
|
+
continue;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
items.push(await normalizeRepositoryOutputRecord(runtime, mappedRecord, {
|
|
960
|
+
operation: "list"
|
|
961
|
+
}));
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
const hydratedItems = await hydrateCrudLookupRecords(items, {
|
|
965
|
+
...runtime.lookup,
|
|
966
|
+
context: runtime.context
|
|
967
|
+
}, {
|
|
968
|
+
include: callOptions?.include,
|
|
969
|
+
mode: "list",
|
|
970
|
+
repositoryOptions: runtime.repositoryOptions,
|
|
971
|
+
callOptions
|
|
972
|
+
});
|
|
973
|
+
|
|
974
|
+
const lastPageRow = pageRows[pageRows.length - 1] || null;
|
|
975
|
+
const nextCursor = hasMore && lastPageRow
|
|
976
|
+
? (
|
|
977
|
+
usesOrderedListCursor
|
|
978
|
+
? encodeOrderedListCursor(lastPageRow, runtime.list.orderBy)
|
|
979
|
+
: String(lastPageRow[idColumn])
|
|
980
|
+
)
|
|
981
|
+
: null;
|
|
982
|
+
|
|
983
|
+
return {
|
|
984
|
+
items: hydratedItems,
|
|
985
|
+
nextCursor
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
async function findRecordById(runtime, knex, recordId, callOptions = {}) {
|
|
990
|
+
const { client, tableName, idColumn, visible } = resolveCrudRepositoryCall(runtime, knex, callOptions);
|
|
991
|
+
const normalizedRecordId = requireCrudRecordId(recordId, { context: "crudRepository.findById" });
|
|
992
|
+
const actionContextBase = createCrudRepositoryActionContextBase(runtime, callOptions);
|
|
993
|
+
let dbQuery = client(tableName)
|
|
994
|
+
.select(...runtime.selectColumns);
|
|
995
|
+
dbQuery = applyCrudRepositoryVirtualProjections(dbQuery, runtime, {
|
|
996
|
+
knex: client,
|
|
997
|
+
tableName
|
|
998
|
+
});
|
|
999
|
+
|
|
1000
|
+
const readFindStageResult = await applyConfiguredQueryStage(
|
|
1001
|
+
dbQuery,
|
|
1002
|
+
runtime.operations?.read?.applyQuery,
|
|
1003
|
+
{
|
|
1004
|
+
action: "findById",
|
|
1005
|
+
recordId,
|
|
1006
|
+
...actionContextBase
|
|
1007
|
+
},
|
|
1008
|
+
{
|
|
1009
|
+
context: runtime.context,
|
|
1010
|
+
stageKey: "operations.read.applyQuery"
|
|
1011
|
+
}
|
|
1012
|
+
);
|
|
1013
|
+
dbQuery = readFindStageResult.queryBuilder;
|
|
1014
|
+
const findStageResult = await applyConfiguredQueryStage(
|
|
1015
|
+
dbQuery,
|
|
1016
|
+
runtime.operations?.findById?.applyQuery,
|
|
1017
|
+
{
|
|
1018
|
+
action: "findById",
|
|
1019
|
+
recordId,
|
|
1020
|
+
...actionContextBase
|
|
1021
|
+
},
|
|
1022
|
+
{
|
|
1023
|
+
context: runtime.context,
|
|
1024
|
+
stageKey: "operations.findById.applyQuery"
|
|
1025
|
+
}
|
|
1026
|
+
);
|
|
1027
|
+
dbQuery = findStageResult.queryBuilder;
|
|
1028
|
+
|
|
1029
|
+
dbQuery = dbQuery
|
|
1030
|
+
.where(visible)
|
|
1031
|
+
.where({
|
|
1032
|
+
[idColumn]: normalizedRecordId
|
|
1033
|
+
});
|
|
1034
|
+
dbQuery = applyCrudRepositoryReadLock(dbQuery, callOptions);
|
|
1035
|
+
|
|
1036
|
+
const row = await dbQuery.first();
|
|
1037
|
+
|
|
1038
|
+
const mappedRecord = mapRecordRow(
|
|
1039
|
+
row,
|
|
1040
|
+
runtime.mapping.outputKeys,
|
|
1041
|
+
runtime.mapping.columnOverrides,
|
|
1042
|
+
{ recordIdKeys: runtime.mapping.outputRecordIdKeys }
|
|
1043
|
+
);
|
|
1044
|
+
if (!mappedRecord) {
|
|
1045
|
+
return null;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
const normalizedRecord = await normalizeRepositoryOutputRecord(runtime, mappedRecord, {
|
|
1049
|
+
operation: "findById"
|
|
1050
|
+
});
|
|
1051
|
+
const hydrated = await hydrateCrudLookupRecords([normalizedRecord], {
|
|
1052
|
+
...runtime.lookup,
|
|
1053
|
+
context: runtime.context
|
|
1054
|
+
}, {
|
|
1055
|
+
include: callOptions?.include,
|
|
1056
|
+
mode: "view",
|
|
1057
|
+
repositoryOptions: runtime.repositoryOptions,
|
|
1058
|
+
callOptions
|
|
1059
|
+
});
|
|
1060
|
+
|
|
1061
|
+
return hydrated[0] || null;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
async function listRecordsByIds(runtime, knex, ids = [], callOptions = {}) {
|
|
1065
|
+
const { client, tableName, visible } = resolveCrudRepositoryCall(runtime, knex, callOptions);
|
|
1066
|
+
const actionContextBase = createCrudRepositoryActionContextBase(runtime, callOptions);
|
|
1067
|
+
const lookupValueKey = normalizeText(callOptions?.valueKey) || "id";
|
|
1068
|
+
if (!runtime.mapping.outputKeys.includes(lookupValueKey)) {
|
|
1069
|
+
throw new TypeError(
|
|
1070
|
+
`${runtime.context || "crudRepository"} listByIds requires valueKey "${lookupValueKey}" to exist in output schema.`
|
|
1071
|
+
);
|
|
1072
|
+
}
|
|
1073
|
+
if (runtime.mapping.fieldStorageByKey?.[lookupValueKey] !== CRUD_FIELD_REPOSITORY_STORAGE_COLUMN) {
|
|
1074
|
+
throw new TypeError(
|
|
1075
|
+
`${runtime.context || "crudRepository"} listByIds requires valueKey "${lookupValueKey}" to be column-backed.`
|
|
1076
|
+
);
|
|
1077
|
+
}
|
|
1078
|
+
const lookupColumn = resolveColumnName(lookupValueKey, runtime.mapping.columnOverrides);
|
|
1079
|
+
if (!lookupColumn) {
|
|
1080
|
+
throw new TypeError(`${runtime.context || "crudRepository"} listByIds requires a valid valueKey.`);
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
const normalizedIds = Array.isArray(ids)
|
|
1084
|
+
? [...new Set(ids.map((value) => normalizeText(value)).filter(Boolean))]
|
|
1085
|
+
: [];
|
|
1086
|
+
if (normalizedIds.length < 1) {
|
|
1087
|
+
return [];
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
let dbQuery = client(tableName)
|
|
1091
|
+
.select(...runtime.selectColumns);
|
|
1092
|
+
dbQuery = applyCrudRepositoryVirtualProjections(dbQuery, runtime, {
|
|
1093
|
+
knex: client,
|
|
1094
|
+
tableName
|
|
1095
|
+
});
|
|
1096
|
+
|
|
1097
|
+
const readListByIdsStageResult = await applyConfiguredQueryStage(
|
|
1098
|
+
dbQuery,
|
|
1099
|
+
runtime.operations?.read?.applyQuery,
|
|
1100
|
+
{
|
|
1101
|
+
action: "listByIds",
|
|
1102
|
+
ids: normalizedIds,
|
|
1103
|
+
lookupValueKey,
|
|
1104
|
+
lookupColumn,
|
|
1105
|
+
...actionContextBase
|
|
1106
|
+
},
|
|
1107
|
+
{
|
|
1108
|
+
context: runtime.context,
|
|
1109
|
+
stageKey: "operations.read.applyQuery"
|
|
1110
|
+
}
|
|
1111
|
+
);
|
|
1112
|
+
dbQuery = readListByIdsStageResult.queryBuilder;
|
|
1113
|
+
const listByIdsStageResult = await applyConfiguredQueryStage(
|
|
1114
|
+
dbQuery,
|
|
1115
|
+
runtime.operations?.listByIds?.applyQuery,
|
|
1116
|
+
{
|
|
1117
|
+
action: "listByIds",
|
|
1118
|
+
ids: normalizedIds,
|
|
1119
|
+
lookupValueKey,
|
|
1120
|
+
lookupColumn,
|
|
1121
|
+
...actionContextBase
|
|
1122
|
+
},
|
|
1123
|
+
{
|
|
1124
|
+
context: runtime.context,
|
|
1125
|
+
stageKey: "operations.listByIds.applyQuery"
|
|
1126
|
+
}
|
|
1127
|
+
);
|
|
1128
|
+
dbQuery = listByIdsStageResult.queryBuilder;
|
|
1129
|
+
|
|
1130
|
+
dbQuery = dbQuery
|
|
1131
|
+
.where(visible)
|
|
1132
|
+
.whereIn(lookupColumn, normalizedIds);
|
|
1133
|
+
dbQuery = applyCrudRepositoryReadLock(dbQuery, callOptions);
|
|
1134
|
+
|
|
1135
|
+
const rows = await dbQuery;
|
|
1136
|
+
|
|
1137
|
+
const records = [];
|
|
1138
|
+
for (const row of rows) {
|
|
1139
|
+
const mappedRecord = mapRecordRow(
|
|
1140
|
+
row,
|
|
1141
|
+
runtime.mapping.outputKeys,
|
|
1142
|
+
runtime.mapping.columnOverrides,
|
|
1143
|
+
{ recordIdKeys: runtime.mapping.outputRecordIdKeys }
|
|
1144
|
+
);
|
|
1145
|
+
if (!mappedRecord) {
|
|
1146
|
+
continue;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
records.push(await normalizeRepositoryOutputRecord(runtime, mappedRecord, {
|
|
1150
|
+
operation: "listByIds"
|
|
1151
|
+
}));
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
const lookupInclude = callOptions?.include === undefined ? "none" : callOptions.include;
|
|
1155
|
+
return hydrateCrudLookupRecords(records, {
|
|
1156
|
+
...runtime.lookup,
|
|
1157
|
+
context: runtime.context
|
|
1158
|
+
}, {
|
|
1159
|
+
include: lookupInclude,
|
|
1160
|
+
mode: "list",
|
|
1161
|
+
repositoryOptions: runtime.repositoryOptions,
|
|
1162
|
+
callOptions
|
|
1163
|
+
});
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
async function createRecord(runtime, knex, payload = {}, callOptions = {}) {
|
|
1167
|
+
const { client, tableName } = resolveCrudRepositoryCall(runtime, knex, callOptions);
|
|
1168
|
+
const actionContextBase = createCrudRepositoryActionContextBase(runtime, callOptions);
|
|
1169
|
+
let sourcePayload = await normalizeRepositoryInputPayload(runtime, payload, {
|
|
1170
|
+
operationKey: "create",
|
|
1171
|
+
phase: "crudCreate",
|
|
1172
|
+
action: "create",
|
|
1173
|
+
actionContextBase
|
|
1174
|
+
});
|
|
1175
|
+
sourcePayload = await applyConfiguredObjectStage(
|
|
1176
|
+
sourcePayload,
|
|
1177
|
+
runtime.operations?.create?.preparePayload,
|
|
1178
|
+
{
|
|
1179
|
+
action: "create",
|
|
1180
|
+
payload: sourcePayload,
|
|
1181
|
+
...actionContextBase
|
|
1182
|
+
},
|
|
1183
|
+
{
|
|
1184
|
+
context: runtime.context,
|
|
1185
|
+
stageKey: "operations.create.preparePayload"
|
|
1186
|
+
}
|
|
1187
|
+
);
|
|
1188
|
+
|
|
1189
|
+
let insertPayload = buildWritePayload(sourcePayload, runtime.mapping.writeKeys, runtime.mapping.columnOverrides);
|
|
1190
|
+
const timestamp = toInsertDateTime();
|
|
1191
|
+
if (runtime.defaults.createdAtColumn && !Object.hasOwn(insertPayload, runtime.defaults.createdAtColumn)) {
|
|
1192
|
+
insertPayload[runtime.defaults.createdAtColumn] = timestamp;
|
|
1193
|
+
}
|
|
1194
|
+
if (runtime.defaults.updatedAtColumn && !Object.hasOwn(insertPayload, runtime.defaults.updatedAtColumn)) {
|
|
1195
|
+
insertPayload[runtime.defaults.updatedAtColumn] = timestamp;
|
|
1196
|
+
}
|
|
1197
|
+
insertPayload = await applyConfiguredObjectStage(
|
|
1198
|
+
insertPayload,
|
|
1199
|
+
runtime.operations?.create?.prepareInsertPayload,
|
|
1200
|
+
{
|
|
1201
|
+
action: "create",
|
|
1202
|
+
payload: sourcePayload,
|
|
1203
|
+
insertPayload: {
|
|
1204
|
+
...insertPayload
|
|
1205
|
+
},
|
|
1206
|
+
...actionContextBase
|
|
1207
|
+
},
|
|
1208
|
+
{
|
|
1209
|
+
context: runtime.context,
|
|
1210
|
+
stageKey: "operations.create.prepareInsertPayload"
|
|
1211
|
+
}
|
|
1212
|
+
);
|
|
1213
|
+
|
|
1214
|
+
const withOwners = applyVisibilityOwners(insertPayload, callOptions.visibilityContext);
|
|
1215
|
+
let createQuery = client(tableName);
|
|
1216
|
+
const createStageResult = await applyConfiguredQueryStage(
|
|
1217
|
+
createQuery,
|
|
1218
|
+
runtime.operations?.create?.applyQuery,
|
|
1219
|
+
{
|
|
1220
|
+
action: "create",
|
|
1221
|
+
payload: {
|
|
1222
|
+
...withOwners
|
|
1223
|
+
},
|
|
1224
|
+
...actionContextBase
|
|
1225
|
+
},
|
|
1226
|
+
{
|
|
1227
|
+
context: runtime.context,
|
|
1228
|
+
stageKey: "operations.create.applyQuery"
|
|
1229
|
+
}
|
|
1230
|
+
);
|
|
1231
|
+
createQuery = createStageResult.queryBuilder;
|
|
1232
|
+
|
|
1233
|
+
const insertResult = await createQuery.insert(withOwners);
|
|
1234
|
+
const recordId = resolveInsertedRecordId(insertResult, { fallback: null });
|
|
1235
|
+
if (!recordId) {
|
|
1236
|
+
throw new Error("crudRepository.create could not resolve inserted id.");
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
return findRecordById(runtime, knex, recordId, {
|
|
1240
|
+
...callOptions,
|
|
1241
|
+
trx: client,
|
|
1242
|
+
sourceOperation: "create"
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
async function updateRecordById(runtime, knex, recordId, patch = {}, callOptions = {}) {
|
|
1247
|
+
const { client, tableName, idColumn, visible } = resolveCrudRepositoryCall(runtime, knex, callOptions);
|
|
1248
|
+
const normalizedRecordId = requireCrudRecordId(recordId, { context: "crudRepository.updateById" });
|
|
1249
|
+
const actionContextBase = createCrudRepositoryActionContextBase(runtime, callOptions);
|
|
1250
|
+
const existingRecord = Object.hasOwn(callOptions, "existingRecord")
|
|
1251
|
+
? callOptions.existingRecord
|
|
1252
|
+
: await findRecordById(runtime, knex, normalizedRecordId, {
|
|
1253
|
+
...callOptions,
|
|
1254
|
+
trx: client,
|
|
1255
|
+
sourceOperation: "update"
|
|
1256
|
+
});
|
|
1257
|
+
let sourcePatch = await normalizeRepositoryInputPayload(runtime, patch, {
|
|
1258
|
+
operationKey: "patch",
|
|
1259
|
+
phase: "crudPatch",
|
|
1260
|
+
action: "update",
|
|
1261
|
+
recordId: normalizedRecordId,
|
|
1262
|
+
existingRecord,
|
|
1263
|
+
actionContextBase
|
|
1264
|
+
});
|
|
1265
|
+
sourcePatch = await applyConfiguredObjectStage(
|
|
1266
|
+
sourcePatch,
|
|
1267
|
+
runtime.operations?.updateById?.preparePatch,
|
|
1268
|
+
{
|
|
1269
|
+
action: "updateById",
|
|
1270
|
+
recordId,
|
|
1271
|
+
patch: sourcePatch,
|
|
1272
|
+
...actionContextBase
|
|
1273
|
+
},
|
|
1274
|
+
{
|
|
1275
|
+
context: runtime.context,
|
|
1276
|
+
stageKey: "operations.updateById.preparePatch"
|
|
1277
|
+
}
|
|
1278
|
+
);
|
|
1279
|
+
const dbPatch = buildWritePayload(sourcePatch, runtime.mapping.writeKeys, runtime.mapping.columnOverrides);
|
|
1280
|
+
|
|
1281
|
+
if (runtime.defaults.updatedAtColumn) {
|
|
1282
|
+
dbPatch[runtime.defaults.updatedAtColumn] = toInsertDateTime();
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
if (Object.keys(dbPatch).length < 1) {
|
|
1286
|
+
return findRecordById(runtime, knex, recordId, {
|
|
1287
|
+
...callOptions,
|
|
1288
|
+
trx: client,
|
|
1289
|
+
sourceOperation: "update"
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
let updateQuery = client(tableName);
|
|
1294
|
+
const updateStageResult = await applyConfiguredQueryStage(
|
|
1295
|
+
updateQuery,
|
|
1296
|
+
runtime.operations?.updateById?.applyQuery,
|
|
1297
|
+
{
|
|
1298
|
+
action: "updateById",
|
|
1299
|
+
recordId,
|
|
1300
|
+
patch: {
|
|
1301
|
+
...dbPatch
|
|
1302
|
+
},
|
|
1303
|
+
...actionContextBase
|
|
1304
|
+
},
|
|
1305
|
+
{
|
|
1306
|
+
context: runtime.context,
|
|
1307
|
+
stageKey: "operations.updateById.applyQuery"
|
|
1308
|
+
}
|
|
1309
|
+
);
|
|
1310
|
+
updateQuery = updateStageResult.queryBuilder;
|
|
1311
|
+
|
|
1312
|
+
updateQuery = updateQuery
|
|
1313
|
+
.where(visible)
|
|
1314
|
+
.where({
|
|
1315
|
+
[idColumn]: normalizedRecordId
|
|
1316
|
+
});
|
|
1317
|
+
|
|
1318
|
+
await updateQuery.update(dbPatch);
|
|
1319
|
+
|
|
1320
|
+
return findRecordById(runtime, knex, recordId, {
|
|
1321
|
+
...callOptions,
|
|
1322
|
+
trx: client,
|
|
1323
|
+
sourceOperation: "update"
|
|
1324
|
+
});
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
async function deleteRecordById(runtime, knex, recordId, callOptions = {}) {
|
|
1328
|
+
const { client, tableName, idColumn, visible } = resolveCrudRepositoryCall(runtime, knex, callOptions);
|
|
1329
|
+
const normalizedRecordId = requireCrudRecordId(recordId, { context: "crudRepository.deleteById" });
|
|
1330
|
+
const actionContextBase = createCrudRepositoryActionContextBase(runtime, callOptions);
|
|
1331
|
+
const existing = await findRecordById(runtime, knex, recordId, {
|
|
1332
|
+
...callOptions,
|
|
1333
|
+
include: "none",
|
|
1334
|
+
trx: client
|
|
1335
|
+
});
|
|
1336
|
+
if (!existing) {
|
|
1337
|
+
return null;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
let deleteQuery = client(tableName);
|
|
1341
|
+
const deleteStageResult = await applyConfiguredQueryStage(
|
|
1342
|
+
deleteQuery,
|
|
1343
|
+
runtime.operations?.deleteById?.applyQuery,
|
|
1344
|
+
{
|
|
1345
|
+
action: "deleteById",
|
|
1346
|
+
recordId,
|
|
1347
|
+
existing,
|
|
1348
|
+
...actionContextBase
|
|
1349
|
+
},
|
|
1350
|
+
{
|
|
1351
|
+
context: runtime.context,
|
|
1352
|
+
stageKey: "operations.deleteById.applyQuery"
|
|
1353
|
+
}
|
|
1354
|
+
);
|
|
1355
|
+
deleteQuery = deleteStageResult.queryBuilder;
|
|
1356
|
+
|
|
1357
|
+
deleteQuery = deleteQuery
|
|
1358
|
+
.where(visible)
|
|
1359
|
+
.where({
|
|
1360
|
+
[idColumn]: normalizedRecordId
|
|
1361
|
+
});
|
|
1362
|
+
|
|
1363
|
+
await deleteQuery.delete();
|
|
1364
|
+
|
|
1365
|
+
return {
|
|
1366
|
+
id: existing.id,
|
|
1367
|
+
deleted: true
|
|
1368
|
+
};
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
function createCrudResourceRuntime(resource = {}, knex, repositoryOptions = {}) {
|
|
1372
|
+
if (typeof knex !== "function") {
|
|
1373
|
+
throw new TypeError("createCrudResourceRuntime requires knex.");
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
const runtime = createCompiledCrudRepositoryRuntime(resource, repositoryOptions);
|
|
1377
|
+
const withTransaction = createWithTransaction(knex);
|
|
1378
|
+
|
|
1379
|
+
return Object.freeze({
|
|
1380
|
+
withTransaction,
|
|
1381
|
+
async list(query = {}, callOptions = {}) {
|
|
1382
|
+
return listRecords(runtime, knex, query, callOptions);
|
|
1383
|
+
},
|
|
1384
|
+
async findById(recordId, callOptions = {}) {
|
|
1385
|
+
return findRecordById(runtime, knex, recordId, callOptions);
|
|
1386
|
+
},
|
|
1387
|
+
async listByIds(ids = [], callOptions = {}) {
|
|
1388
|
+
return listRecordsByIds(runtime, knex, ids, callOptions);
|
|
1389
|
+
},
|
|
1390
|
+
async listByForeignIds(ids = [], foreignKey = "", callOptions = {}) {
|
|
1391
|
+
const normalizedForeignKey = normalizeText(foreignKey);
|
|
1392
|
+
if (!normalizedForeignKey) {
|
|
1393
|
+
throw new TypeError(`${runtime.context || "crudRepository"} listByForeignIds requires foreignKey.`);
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
return listRecordsByIds(runtime, knex, ids, {
|
|
1397
|
+
...callOptions,
|
|
1398
|
+
valueKey: normalizedForeignKey
|
|
1399
|
+
});
|
|
1400
|
+
},
|
|
1401
|
+
async create(payload = {}, callOptions = {}) {
|
|
1402
|
+
return createRecord(runtime, knex, payload, callOptions);
|
|
1403
|
+
},
|
|
1404
|
+
async updateById(recordId, patch = {}, callOptions = {}) {
|
|
1405
|
+
return updateRecordById(runtime, knex, recordId, patch, callOptions);
|
|
1406
|
+
},
|
|
1407
|
+
async deleteById(recordId, callOptions = {}) {
|
|
1408
|
+
return deleteRecordById(runtime, knex, recordId, callOptions);
|
|
1409
|
+
}
|
|
1410
|
+
});
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
export { createCrudResourceRuntime };
|