@atscript/moost-db 0.1.58 → 0.1.59
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 +1 -1
- package/dist/index.cjs +608 -290
- package/dist/index.d.cts +180 -136
- package/dist/index.d.mts +180 -136
- package/dist/index.mjs +604 -287
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -99,7 +99,7 @@ var SelectControlDto = class {
|
|
|
99
99
|
(0, _atscript_typescript_utils.throwFeatureDisabled)("JSON Schema", "jsonSchema", "emit.jsonSchema");
|
|
100
100
|
}
|
|
101
101
|
};
|
|
102
|
-
(0, _atscript_typescript_utils.defineAnnotatedType)("object", QueryControlsDto).prop("$skip", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("number").tags("positive", "int", "number").annotate("expect.int", true).annotate("expect.min", { minValue: 0 }).optional().$type).prop("$limit", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("number").tags("positive", "int", "number").annotate("expect.int", true).annotate("expect.min", { minValue: 0 }).optional().$type).prop("$count", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("boolean").tags("boolean").optional().$type).prop("$sort", (0, _atscript_typescript_utils.defineAnnotatedType)().refTo(SortControlDto).optional().$type).prop("$select", (0, _atscript_typescript_utils.defineAnnotatedType)("union").item((0, _atscript_typescript_utils.defineAnnotatedType)().refTo(SelectControlDto).$type).item((0, _atscript_typescript_utils.defineAnnotatedType)("array").of((0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").$type).$type).optional().$type).prop("$search", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").optional().$type).prop("$index", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").optional().$type).prop("$vector", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").optional().$type).prop("$threshold", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").optional().$type).prop("$with", (0, _atscript_typescript_utils.defineAnnotatedType)("array").of((0, _atscript_typescript_utils.defineAnnotatedType)().refTo(WithRelationDto).$type).optional().$type);
|
|
102
|
+
(0, _atscript_typescript_utils.defineAnnotatedType)("object", QueryControlsDto).prop("$skip", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("number").tags("positive", "int", "number").annotate("expect.int", true).annotate("expect.min", { minValue: 0 }).optional().$type).prop("$limit", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("number").tags("positive", "int", "number").annotate("expect.int", true).annotate("expect.min", { minValue: 0 }).optional().$type).prop("$count", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("boolean").tags("boolean").optional().$type).prop("$sort", (0, _atscript_typescript_utils.defineAnnotatedType)().refTo(SortControlDto).optional().$type).prop("$select", (0, _atscript_typescript_utils.defineAnnotatedType)("union").item((0, _atscript_typescript_utils.defineAnnotatedType)().refTo(SelectControlDto).$type).item((0, _atscript_typescript_utils.defineAnnotatedType)("array").of((0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").$type).$type).optional().$type).prop("$search", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").optional().$type).prop("$index", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").optional().$type).prop("$vector", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").optional().$type).prop("$threshold", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").optional().$type).prop("$with", (0, _atscript_typescript_utils.defineAnnotatedType)("array").of((0, _atscript_typescript_utils.defineAnnotatedType)().refTo(WithRelationDto).$type).optional().$type).prop("$actions", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("boolean").tags("boolean").optional().$type);
|
|
103
103
|
(0, _atscript_typescript_utils.defineAnnotatedType)("object", PagesControlsDto).prop("$page", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").annotate("expect.pattern", {
|
|
104
104
|
pattern: "^\\d+$",
|
|
105
105
|
flags: "u",
|
|
@@ -108,8 +108,8 @@ var SelectControlDto = class {
|
|
|
108
108
|
pattern: "^\\d+$",
|
|
109
109
|
flags: "u",
|
|
110
110
|
message: "Expected positive number"
|
|
111
|
-
}, true).optional().$type).prop("$sort", (0, _atscript_typescript_utils.defineAnnotatedType)().refTo(SortControlDto).optional().$type).prop("$select", (0, _atscript_typescript_utils.defineAnnotatedType)("union").item((0, _atscript_typescript_utils.defineAnnotatedType)().refTo(SelectControlDto).$type).item((0, _atscript_typescript_utils.defineAnnotatedType)("array").of((0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").$type).$type).optional().$type).prop("$search", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").optional().$type).prop("$index", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").optional().$type).prop("$vector", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").optional().$type).prop("$threshold", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").optional().$type).prop("$with", (0, _atscript_typescript_utils.defineAnnotatedType)("array").of((0, _atscript_typescript_utils.defineAnnotatedType)().refTo(WithRelationDto).$type).optional().$type);
|
|
112
|
-
(0, _atscript_typescript_utils.defineAnnotatedType)("object", GetOneControlsDto).prop("$select", (0, _atscript_typescript_utils.defineAnnotatedType)("union").item((0, _atscript_typescript_utils.defineAnnotatedType)().refTo(SelectControlDto).$type).item((0, _atscript_typescript_utils.defineAnnotatedType)("array").of((0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").$type).$type).optional().$type).prop("$with", (0, _atscript_typescript_utils.defineAnnotatedType)("array").of((0, _atscript_typescript_utils.defineAnnotatedType)().refTo(WithRelationDto).$type).optional().$type);
|
|
111
|
+
}, true).optional().$type).prop("$sort", (0, _atscript_typescript_utils.defineAnnotatedType)().refTo(SortControlDto).optional().$type).prop("$select", (0, _atscript_typescript_utils.defineAnnotatedType)("union").item((0, _atscript_typescript_utils.defineAnnotatedType)().refTo(SelectControlDto).$type).item((0, _atscript_typescript_utils.defineAnnotatedType)("array").of((0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").$type).$type).optional().$type).prop("$search", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").optional().$type).prop("$index", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").optional().$type).prop("$vector", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").optional().$type).prop("$threshold", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").optional().$type).prop("$with", (0, _atscript_typescript_utils.defineAnnotatedType)("array").of((0, _atscript_typescript_utils.defineAnnotatedType)().refTo(WithRelationDto).$type).optional().$type).prop("$actions", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("boolean").tags("boolean").optional().$type);
|
|
112
|
+
(0, _atscript_typescript_utils.defineAnnotatedType)("object", GetOneControlsDto).prop("$select", (0, _atscript_typescript_utils.defineAnnotatedType)("union").item((0, _atscript_typescript_utils.defineAnnotatedType)().refTo(SelectControlDto).$type).item((0, _atscript_typescript_utils.defineAnnotatedType)("array").of((0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").$type).$type).optional().$type).prop("$with", (0, _atscript_typescript_utils.defineAnnotatedType)("array").of((0, _atscript_typescript_utils.defineAnnotatedType)().refTo(WithRelationDto).$type).optional().$type).prop("$actions", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("boolean").tags("boolean").optional().$type);
|
|
113
113
|
(0, _atscript_typescript_utils.defineAnnotatedType)("object", WithRelationDto).prop("name", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").$type).prop("filter", (0, _atscript_typescript_utils.defineAnnotatedType)().refTo(WithFilterDto).optional().$type).prop("controls", (0, _atscript_typescript_utils.defineAnnotatedType)().refTo(WithRelationControlsDto).optional().$type).prop("insights", (0, _atscript_typescript_utils.defineAnnotatedType)().refTo(WithFilterDto).optional().$type);
|
|
114
114
|
(0, _atscript_typescript_utils.defineAnnotatedType)("object", WithRelationControlsDto).prop("$skip", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("number").tags("positive", "int", "number").annotate("expect.int", true).annotate("expect.min", { minValue: 0 }).optional().$type).prop("$limit", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("number").tags("positive", "int", "number").annotate("expect.int", true).annotate("expect.min", { minValue: 0 }).optional().$type).prop("$sort", (0, _atscript_typescript_utils.defineAnnotatedType)().refTo(SortControlDto).optional().$type).prop("$select", (0, _atscript_typescript_utils.defineAnnotatedType)("union").item((0, _atscript_typescript_utils.defineAnnotatedType)().refTo(SelectControlDto).$type).item((0, _atscript_typescript_utils.defineAnnotatedType)("array").of((0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").$type).$type).optional().$type).prop("$with", (0, _atscript_typescript_utils.defineAnnotatedType)("array").of((0, _atscript_typescript_utils.defineAnnotatedType)().refTo(WithRelationDto).$type).optional().$type);
|
|
115
115
|
(0, _atscript_typescript_utils.defineAnnotatedType)("object", WithFilterDto).propPattern(/./, (0, _atscript_typescript_utils.defineAnnotatedType)("union").item((0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").$type).item((0, _atscript_typescript_utils.defineAnnotatedType)().designType("number").tags("number").$type).item((0, _atscript_typescript_utils.defineAnnotatedType)().designType("boolean").tags("boolean").$type).item((0, _atscript_typescript_utils.defineAnnotatedType)().designType("null").tags("null").$type).item((0, _atscript_typescript_utils.defineAnnotatedType)().refTo(WithFilterDto).$type).item((0, _atscript_typescript_utils.defineAnnotatedType)("array").of((0, _atscript_typescript_utils.defineAnnotatedType)().refTo(WithFilterDto).$type).$type).$type);
|
|
@@ -210,7 +210,7 @@ const WARN_PREFIX = "[moost-db actions]";
|
|
|
210
210
|
const MOOST_DB_ACTION = "atscript_db_action";
|
|
211
211
|
/** Class-level metadata key — written by `@DbActions` and the level-pinned shortcuts. Stored as an array; decorators accumulate. */
|
|
212
212
|
const MOOST_DB_ACTIONS = "atscript_db_actions";
|
|
213
|
-
/** Param-level metadata key — written by `@
|
|
213
|
+
/** Param-level metadata key — written by `@DbActionID()` / `@DbActionIDs()`. Drives level inference. */
|
|
214
214
|
const MOOST_DB_ACTION_PARAM = "atscript_db_action_param";
|
|
215
215
|
/** Param-level marker keys — written by `@DbActionRow()` / `@DbActionRows()`. */
|
|
216
216
|
const MOOST_DB_ACTION_ROW = "atscript_db_action_row";
|
|
@@ -240,8 +240,8 @@ function scanParamLevel(params) {
|
|
|
240
240
|
let hasBody = false;
|
|
241
241
|
for (const p of params) {
|
|
242
242
|
const kind = p[MOOST_DB_ACTION_PARAM];
|
|
243
|
-
if (kind === "
|
|
244
|
-
else if (kind === "
|
|
243
|
+
if (kind === "id") single = true;
|
|
244
|
+
else if (kind === "ids") multi = true;
|
|
245
245
|
if (p["atscript_db_action_row"]) {
|
|
246
246
|
single = true;
|
|
247
247
|
hasRowParam = true;
|
|
@@ -262,11 +262,7 @@ function scanParamLevel(params) {
|
|
|
262
262
|
}
|
|
263
263
|
//#endregion
|
|
264
264
|
//#region src/actions/discover.ts
|
|
265
|
-
/**
|
|
266
|
-
* Pure structural-copy fields. `disabled` and `requiredFields` are handled
|
|
267
|
-
* as special cases in {@link emitInfo} so the function-to-string transform
|
|
268
|
-
* stays out of the copy loop.
|
|
269
|
-
*/
|
|
265
|
+
/** Structural-copy fields; `disabled` is handled separately in {@link emitInfo} (function-to-string transform). */
|
|
270
266
|
const OPTIONAL_FIELDS = [
|
|
271
267
|
"icon",
|
|
272
268
|
"intent",
|
|
@@ -277,27 +273,29 @@ const OPTIONAL_FIELDS = [
|
|
|
277
273
|
"shortcut"
|
|
278
274
|
];
|
|
279
275
|
const actionsCache = /* @__PURE__ */ new WeakMap();
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
* Reads class + method metadata via `getMoostMate()` and resolves bound POST
|
|
283
|
-
* paths through the Moost controller overview.
|
|
284
|
-
*
|
|
285
|
-
* Result is memoized per controller constructor — discovery walks every
|
|
286
|
-
* handler entry and reads decorator metadata, which is wasted work to repeat
|
|
287
|
-
* across instances.
|
|
288
|
-
*/
|
|
276
|
+
const rowLevelActionsCache = /* @__PURE__ */ new WeakMap();
|
|
277
|
+
/** Discover actions on a controller, memoized per ctor. `info`-only callers map `e => e.info`. */
|
|
289
278
|
function discoverActions(controllerCtor, app, logger) {
|
|
290
279
|
const cached = actionsCache.get(controllerCtor);
|
|
291
280
|
if (cached) return cached;
|
|
292
281
|
const overview = app.getControllersOverview?.()?.find((o) => o.type === controllerCtor);
|
|
293
282
|
const out = [];
|
|
294
|
-
|
|
295
|
-
|
|
283
|
+
const seen = /* @__PURE__ */ new Set();
|
|
284
|
+
collectMethodActions(controllerCtor, overview, logger, out, seen);
|
|
285
|
+
collectClassActions(controllerCtor, logger, out, seen);
|
|
296
286
|
applyDefaultPerLevel(out, logger);
|
|
297
287
|
actionsCache.set(controllerCtor, out);
|
|
298
288
|
return out;
|
|
299
289
|
}
|
|
300
|
-
|
|
290
|
+
/** Row/rows-level subset of {@link discoverActions}; memoized per ctor. */
|
|
291
|
+
function discoverRowLevelActions(controllerCtor, app, logger) {
|
|
292
|
+
const cached = rowLevelActionsCache.get(controllerCtor);
|
|
293
|
+
if (cached) return cached;
|
|
294
|
+
const filtered = discoverActions(controllerCtor, app, logger).filter((e) => e.info.level === "row" || e.info.level === "rows");
|
|
295
|
+
rowLevelActionsCache.set(controllerCtor, filtered);
|
|
296
|
+
return filtered;
|
|
297
|
+
}
|
|
298
|
+
function collectMethodActions(ctor, overview, logger, out, seen) {
|
|
301
299
|
if (!overview) return;
|
|
302
300
|
const byMethod = /* @__PURE__ */ new Map();
|
|
303
301
|
for (const h of overview.handlers) {
|
|
@@ -316,13 +314,17 @@ function collectMethodActions(ctor, overview, logger, out) {
|
|
|
316
314
|
const levelInfer = inferMethodLevel(methodMeta.params ?? [], action.name, logger);
|
|
317
315
|
if (!levelInfer) continue;
|
|
318
316
|
if (levelInfer.bodyConflict) {
|
|
319
|
-
logger.warn(`${WARN_PREFIX} action "${action.name}" cannot mix @
|
|
317
|
+
logger.warn(`${WARN_PREFIX} action "${action.name}" cannot mix @DbActionID*/@DbActionIDs/@DbActionRow*/@DbActionRows with @Body() — dropping`);
|
|
320
318
|
continue;
|
|
321
319
|
}
|
|
322
320
|
if (levelInfer.level === "table" && action.opts.disabled !== void 0) {
|
|
323
321
|
logger.warn(`${WARN_PREFIX} action "${action.name}" — \`disabled\` is not allowed at the 'table' level; row-state predicates are not meaningful when no row is in scope. Use @Authenticate / arbac for table-level access — dropping`);
|
|
324
322
|
continue;
|
|
325
323
|
}
|
|
324
|
+
if (action.opts.disabled !== void 0 && !isNonEmptyStringArray(action.opts.requiredFields)) {
|
|
325
|
+
logger.warn(`${WARN_PREFIX} action "${action.name}" — \`disabled\` requires a non-empty \`requiredFields\` array (the predicate's field dependencies must be declared explicitly) — dropping`);
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
326
328
|
if (action.opts.disabled !== void 0 || levelInfer.hasRowParam) {
|
|
327
329
|
const extendsReadable = isAsDbReadableControllerSubclass(ctor);
|
|
328
330
|
const hasOptsTable = action.opts.table != null;
|
|
@@ -346,6 +348,10 @@ function collectMethodActions(ctor, overview, logger, out) {
|
|
|
346
348
|
logger.warn(`${WARN_PREFIX} action "${action.name}" requires a label (opts.label or @Label) — dropping`);
|
|
347
349
|
continue;
|
|
348
350
|
}
|
|
351
|
+
if (seen.has(action.name)) {
|
|
352
|
+
logger.warn(`${WARN_PREFIX} duplicate action name "${action.name}" within controller — dropping the second declaration`);
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
349
355
|
const info = {
|
|
350
356
|
name: action.name,
|
|
351
357
|
label,
|
|
@@ -353,14 +359,18 @@ function collectMethodActions(ctor, overview, logger, out) {
|
|
|
353
359
|
processor: "backend",
|
|
354
360
|
value: path
|
|
355
361
|
};
|
|
356
|
-
emitInfo(info, action.opts
|
|
357
|
-
|
|
362
|
+
emitInfo(info, action.opts);
|
|
363
|
+
seen.add(action.name);
|
|
364
|
+
out.push({
|
|
365
|
+
info,
|
|
366
|
+
raw: action.opts
|
|
367
|
+
});
|
|
358
368
|
}
|
|
359
369
|
}
|
|
360
370
|
function inferMethodLevel(params, actionName, logger) {
|
|
361
371
|
const scan = scanParamLevel(params);
|
|
362
372
|
if (scan.single && scan.multi) {
|
|
363
|
-
logger.warn(`${WARN_PREFIX} action "${actionName}" mixes single-cardinality and multi-cardinality decorators (@
|
|
373
|
+
logger.warn(`${WARN_PREFIX} action "${actionName}" mixes single-cardinality and multi-cardinality decorators (@DbActionID / @DbActionRow vs @DbActionIDs / @DbActionRows) — dropping`);
|
|
364
374
|
return null;
|
|
365
375
|
}
|
|
366
376
|
return {
|
|
@@ -369,12 +379,22 @@ function inferMethodLevel(params, actionName, logger) {
|
|
|
369
379
|
hasRowParam: scan.hasRowParam
|
|
370
380
|
};
|
|
371
381
|
}
|
|
372
|
-
function collectClassActions(ctor, logger, out) {
|
|
382
|
+
function collectClassActions(ctor, logger, out, seen) {
|
|
373
383
|
const list = (0, moost.getMoostMate)().read(ctor)?.[MOOST_DB_ACTIONS];
|
|
374
384
|
if (!list) return;
|
|
375
385
|
for (const { name, entry } of list) {
|
|
386
|
+
if (seen.has(name)) {
|
|
387
|
+
logger.warn(`${WARN_PREFIX} duplicate action name "${name}" within controller — dropping the second declaration`);
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
376
390
|
const built = buildClassEntry(name, entry, logger);
|
|
377
|
-
if (built)
|
|
391
|
+
if (built) {
|
|
392
|
+
seen.add(name);
|
|
393
|
+
out.push({
|
|
394
|
+
info: built,
|
|
395
|
+
raw: entry
|
|
396
|
+
});
|
|
397
|
+
}
|
|
378
398
|
}
|
|
379
399
|
}
|
|
380
400
|
function buildClassEntry(name, entry, logger) {
|
|
@@ -391,6 +411,10 @@ function buildClassEntry(name, entry, logger) {
|
|
|
391
411
|
logger.warn(`${WARN_PREFIX} class-level action "${name}" — \`disabled\` is not allowed at the 'table' level — dropping`);
|
|
392
412
|
return null;
|
|
393
413
|
}
|
|
414
|
+
if (entry.disabled !== void 0 && !isNonEmptyStringArray(entry.requiredFields)) {
|
|
415
|
+
logger.warn(`${WARN_PREFIX} class-level action "${name}" — \`disabled\` requires a non-empty \`requiredFields\` array (the predicate's field dependencies must be declared explicitly) — dropping`);
|
|
416
|
+
return null;
|
|
417
|
+
}
|
|
394
418
|
const processor = entry.processor;
|
|
395
419
|
let value;
|
|
396
420
|
if (processor === "navigate" || processor === "backend") {
|
|
@@ -418,39 +442,27 @@ function buildClassEntry(name, entry, logger) {
|
|
|
418
442
|
processor,
|
|
419
443
|
value
|
|
420
444
|
};
|
|
421
|
-
emitInfo(info, entry
|
|
445
|
+
emitInfo(info, entry);
|
|
422
446
|
return info;
|
|
423
447
|
}
|
|
424
|
-
function applyDefaultPerLevel(
|
|
448
|
+
function applyDefaultPerLevel(envelopes, logger) {
|
|
425
449
|
const winners = /* @__PURE__ */ new Map();
|
|
426
|
-
for (const
|
|
427
|
-
if (!
|
|
428
|
-
const existing = winners.get(
|
|
450
|
+
for (const { info } of envelopes) {
|
|
451
|
+
if (!info.default) continue;
|
|
452
|
+
const existing = winners.get(info.level);
|
|
429
453
|
if (existing) {
|
|
430
|
-
|
|
431
|
-
logger.warn(`${WARN_PREFIX} duplicate default action at level "${
|
|
432
|
-
} else winners.set(
|
|
454
|
+
info.default = false;
|
|
455
|
+
logger.warn(`${WARN_PREFIX} duplicate default action at level "${info.level}": "${existing}" wins, "${info.name}" demoted`);
|
|
456
|
+
} else winners.set(info.level, info.name);
|
|
433
457
|
}
|
|
434
458
|
}
|
|
435
|
-
/**
|
|
436
|
-
|
|
437
|
-
* `requiredFields` (forwarded verbatim — server doesn't auto-derive).
|
|
438
|
-
* `requiredFields` without `disabled` is dropped with a warning before the
|
|
439
|
-
* structural copy runs, so method-decorator and class-level-dict origins
|
|
440
|
-
* stay symmetric.
|
|
441
|
-
*/
|
|
442
|
-
function emitInfo(info, source, name, logger) {
|
|
459
|
+
/** Emit structural-copy fields plus stringified `disabled`. `requiredFields` is server-internal (never on the wire). */
|
|
460
|
+
function emitInfo(info, source) {
|
|
443
461
|
const disabled = source.disabled;
|
|
444
462
|
const hasDisabled = typeof disabled === "function";
|
|
445
|
-
let requiredFields = source.requiredFields;
|
|
446
|
-
if (!hasDisabled && requiredFields !== void 0) {
|
|
447
|
-
logger.warn(`${WARN_PREFIX} action "${name}" has \`requiredFields\` without \`disabled\` — \`requiredFields\` is purely a UI hint and meaningless without a predicate. Dropping \`requiredFields\` from /meta.`);
|
|
448
|
-
requiredFields = void 0;
|
|
449
|
-
}
|
|
450
463
|
copyOptionalFields(info, source);
|
|
451
464
|
if (Array.isArray(info.promptText)) info.promptText = info.promptText.slice();
|
|
452
465
|
if (hasDisabled) info.disabled = disabled.toString();
|
|
453
|
-
if (Array.isArray(requiredFields)) info.requiredFields = requiredFields.slice();
|
|
454
466
|
}
|
|
455
467
|
function copyOptionalFields(info, source) {
|
|
456
468
|
for (const key of OPTIONAL_FIELDS) {
|
|
@@ -458,6 +470,9 @@ function copyOptionalFields(info, source) {
|
|
|
458
470
|
if (value !== void 0) info[key] = value;
|
|
459
471
|
}
|
|
460
472
|
}
|
|
473
|
+
function isNonEmptyStringArray(value) {
|
|
474
|
+
return Array.isArray(value) && value.length > 0 && value.every((v) => typeof v === "string");
|
|
475
|
+
}
|
|
461
476
|
//#endregion
|
|
462
477
|
//#region \0@oxc-project+runtime@0.120.0/helpers/decorateMetadata.js
|
|
463
478
|
function __decorateMetadata(k, v) {
|
|
@@ -640,6 +655,7 @@ let AsReadableController = class AsReadableController {
|
|
|
640
655
|
vectorSearchable: false,
|
|
641
656
|
searchIndexes: [],
|
|
642
657
|
primaryKeys: [],
|
|
658
|
+
preferredId: [],
|
|
643
659
|
relations: [],
|
|
644
660
|
fields: {},
|
|
645
661
|
type: this.getSerializedType(),
|
|
@@ -653,7 +669,7 @@ let AsReadableController = class AsReadableController {
|
|
|
653
669
|
* controllers — see {@link AsValueHelpController#buildMetaResponse}.
|
|
654
670
|
*/
|
|
655
671
|
buildActions() {
|
|
656
|
-
return discoverActions(this.constructor, this.app, this.logger);
|
|
672
|
+
return discoverActions(this.constructor, this.app, this.logger).map((e) => e.info);
|
|
657
673
|
}
|
|
658
674
|
/**
|
|
659
675
|
* Declares the built-in CRUD operations this controller exposes. Subclasses
|
|
@@ -688,6 +704,80 @@ AsReadableController = __decorate([UseValidationErrorTransform(), __decorateMeta
|
|
|
688
704
|
Object
|
|
689
705
|
])], AsReadableController);
|
|
690
706
|
//#endregion
|
|
707
|
+
//#region src/actions/verdict.ts
|
|
708
|
+
/** Assert that a `disabled` predicate returned a `boolean[]` of the expected length; throws HTTP 500 otherwise. */
|
|
709
|
+
function assertVerdictLength(action, verdicts, expected) {
|
|
710
|
+
if (!Array.isArray(verdicts) || verdicts.length !== expected) throw new _moostjs_event_http.HttpError(500, `Action "${action}" disabled predicate returned an invalid verdict array`);
|
|
711
|
+
}
|
|
712
|
+
//#endregion
|
|
713
|
+
//#region src/actions/list-augmenter.ts
|
|
714
|
+
const candidateCache = /* @__PURE__ */ new WeakMap();
|
|
715
|
+
/** WHY: envelopes are immutable post-discovery, so derived `Candidate` shape is cached for the envelope's lifetime; `null` sentinel pins table-level skip. */
|
|
716
|
+
function getCandidate(e) {
|
|
717
|
+
const cached = candidateCache.get(e);
|
|
718
|
+
if (cached !== void 0) return cached;
|
|
719
|
+
if (e.info.level !== "row" && e.info.level !== "rows") {
|
|
720
|
+
candidateCache.set(e, null);
|
|
721
|
+
return null;
|
|
722
|
+
}
|
|
723
|
+
const raw = e.raw;
|
|
724
|
+
const c = {
|
|
725
|
+
envelope: e,
|
|
726
|
+
disabledFn: typeof raw.disabled === "function" ? raw.disabled : void 0,
|
|
727
|
+
requiredFields: Array.isArray(raw.requiredFields) ? raw.requiredFields : []
|
|
728
|
+
};
|
|
729
|
+
candidateCache.set(e, c);
|
|
730
|
+
return c;
|
|
731
|
+
}
|
|
732
|
+
function collectCandidates(envelopes) {
|
|
733
|
+
const out = [];
|
|
734
|
+
for (const e of envelopes) {
|
|
735
|
+
const c = getCandidate(e);
|
|
736
|
+
if (c !== null) out.push(c);
|
|
737
|
+
}
|
|
738
|
+
return out;
|
|
739
|
+
}
|
|
740
|
+
function computeStripFields(candidates, resolvedProjection) {
|
|
741
|
+
let userSet = null;
|
|
742
|
+
let strip = null;
|
|
743
|
+
for (const c of candidates) for (const f of c.requiredFields) {
|
|
744
|
+
if (userSet === null) userSet = new Set(resolvedProjection);
|
|
745
|
+
if (userSet.has(f)) continue;
|
|
746
|
+
if (strip === null) strip = /* @__PURE__ */ new Set();
|
|
747
|
+
strip.add(f);
|
|
748
|
+
}
|
|
749
|
+
return strip;
|
|
750
|
+
}
|
|
751
|
+
function augmentRowsWithActions(args) {
|
|
752
|
+
const { envelopes, rows, resolvedProjection } = args;
|
|
753
|
+
const candidates = collectCandidates(envelopes);
|
|
754
|
+
if (candidates.length === 0 || rows.length === 0) return rows;
|
|
755
|
+
const verdicts = candidates.map((c) => {
|
|
756
|
+
if (!c.disabledFn) return void 0;
|
|
757
|
+
const out = c.disabledFn(rows);
|
|
758
|
+
assertVerdictLength(c.envelope.info.name, out, rows.length);
|
|
759
|
+
return out;
|
|
760
|
+
});
|
|
761
|
+
for (let i = 0; i < rows.length; i++) {
|
|
762
|
+
const row = rows[i];
|
|
763
|
+
const names = [];
|
|
764
|
+
for (let j = 0; j < candidates.length; j++) {
|
|
765
|
+
const v = verdicts[j];
|
|
766
|
+
if (v === void 0) {
|
|
767
|
+
names.push(candidates[j].envelope.info.name);
|
|
768
|
+
continue;
|
|
769
|
+
}
|
|
770
|
+
if (!v[i]) names.push(candidates[j].envelope.info.name);
|
|
771
|
+
}
|
|
772
|
+
row.$actions = names;
|
|
773
|
+
}
|
|
774
|
+
if (resolvedProjection !== null) {
|
|
775
|
+
const stripFields = computeStripFields(candidates, resolvedProjection);
|
|
776
|
+
if (stripFields !== null) for (const row of rows) for (const f of stripFields) delete row[f];
|
|
777
|
+
}
|
|
778
|
+
return rows;
|
|
779
|
+
}
|
|
780
|
+
//#endregion
|
|
691
781
|
//#region src/decorators.ts
|
|
692
782
|
/**
|
|
693
783
|
* DI token under which the {@link AtscriptDbReadable} instance
|
|
@@ -782,10 +872,17 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
|
|
|
782
872
|
/** Reference to the underlying readable (table or view). */
|
|
783
873
|
readable;
|
|
784
874
|
_gates;
|
|
875
|
+
_preferredIdSet;
|
|
876
|
+
_compositeIdShapes;
|
|
877
|
+
_overlayIsNoOp;
|
|
785
878
|
constructor(readable, app) {
|
|
786
879
|
super(readable.type, readable.tableName, app, readable.isView ? "view" : "table");
|
|
787
880
|
this.readable = readable;
|
|
788
881
|
this._gates = this._buildGates();
|
|
882
|
+
this._preferredIdSet = new Set(readable.preferredId ?? []);
|
|
883
|
+
this._compositeIdShapes = (readable.identifications ?? []).filter((id) => id.fields.length >= 2);
|
|
884
|
+
const defaultOverlay = AsReadableController.prototype.applyMetaOverlay;
|
|
885
|
+
this._overlayIsNoOp = this.applyMetaOverlay === defaultOverlay;
|
|
789
886
|
}
|
|
790
887
|
_buildGates() {
|
|
791
888
|
const meta = this.readable.type.metadata;
|
|
@@ -853,16 +950,169 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
|
|
|
853
950
|
transformProjection(projection) {
|
|
854
951
|
return projection;
|
|
855
952
|
}
|
|
953
|
+
widenPreferredIdProjection(projection) {
|
|
954
|
+
if (this._preferredIdSet.size === 0 || projection === void 0) return projection;
|
|
955
|
+
if (Array.isArray(projection)) return this._widenArrayProjection(projection);
|
|
956
|
+
return this._widenMapProjection(projection);
|
|
957
|
+
}
|
|
958
|
+
_widenArrayProjection(projection) {
|
|
959
|
+
const stringItems = /* @__PURE__ */ new Set();
|
|
960
|
+
for (const item of projection) if (typeof item === "string") stringItems.add(item);
|
|
961
|
+
let allPresent = true;
|
|
962
|
+
for (const field of this._preferredIdSet) if (!stringItems.has(field)) {
|
|
963
|
+
allPresent = false;
|
|
964
|
+
break;
|
|
965
|
+
}
|
|
966
|
+
if (allPresent) return projection;
|
|
967
|
+
const out = [...projection];
|
|
968
|
+
for (const field of this._preferredIdSet) if (!stringItems.has(field)) out.push(field);
|
|
969
|
+
return out;
|
|
970
|
+
}
|
|
971
|
+
_widenMapProjection(projection) {
|
|
972
|
+
const entries = Object.entries(projection);
|
|
973
|
+
if (entries.length === 0) return projection;
|
|
974
|
+
const included = /* @__PURE__ */ new Set();
|
|
975
|
+
const excluded = /* @__PURE__ */ new Set();
|
|
976
|
+
for (const [k, v] of entries) if (v === 1 || v === true) included.add(k);
|
|
977
|
+
else if (v === 0 || v === false) excluded.add(k);
|
|
978
|
+
if (included.size > 0 && excluded.size > 0) return new _moostjs_event_http.HttpError(400, "Mixed inclusion/exclusion $select maps are not supported");
|
|
979
|
+
if (excluded.size === 0) {
|
|
980
|
+
let allPresent = true;
|
|
981
|
+
for (const field of this._preferredIdSet) if (!included.has(field)) {
|
|
982
|
+
allPresent = false;
|
|
983
|
+
break;
|
|
984
|
+
}
|
|
985
|
+
if (allPresent) return projection;
|
|
986
|
+
const widened = {};
|
|
987
|
+
for (const k of included) widened[k] = 1;
|
|
988
|
+
for (const field of this._preferredIdSet) widened[field] = 1;
|
|
989
|
+
return widened;
|
|
990
|
+
}
|
|
991
|
+
const widened = {};
|
|
992
|
+
for (const fd of this.readable.fieldDescriptors) if (!fd.ignored && !excluded.has(fd.path)) widened[fd.path] = 1;
|
|
993
|
+
for (const field of this._preferredIdSet) widened[field] = 1;
|
|
994
|
+
return widened;
|
|
995
|
+
}
|
|
996
|
+
/** WHY: the URL parser only auto-coerces `$count`; every other boolean control reaches us as `"true"`/`"1"` and would fail DTO validation. */
|
|
997
|
+
_coerceActionsControl(controls) {
|
|
998
|
+
const v = controls.$actions;
|
|
999
|
+
if (typeof v === "string") controls.$actions = v === "true" || v === "1" || v === "";
|
|
1000
|
+
}
|
|
1001
|
+
/** Normalize a post-`widenPreferredIdProjection` $select into `string[] | null` (`null` = all fields). */
|
|
1002
|
+
_resolveProjectionForAugmenter(select) {
|
|
1003
|
+
if (select === void 0) return null;
|
|
1004
|
+
if (Array.isArray(select)) {
|
|
1005
|
+
const out = [];
|
|
1006
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1007
|
+
for (const item of select) if (typeof item === "string" && !seen.has(item)) {
|
|
1008
|
+
seen.add(item);
|
|
1009
|
+
out.push(item);
|
|
1010
|
+
}
|
|
1011
|
+
return out;
|
|
1012
|
+
}
|
|
1013
|
+
const obj = select;
|
|
1014
|
+
const included = [];
|
|
1015
|
+
const excluded = [];
|
|
1016
|
+
for (const [k, v] of Object.entries(obj)) if (v === 1 || v === true) included.push(k);
|
|
1017
|
+
else if (v === 0 || v === false) excluded.push(k);
|
|
1018
|
+
if (included.length > 0 && excluded.length === 0) return included;
|
|
1019
|
+
if (excluded.length > 0 && included.length === 0) {
|
|
1020
|
+
const excludedSet = new Set(excluded);
|
|
1021
|
+
const out = [];
|
|
1022
|
+
for (const fd of this.readable.fieldDescriptors) if (!fd.ignored && !excludedSet.has(fd.path)) out.push(fd.path);
|
|
1023
|
+
return out;
|
|
1024
|
+
}
|
|
1025
|
+
throw new _moostjs_event_http.HttpError(500, "[moost-db] mixed inclusion/exclusion projection reached augmenter; widenPreferredIdProjection should have rejected it");
|
|
1026
|
+
}
|
|
1027
|
+
/** WHY: filter row/rows envelopes by the per-request `applyMetaOverlay` action set; skip `meta()` when overlay is identity. */
|
|
1028
|
+
async _resolveAugmentEnvelopes() {
|
|
1029
|
+
const rowLevelEnvelopes = discoverRowLevelActions(this.constructor, this.app, this.logger);
|
|
1030
|
+
if (rowLevelEnvelopes.length === 0) return null;
|
|
1031
|
+
if (this._overlayIsNoOp) return rowLevelEnvelopes;
|
|
1032
|
+
const overlayMeta = await this.meta();
|
|
1033
|
+
const allowedNames = new Set(overlayMeta.actions.map((a) => a.name));
|
|
1034
|
+
const filtered = rowLevelEnvelopes.filter((e) => allowedNames.has(e.info.name));
|
|
1035
|
+
return filtered.length === 0 ? null : filtered;
|
|
1036
|
+
}
|
|
1037
|
+
/** Returns a widened `$select` only when at least one `requiredFields` entry is missing; `null` means "no widening needed". */
|
|
1038
|
+
_widenSelectForActions(envelopes, baseSelect) {
|
|
1039
|
+
let resultSet = null;
|
|
1040
|
+
let result = null;
|
|
1041
|
+
for (const e of envelopes) {
|
|
1042
|
+
const raw = e.raw;
|
|
1043
|
+
if (!Array.isArray(raw.requiredFields)) continue;
|
|
1044
|
+
for (const f of raw.requiredFields) {
|
|
1045
|
+
if (resultSet ? resultSet.has(f) : baseSelect.includes(f)) continue;
|
|
1046
|
+
if (resultSet === null) {
|
|
1047
|
+
resultSet = new Set(baseSelect);
|
|
1048
|
+
result = [...baseSelect];
|
|
1049
|
+
}
|
|
1050
|
+
resultSet.add(f);
|
|
1051
|
+
result.push(f);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
return result;
|
|
1055
|
+
}
|
|
1056
|
+
async _prepareAugmentation(controls, select) {
|
|
1057
|
+
if (!controls.$actions) return null;
|
|
1058
|
+
const envelopes = await this._resolveAugmentEnvelopes();
|
|
1059
|
+
if (envelopes === null) return null;
|
|
1060
|
+
const resolvedProjection = this._resolveProjectionForAugmenter(select);
|
|
1061
|
+
return {
|
|
1062
|
+
envelopes,
|
|
1063
|
+
resolvedProjection,
|
|
1064
|
+
widenedSelect: resolvedProjection === null ? null : this._widenSelectForActions(envelopes, resolvedProjection)
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
async _resolveReadStrategy(controls) {
|
|
1068
|
+
const searchTerm = controls.$search;
|
|
1069
|
+
const indexName = controls.$index;
|
|
1070
|
+
const vectorField = controls.$vector;
|
|
1071
|
+
if (vectorField !== void 0 && searchTerm) return {
|
|
1072
|
+
kind: "vector",
|
|
1073
|
+
vector: await this.computeEmbedding(searchTerm, vectorField || void 0),
|
|
1074
|
+
vectorField
|
|
1075
|
+
};
|
|
1076
|
+
if (searchTerm && this.readable.isSearchable()) return {
|
|
1077
|
+
kind: "search",
|
|
1078
|
+
term: searchTerm,
|
|
1079
|
+
index: indexName
|
|
1080
|
+
};
|
|
1081
|
+
return { kind: "plain" };
|
|
1082
|
+
}
|
|
1083
|
+
/**
|
|
1084
|
+
* Shared `query` / `pages` pipeline: prepare actions augmentation + read
|
|
1085
|
+
* strategy in parallel, pre-widen $select for `requiredFields`, run
|
|
1086
|
+
* `exec`, and augment `result.data` with `$actions` when the request set
|
|
1087
|
+
* `$actions=true`. Caller dispatches the strategy to its read-method
|
|
1088
|
+
* family (count vs no-count).
|
|
1089
|
+
*/
|
|
1090
|
+
async _runReadWithActions(queryObj, controls, select, exec) {
|
|
1091
|
+
const [prep, strategy] = await Promise.all([this._prepareAugmentation(controls, select), this._resolveReadStrategy(controls)]);
|
|
1092
|
+
const result = await exec(prep?.widenedSelect ? {
|
|
1093
|
+
...queryObj,
|
|
1094
|
+
controls: {
|
|
1095
|
+
...queryObj.controls,
|
|
1096
|
+
$select: prep.widenedSelect
|
|
1097
|
+
}
|
|
1098
|
+
} : queryObj, strategy);
|
|
1099
|
+
if (!prep) return result;
|
|
1100
|
+
result.data = augmentRowsWithActions({
|
|
1101
|
+
envelopes: prep.envelopes,
|
|
1102
|
+
rows: result.data,
|
|
1103
|
+
resolvedProjection: prep.resolvedProjection
|
|
1104
|
+
});
|
|
1105
|
+
return result;
|
|
1106
|
+
}
|
|
856
1107
|
/**
|
|
857
1108
|
* Extracts a composite identifier object from query params.
|
|
858
1109
|
* Tries composite primary key first, then compound unique indexes.
|
|
859
1110
|
*/
|
|
860
1111
|
extractCompositeId(query) {
|
|
861
|
-
const
|
|
862
|
-
if (pkFields.length > 1) {
|
|
1112
|
+
for (const id of this._compositeIdShapes) {
|
|
863
1113
|
const idObj = {};
|
|
864
1114
|
let allPresent = true;
|
|
865
|
-
for (const field of
|
|
1115
|
+
for (const field of id.fields) {
|
|
866
1116
|
if (query[field] === void 0) {
|
|
867
1117
|
allPresent = false;
|
|
868
1118
|
break;
|
|
@@ -871,19 +1121,6 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
|
|
|
871
1121
|
}
|
|
872
1122
|
if (allPresent) return idObj;
|
|
873
1123
|
}
|
|
874
|
-
for (const index of this.readable.indexes.values()) {
|
|
875
|
-
if (index.type !== "unique" || index.fields.length < 2) continue;
|
|
876
|
-
const idObj = {};
|
|
877
|
-
let allPresent = true;
|
|
878
|
-
for (const indexField of index.fields) {
|
|
879
|
-
if (query[indexField.name] === void 0) {
|
|
880
|
-
allPresent = false;
|
|
881
|
-
break;
|
|
882
|
-
}
|
|
883
|
-
idObj[indexField.name] = query[indexField.name];
|
|
884
|
-
}
|
|
885
|
-
if (allPresent) return idObj;
|
|
886
|
-
}
|
|
887
1124
|
return new _moostjs_event_http.HttpError(400, "Query params do not match any composite primary key or compound unique index");
|
|
888
1125
|
}
|
|
889
1126
|
/**
|
|
@@ -892,6 +1129,7 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
|
|
|
892
1129
|
async query(url) {
|
|
893
1130
|
const parsed = this.parseQueryString(url);
|
|
894
1131
|
const controls = parsed.controls;
|
|
1132
|
+
this._coerceActionsControl(controls);
|
|
895
1133
|
if (controls.$groupBy?.length) {
|
|
896
1134
|
if (controls.$with?.length) return new _moostjs_event_http.HttpError(400, "Cannot combine $with and $groupBy in the same query");
|
|
897
1135
|
if (parsed.insights) {
|
|
@@ -909,17 +1147,16 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
|
|
|
909
1147
|
if (error) return error;
|
|
910
1148
|
const gateError = this.checkGates(parsed.filter, controls, this._gates);
|
|
911
1149
|
if (gateError) return gateError;
|
|
912
|
-
const [filter,
|
|
1150
|
+
const [filter, rawSelect] = await Promise.all([this.transformFilter(parsed.filter), this.transformProjection(controls.$select)]);
|
|
913
1151
|
if (controls.$count) return this.readable.count({
|
|
914
1152
|
filter,
|
|
915
1153
|
controls: {
|
|
916
1154
|
...controls,
|
|
917
|
-
$select:
|
|
1155
|
+
$select: rawSelect
|
|
918
1156
|
}
|
|
919
1157
|
});
|
|
920
|
-
const
|
|
921
|
-
|
|
922
|
-
const vectorField = controls.$vector;
|
|
1158
|
+
const select = this.widenPreferredIdProjection(rawSelect);
|
|
1159
|
+
if (select instanceof _moostjs_event_http.HttpError) return select;
|
|
923
1160
|
const threshold = controls.$threshold ? Number(controls.$threshold) : void 0;
|
|
924
1161
|
const queryObj = {
|
|
925
1162
|
filter,
|
|
@@ -930,19 +1167,20 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
|
|
|
930
1167
|
$threshold: threshold
|
|
931
1168
|
}
|
|
932
1169
|
};
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
1170
|
+
return (await this._runReadWithActions(queryObj, controls, select, async (q, strategy) => {
|
|
1171
|
+
switch (strategy.kind) {
|
|
1172
|
+
case "vector": return { data: await (strategy.vectorField ? this.readable.vectorSearch(strategy.vectorField, strategy.vector, q) : this.readable.vectorSearch(strategy.vector, q)) };
|
|
1173
|
+
case "search": return { data: await this.readable.search(strategy.term, q, strategy.index) };
|
|
1174
|
+
case "plain": return { data: await this.readable.findMany(q) };
|
|
1175
|
+
}
|
|
1176
|
+
})).data;
|
|
940
1177
|
}
|
|
941
1178
|
/**
|
|
942
1179
|
* **GET /pages** — returns paginated records with metadata.
|
|
943
1180
|
*/
|
|
944
1181
|
async pages(url) {
|
|
945
1182
|
const parsed = this.parseQueryString(url);
|
|
1183
|
+
this._coerceActionsControl(parsed.controls);
|
|
946
1184
|
const error = this.validateParsed(parsed, "pages");
|
|
947
1185
|
if (error) return error;
|
|
948
1186
|
const controls = parsed.controls;
|
|
@@ -951,10 +1189,9 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
|
|
|
951
1189
|
const page = Math.max(Number(controls.$page || 1), 1);
|
|
952
1190
|
const size = Math.max(Number(controls.$size || 10), 1);
|
|
953
1191
|
const skip = (page - 1) * size;
|
|
954
|
-
const [filter,
|
|
955
|
-
const
|
|
956
|
-
|
|
957
|
-
const vectorField = controls.$vector;
|
|
1192
|
+
const [filter, rawSelect] = await Promise.all([this.transformFilter(parsed.filter), this.transformProjection(controls.$select)]);
|
|
1193
|
+
const select = this.widenPreferredIdProjection(rawSelect);
|
|
1194
|
+
if (select instanceof _moostjs_event_http.HttpError) return select;
|
|
958
1195
|
const threshold = controls.$threshold ? Number(controls.$threshold) : void 0;
|
|
959
1196
|
const query = {
|
|
960
1197
|
filter,
|
|
@@ -966,13 +1203,13 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
|
|
|
966
1203
|
$threshold: threshold
|
|
967
1204
|
}
|
|
968
1205
|
};
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
1206
|
+
const result = await this._runReadWithActions(query, controls, select, async (q, strategy) => {
|
|
1207
|
+
switch (strategy.kind) {
|
|
1208
|
+
case "vector": return strategy.vectorField ? this.readable.vectorSearchWithCount(strategy.vectorField, strategy.vector, q) : this.readable.vectorSearchWithCount(strategy.vector, q);
|
|
1209
|
+
case "search": return this.readable.searchWithCount(strategy.term, q, strategy.index);
|
|
1210
|
+
case "plain": return this.readable.findManyWithCount(q);
|
|
1211
|
+
}
|
|
1212
|
+
});
|
|
976
1213
|
return {
|
|
977
1214
|
data: result.data,
|
|
978
1215
|
page,
|
|
@@ -986,15 +1223,14 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
|
|
|
986
1223
|
*/
|
|
987
1224
|
async getOne(id, url) {
|
|
988
1225
|
const parsed = this.parseQueryString(url);
|
|
1226
|
+
this._coerceActionsControl(parsed.controls);
|
|
989
1227
|
if (Object.keys(parsed.filter).length > 0) return new _moostjs_event_http.HttpError(400, "Filtering is not allowed for \"one\" endpoint");
|
|
990
1228
|
const error = this.validateParsed(parsed, "getOne");
|
|
991
1229
|
if (error) return error;
|
|
992
|
-
const
|
|
993
|
-
const
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
};
|
|
997
|
-
return this.returnOne(this.readable.findById(id, { controls }));
|
|
1230
|
+
const rawSelect = await this.transformProjection(parsed.controls.$select);
|
|
1231
|
+
const select = this.widenPreferredIdProjection(rawSelect);
|
|
1232
|
+
if (select instanceof _moostjs_event_http.HttpError) return select;
|
|
1233
|
+
return this._findByIdAndAugment(id, parsed.controls, select);
|
|
998
1234
|
}
|
|
999
1235
|
/**
|
|
1000
1236
|
* **GET /one?field1=val1&field2=val2** — retrieves a single record by composite key
|
|
@@ -1004,12 +1240,29 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
|
|
|
1004
1240
|
const idObj = this.extractCompositeId(query);
|
|
1005
1241
|
if (idObj instanceof _moostjs_event_http.HttpError) return idObj;
|
|
1006
1242
|
const parsed = this.parseQueryString(url);
|
|
1007
|
-
|
|
1243
|
+
this._coerceActionsControl(parsed.controls);
|
|
1244
|
+
const rawSelect = await this.transformProjection(parsed.controls.$select);
|
|
1245
|
+
const select = this.widenPreferredIdProjection(rawSelect);
|
|
1246
|
+
if (select instanceof _moostjs_event_http.HttpError) return select;
|
|
1247
|
+
return this._findByIdAndAugment(idObj, parsed.controls, select);
|
|
1248
|
+
}
|
|
1249
|
+
async _findByIdAndAugment(id, parsedControls, select) {
|
|
1250
|
+
const prep = await this._prepareAugmentation(parsedControls, select);
|
|
1251
|
+
const initialSelect = prep?.widenedSelect ?? select;
|
|
1008
1252
|
const controls = {
|
|
1009
|
-
...
|
|
1010
|
-
$select:
|
|
1253
|
+
...parsedControls,
|
|
1254
|
+
$select: initialSelect
|
|
1011
1255
|
};
|
|
1012
|
-
|
|
1256
|
+
const row = await this.readable.findById(id, { controls });
|
|
1257
|
+
const item = await this.returnOne(Promise.resolve(row));
|
|
1258
|
+
if (item instanceof _moostjs_event_http.HttpError) return item;
|
|
1259
|
+
if (!prep) return item;
|
|
1260
|
+
const [augmented] = augmentRowsWithActions({
|
|
1261
|
+
envelopes: prep.envelopes,
|
|
1262
|
+
rows: [item],
|
|
1263
|
+
resolvedProjection: prep.resolvedProjection
|
|
1264
|
+
});
|
|
1265
|
+
return augmented;
|
|
1013
1266
|
}
|
|
1014
1267
|
/**
|
|
1015
1268
|
* **GET /meta** — returns table/view metadata for UI.
|
|
@@ -1043,6 +1296,7 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
|
|
|
1043
1296
|
vectorSearchable: this.readable.isVectorSearchable(),
|
|
1044
1297
|
searchIndexes: this.readable.getSearchIndexes(),
|
|
1045
1298
|
primaryKeys: [...this.readable.primaryKeys],
|
|
1299
|
+
preferredId: [...this.readable.preferredId],
|
|
1046
1300
|
relations,
|
|
1047
1301
|
fields,
|
|
1048
1302
|
type: this.getSerializedType(),
|
|
@@ -1341,6 +1595,7 @@ let AsValueHelpController = class AsValueHelpController extends AsReadableContro
|
|
|
1341
1595
|
vectorSearchable: false,
|
|
1342
1596
|
searchIndexes: [],
|
|
1343
1597
|
primaryKeys: this.primaryKey ? [this.primaryKey] : [],
|
|
1598
|
+
preferredId: this.primaryKey ? [this.primaryKey] : [],
|
|
1344
1599
|
relations: [],
|
|
1345
1600
|
fields,
|
|
1346
1601
|
type: this.getSerializedType(),
|
|
@@ -1555,8 +1810,8 @@ function applySelect(rows, select) {
|
|
|
1555
1810
|
}
|
|
1556
1811
|
//#endregion
|
|
1557
1812
|
//#region src/actions/action-disabled-error.ts
|
|
1558
|
-
function buildMessage(action,
|
|
1559
|
-
if (
|
|
1813
|
+
function buildMessage(action, ids) {
|
|
1814
|
+
if (ids !== void 0) return `Action "${action}" is disabled for ${ids.length} of the selected rows`;
|
|
1560
1815
|
return `Action "${action}" is disabled for this row`;
|
|
1561
1816
|
}
|
|
1562
1817
|
/**
|
|
@@ -1564,95 +1819,110 @@ function buildMessage(action, pks) {
|
|
|
1564
1819
|
* with Moost's existing error mapper to produce HTTP 409 with the wire body
|
|
1565
1820
|
* defined by {@link ActionDisabledErrorBody}.
|
|
1566
1821
|
*
|
|
1567
|
-
* - `'row'`-level rejection: pass `(action,
|
|
1568
|
-
* - `'rows'`-level rejection: pass `(action, undefined,
|
|
1569
|
-
* emits `
|
|
1570
|
-
* of request
|
|
1822
|
+
* - `'row'`-level rejection: pass `(action, id)` — the body emits `id`.
|
|
1823
|
+
* - `'rows'`-level rejection: pass `(action, undefined, ids)` — the body
|
|
1824
|
+
* emits `ids` (the FULL list of failing IDs in reject mode; the FULL list
|
|
1825
|
+
* of request IDs in skip mode with zero survivors).
|
|
1571
1826
|
*/
|
|
1572
1827
|
var ActionDisabledError = class extends _moostjs_event_http.HttpError {
|
|
1573
1828
|
name = "ActionDisabledError";
|
|
1574
|
-
constructor(action,
|
|
1829
|
+
constructor(action, id, ids) {
|
|
1575
1830
|
const body = {
|
|
1576
1831
|
name: "ActionDisabledError",
|
|
1577
|
-
message: buildMessage(action,
|
|
1832
|
+
message: buildMessage(action, ids),
|
|
1578
1833
|
statusCode: 409,
|
|
1579
1834
|
action
|
|
1580
1835
|
};
|
|
1581
|
-
if (
|
|
1582
|
-
else if (
|
|
1836
|
+
if (ids !== void 0) body.ids = ids;
|
|
1837
|
+
else if (id !== void 0) body.id = id;
|
|
1583
1838
|
super(409, body);
|
|
1584
1839
|
}
|
|
1585
1840
|
};
|
|
1586
1841
|
//#endregion
|
|
1587
|
-
//#region src/actions/
|
|
1588
|
-
|
|
1842
|
+
//#region src/actions/current-action.ts
|
|
1843
|
+
/** Read the current action's `TDbActionMeta` from the wook context. Returns undefined outside a controller (e.g. direct-wook test paths). */
|
|
1844
|
+
function readCurrentActionMeta(ctx) {
|
|
1845
|
+
let ctrl;
|
|
1846
|
+
let methodName;
|
|
1847
|
+
try {
|
|
1848
|
+
const cc = (0, moost.useControllerContext)(ctx);
|
|
1849
|
+
ctrl = cc.getController();
|
|
1850
|
+
methodName = cc.getMethod();
|
|
1851
|
+
} catch {
|
|
1852
|
+
return;
|
|
1853
|
+
}
|
|
1854
|
+
if (!ctrl || !methodName) return void 0;
|
|
1855
|
+
return (0, moost.getMoostMate)().read(ctrl.constructor, methodName)?.[MOOST_DB_ACTION];
|
|
1856
|
+
}
|
|
1857
|
+
//#endregion
|
|
1858
|
+
//#region src/actions/id-validation.ts
|
|
1859
|
+
const SOURCE_CACHE = /* @__PURE__ */ new WeakMap();
|
|
1860
|
+
function getSourceCache(source) {
|
|
1861
|
+
let cache = SOURCE_CACHE.get(source);
|
|
1862
|
+
if (cache) return cache;
|
|
1863
|
+
const identifications = source.getIdentifications();
|
|
1864
|
+
const byKeySig = /* @__PURE__ */ new Map();
|
|
1865
|
+
for (const ident of identifications) byKeySig.set(fieldsSig(ident.fields), ident);
|
|
1866
|
+
const fieldByName = /* @__PURE__ */ new Map();
|
|
1867
|
+
for (const fd of source.fieldDescriptors) fieldByName.set(fd.path, fd);
|
|
1868
|
+
cache = {
|
|
1869
|
+
byKeySig,
|
|
1870
|
+
fieldByName,
|
|
1871
|
+
formatted: identifications.map((id) => `[${id.fields.join(", ")}]`).join(", ")
|
|
1872
|
+
};
|
|
1873
|
+
SOURCE_CACHE.set(source, cache);
|
|
1874
|
+
return cache;
|
|
1875
|
+
}
|
|
1876
|
+
function fieldsSig(fields) {
|
|
1877
|
+
return fields.toSorted().join("");
|
|
1878
|
+
}
|
|
1879
|
+
function isIdValidationSource(value) {
|
|
1589
1880
|
if (!value || typeof value !== "object") return false;
|
|
1590
1881
|
const v = value;
|
|
1591
|
-
return
|
|
1882
|
+
return typeof v.getIdentifications === "function" && Array.isArray(v.fieldDescriptors);
|
|
1592
1883
|
}
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
* composite). Throws {@link ValidatorError} with structured `errors` so the
|
|
1596
|
-
* existing validation interceptor returns HTTP 400.
|
|
1597
|
-
*/
|
|
1598
|
-
function validateSinglePk(body, source, path = "") {
|
|
1599
|
-
const errors = collectPkErrors(body, source, path);
|
|
1884
|
+
function validateSingleId(body, source, path = "") {
|
|
1885
|
+
const errors = collectIdErrors(body, source, path);
|
|
1600
1886
|
if (errors.length > 0) throw new _atscript_typescript_utils.ValidatorError(errors);
|
|
1887
|
+
return body;
|
|
1601
1888
|
}
|
|
1602
|
-
|
|
1603
|
-
* Validate a JSON-decoded body against an array of PK shapes (`@DbActionPKs`).
|
|
1604
|
-
* The body MUST be an array; each element is validated against the PK schema.
|
|
1605
|
-
*/
|
|
1606
|
-
function validateMultiPk(body, source) {
|
|
1889
|
+
function validateMultiId(body, source) {
|
|
1607
1890
|
if (!Array.isArray(body)) throw new _atscript_typescript_utils.ValidatorError([{
|
|
1608
1891
|
path: "",
|
|
1609
|
-
message: "Expected JSON array of
|
|
1892
|
+
message: "Expected JSON array of identifier objects",
|
|
1610
1893
|
details: []
|
|
1611
1894
|
}]);
|
|
1612
1895
|
const errors = [];
|
|
1613
|
-
for (let i = 0; i < body.length; i++) errors.push(...
|
|
1896
|
+
for (let i = 0; i < body.length; i++) errors.push(...collectIdErrors(body[i], source, `[${i}]`));
|
|
1614
1897
|
if (errors.length > 0) throw new _atscript_typescript_utils.ValidatorError(errors);
|
|
1898
|
+
return body;
|
|
1615
1899
|
}
|
|
1616
|
-
function
|
|
1617
|
-
|
|
1618
|
-
|
|
1900
|
+
function collectIdErrors(value, source, pathPrefix) {
|
|
1901
|
+
if (!isPlainObject(value)) return [{
|
|
1902
|
+
path: pathPrefix,
|
|
1903
|
+
message: "Expected JSON object for row identifier",
|
|
1904
|
+
details: []
|
|
1905
|
+
}];
|
|
1906
|
+
const cache = getSourceCache(source);
|
|
1907
|
+
if (cache.byKeySig.size === 0) return [{
|
|
1908
|
+
path: pathPrefix,
|
|
1909
|
+
message: "Table has no identifier configured",
|
|
1910
|
+
details: []
|
|
1911
|
+
}];
|
|
1912
|
+
const match = cache.byKeySig.get(fieldsSig(Object.keys(value)));
|
|
1913
|
+
if (!match) return [{
|
|
1619
1914
|
path: pathPrefix,
|
|
1620
|
-
message:
|
|
1915
|
+
message: `Identifier fields must exactly match one of: ${cache.formatted}`,
|
|
1621
1916
|
details: []
|
|
1622
1917
|
}];
|
|
1623
1918
|
const errors = [];
|
|
1624
|
-
|
|
1625
|
-
const err = checkScalar(value, findFieldDescriptor(source, pkFields[0]), pathPrefix);
|
|
1626
|
-
if (err) errors.push(err);
|
|
1627
|
-
return errors;
|
|
1628
|
-
}
|
|
1629
|
-
if (!isPlainObject(value)) {
|
|
1630
|
-
errors.push({
|
|
1631
|
-
path: pathPrefix,
|
|
1632
|
-
message: "Expected JSON object for composite primary key",
|
|
1633
|
-
details: []
|
|
1634
|
-
});
|
|
1635
|
-
return errors;
|
|
1636
|
-
}
|
|
1637
|
-
for (const fieldName of pkFields) {
|
|
1919
|
+
for (const fieldName of match.fields) {
|
|
1638
1920
|
const sub = pathPrefix ? `${pathPrefix}.${fieldName}` : fieldName;
|
|
1639
|
-
|
|
1640
|
-
errors.push({
|
|
1641
|
-
path: sub,
|
|
1642
|
-
message: `Missing primary-key field "${fieldName}"`,
|
|
1643
|
-
details: []
|
|
1644
|
-
});
|
|
1645
|
-
continue;
|
|
1646
|
-
}
|
|
1647
|
-
const fd = findFieldDescriptor(source, fieldName);
|
|
1648
|
-
const err = checkScalar(value[fieldName], fd, sub);
|
|
1921
|
+
const err = checkScalar(value[fieldName], cache.fieldByName.get(fieldName), sub);
|
|
1649
1922
|
if (err) errors.push(err);
|
|
1650
1923
|
}
|
|
1651
1924
|
return errors;
|
|
1652
1925
|
}
|
|
1653
|
-
function findFieldDescriptor(source, name) {
|
|
1654
|
-
for (const fd of source.fieldDescriptors) if (fd.path === name) return fd;
|
|
1655
|
-
}
|
|
1656
1926
|
function checkScalar(value, fd, path) {
|
|
1657
1927
|
const expected = fd?.designType ?? "string";
|
|
1658
1928
|
if (expected === "string" && typeof value !== "string") return scalarMismatch(path, expected, value);
|
|
@@ -1662,7 +1932,7 @@ function checkScalar(value, fd, path) {
|
|
|
1662
1932
|
function scalarMismatch(path, expected, value) {
|
|
1663
1933
|
return {
|
|
1664
1934
|
path,
|
|
1665
|
-
message: `Expected
|
|
1935
|
+
message: `Expected identifier value to be ${expected}, got ${describe(value)}`,
|
|
1666
1936
|
details: []
|
|
1667
1937
|
};
|
|
1668
1938
|
}
|
|
@@ -1675,105 +1945,115 @@ function isPlainObject(value) {
|
|
|
1675
1945
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1676
1946
|
}
|
|
1677
1947
|
//#endregion
|
|
1678
|
-
//#region src/actions/
|
|
1948
|
+
//#region src/actions/id-cache.ts
|
|
1679
1949
|
const boundTableKey = (0, _wooksjs_event_core.key)("atscript_db_action_bound_table");
|
|
1680
1950
|
function getActionTable(ctx) {
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
if (fromSlot) return fromSlot;
|
|
1684
|
-
}
|
|
1951
|
+
const fromSlot = ctx.has(boundTableKey) ? ctx.get(boundTableKey) : void 0;
|
|
1952
|
+
if (fromSlot) return fromSlot;
|
|
1685
1953
|
const ctrl = (0, moost.useControllerContext)(ctx).getController();
|
|
1686
|
-
|
|
1687
|
-
const t = ctrl.readable ?? ctrl.table;
|
|
1688
|
-
if (t) return t;
|
|
1689
|
-
}
|
|
1690
|
-
return null;
|
|
1954
|
+
return ctrl?.readable ?? ctrl?.table ?? null;
|
|
1691
1955
|
}
|
|
1692
1956
|
function noTableError(ctx) {
|
|
1693
|
-
const
|
|
1694
|
-
const ctrl = cc.getController();
|
|
1695
|
-
const methodName = cc.getMethod();
|
|
1696
|
-
let actionName;
|
|
1697
|
-
if (ctrl && methodName) actionName = (0, moost.getMoostMate)().read(ctrl.constructor, methodName)?.[MOOST_DB_ACTION]?.name;
|
|
1957
|
+
const actionName = readCurrentActionMeta(ctx)?.name;
|
|
1698
1958
|
return new _moostjs_event_http.HttpError(500, `${WARN_PREFIX} ${actionName ? `"${actionName}"` : "<unknown>"}: controller has no readable/table property and the action declares no opts.table. Either expose readable/table on the controller, extend AsDbReadableController, or pass opts.table on @DbAction.`);
|
|
1699
1959
|
}
|
|
1700
|
-
async function
|
|
1960
|
+
async function resolveValidatedId(ctx, validate) {
|
|
1701
1961
|
const table = getActionTable(ctx);
|
|
1702
|
-
if (!
|
|
1962
|
+
if (!isIdValidationSource(table)) throw noTableError(ctx);
|
|
1703
1963
|
const body = await (0, _wooksjs_http_body.useBody)(ctx).parseBody();
|
|
1704
1964
|
validate(body, table);
|
|
1705
1965
|
return body;
|
|
1706
1966
|
}
|
|
1707
|
-
const
|
|
1708
|
-
const
|
|
1709
|
-
return await
|
|
1967
|
+
const dbActionIdSlot = (0, _wooksjs_event_core.cached)((ctx) => resolveValidatedId(ctx, validateSingleId));
|
|
1968
|
+
const dbActionIdsSlot = (0, _wooksjs_event_core.cached)(async (ctx) => {
|
|
1969
|
+
return await resolveValidatedId(ctx, validateMultiId);
|
|
1710
1970
|
});
|
|
1711
|
-
const
|
|
1712
|
-
const
|
|
1971
|
+
const useDbActionId = (0, _wooksjs_event_core.defineWook)((ctx) => ({ load: () => ctx.get(dbActionIdSlot) }));
|
|
1972
|
+
const useDbActionIds = (0, _wooksjs_event_core.defineWook)((ctx) => ({ load: () => ctx.get(dbActionIdsSlot) }));
|
|
1713
1973
|
//#endregion
|
|
1714
1974
|
//#region src/actions/row-cache.ts
|
|
1715
1975
|
function asFetchTable(value) {
|
|
1716
1976
|
if (!value || typeof value !== "object") return null;
|
|
1717
1977
|
const v = value;
|
|
1718
|
-
|
|
1978
|
+
if (Array.isArray(v.primaryKeys) && typeof v.findOne === "function" && typeof v.findMany === "function") return v;
|
|
1979
|
+
return null;
|
|
1980
|
+
}
|
|
1981
|
+
function stringifyScalar(value) {
|
|
1982
|
+
if (value === null) return "null";
|
|
1983
|
+
if (value === void 0) return "undefined";
|
|
1984
|
+
return String(value);
|
|
1985
|
+
}
|
|
1986
|
+
/** Returns the action's `requiredFields` or null when called outside a controller context (e.g. direct wook usage in tests). */
|
|
1987
|
+
function readActionFieldSet(ctx) {
|
|
1988
|
+
const action = readCurrentActionMeta(ctx);
|
|
1989
|
+
if (!action) return null;
|
|
1990
|
+
const opts = action.opts;
|
|
1991
|
+
return Array.isArray(opts.requiredFields) ? opts.requiredFields : null;
|
|
1719
1992
|
}
|
|
1720
|
-
function
|
|
1721
|
-
|
|
1993
|
+
function seedActionFields(ctx, table) {
|
|
1994
|
+
const fields = /* @__PURE__ */ new Set();
|
|
1995
|
+
for (const f of table.preferredId ?? table.primaryKeys) fields.add(f);
|
|
1996
|
+
const action = readActionFieldSet(ctx);
|
|
1997
|
+
if (action) for (const f of action) fields.add(f);
|
|
1998
|
+
return fields;
|
|
1722
1999
|
}
|
|
1723
2000
|
async function loadRow(ctx) {
|
|
1724
|
-
const
|
|
1725
|
-
const
|
|
1726
|
-
if (
|
|
2001
|
+
const id = await ctx.get(dbActionIdSlot);
|
|
2002
|
+
const table = asFetchTable(getActionTable(ctx));
|
|
2003
|
+
if (!table) throw noTableError(ctx);
|
|
2004
|
+
const fields = seedActionFields(ctx, table);
|
|
2005
|
+
for (const k of Object.keys(id)) fields.add(k);
|
|
2006
|
+
const row = await table.findOne({
|
|
2007
|
+
filter: id,
|
|
2008
|
+
controls: { $select: [...fields] }
|
|
2009
|
+
});
|
|
2010
|
+
if (row == null) throw new _moostjs_event_http.HttpError(404, "Row not found for action identifier");
|
|
1727
2011
|
return row;
|
|
1728
2012
|
}
|
|
1729
2013
|
async function loadRows(ctx) {
|
|
1730
|
-
const
|
|
1731
|
-
const table = asFetchTable(getActionTable(ctx))
|
|
1732
|
-
if (
|
|
1733
|
-
|
|
1734
|
-
const
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
2014
|
+
const ids = await ctx.get(dbActionIdsSlot);
|
|
2015
|
+
const table = asFetchTable(getActionTable(ctx));
|
|
2016
|
+
if (!table) throw noTableError(ctx);
|
|
2017
|
+
if (ids.length === 0) return [];
|
|
2018
|
+
const fields = seedActionFields(ctx, table);
|
|
2019
|
+
const idKeys = [];
|
|
2020
|
+
const shapes = /* @__PURE__ */ new Map();
|
|
2021
|
+
const dedupedIds = [];
|
|
2022
|
+
const seenKeys = /* @__PURE__ */ new Set();
|
|
2023
|
+
for (const id of ids) {
|
|
2024
|
+
const sortedFields = Object.keys(id).toSorted();
|
|
2025
|
+
const sig = sortedFields.join("");
|
|
2026
|
+
let key = "";
|
|
2027
|
+
for (const f of sortedFields) {
|
|
2028
|
+
fields.add(f);
|
|
2029
|
+
key += `${f}\x1f${stringifyScalar(id[f])}\x1e`;
|
|
2030
|
+
}
|
|
2031
|
+
idKeys.push(key);
|
|
2032
|
+
if (!shapes.has(sig)) shapes.set(sig, sortedFields);
|
|
2033
|
+
if (!seenKeys.has(key)) {
|
|
2034
|
+
seenKeys.add(key);
|
|
2035
|
+
dedupedIds.push(id);
|
|
1743
2036
|
}
|
|
1744
|
-
return ordered;
|
|
1745
|
-
}
|
|
1746
|
-
const byKey = /* @__PURE__ */ new Map();
|
|
1747
|
-
for (const row of rows) byKey.set(compositeKey(row, primaryKeys), row);
|
|
1748
|
-
const ordered = [];
|
|
1749
|
-
for (const pk of pks) {
|
|
1750
|
-
const found = byKey.get(compositeKey(pk, primaryKeys));
|
|
1751
|
-
if (found !== void 0) ordered.push(found);
|
|
1752
|
-
}
|
|
1753
|
-
return ordered;
|
|
1754
|
-
}
|
|
1755
|
-
function buildPksFilter(pks, primaryKeys) {
|
|
1756
|
-
if (primaryKeys.length === 1) return { [primaryKeys[0]]: { $in: pks } };
|
|
1757
|
-
return { $or: pks.map((pk) => {
|
|
1758
|
-
const obj = pk;
|
|
1759
|
-
const clause = {};
|
|
1760
|
-
for (const field of primaryKeys) clause[field] = obj[field];
|
|
1761
|
-
return clause;
|
|
1762
|
-
}) };
|
|
1763
|
-
}
|
|
1764
|
-
function compositeKey(obj, primaryKeys) {
|
|
1765
|
-
let out = "";
|
|
1766
|
-
for (const f of primaryKeys) {
|
|
1767
|
-
if (out !== "") out += "\0";
|
|
1768
|
-
const v = obj[f];
|
|
1769
|
-
if (v === null) out += "n";
|
|
1770
|
-
else if (v === void 0) out += "u";
|
|
1771
|
-
else if (typeof v === "string") out += `s\x02${v}`;
|
|
1772
|
-
else if (typeof v === "number") out += `n\x02${v}`;
|
|
1773
|
-
else if (typeof v === "boolean") out += `b\x02${v}`;
|
|
1774
|
-
else out += `j\x02${JSON.stringify(v)}`;
|
|
1775
2037
|
}
|
|
1776
|
-
|
|
2038
|
+
const rows = await table.findMany({
|
|
2039
|
+
filter: { $or: dedupedIds },
|
|
2040
|
+
controls: { $select: [...fields] }
|
|
2041
|
+
});
|
|
2042
|
+
const rowByKey = /* @__PURE__ */ new Map();
|
|
2043
|
+
for (const row of rows) for (const sortedFields of shapes.values()) {
|
|
2044
|
+
let key = "";
|
|
2045
|
+
let ok = true;
|
|
2046
|
+
for (const f of sortedFields) {
|
|
2047
|
+
const v = row[f];
|
|
2048
|
+
if (v === void 0) {
|
|
2049
|
+
ok = false;
|
|
2050
|
+
break;
|
|
2051
|
+
}
|
|
2052
|
+
key += `${f}\x1f${stringifyScalar(v)}\x1e`;
|
|
2053
|
+
}
|
|
2054
|
+
if (ok && !rowByKey.has(key)) rowByKey.set(key, row);
|
|
2055
|
+
}
|
|
2056
|
+
return ids.map((_, i) => rowByKey.get(idKeys[i]));
|
|
1777
2057
|
}
|
|
1778
2058
|
const dbActionRowSlot = (0, _wooksjs_event_core.cached)((ctx) => loadRow(ctx));
|
|
1779
2059
|
const dbActionRowsSlot = (0, _wooksjs_event_core.cached)((ctx) => loadRows(ctx));
|
|
@@ -1798,28 +2078,38 @@ function buildGateInterceptor(opts) {
|
|
|
1798
2078
|
injectBoundTable(table);
|
|
1799
2079
|
const ctx = (0, _wooksjs_event_core.current)();
|
|
1800
2080
|
if (level === "row") {
|
|
1801
|
-
|
|
2081
|
+
const verdicts = disabled([await ctx.get(dbActionRowSlot)]);
|
|
2082
|
+
assertVerdictLength(action, verdicts, 1);
|
|
2083
|
+
if (verdicts[0]) throw new ActionDisabledError(action, await ctx.get(dbActionIdSlot));
|
|
1802
2084
|
return;
|
|
1803
2085
|
}
|
|
1804
|
-
const
|
|
2086
|
+
const ids = await ctx.get(dbActionIdsSlot);
|
|
1805
2087
|
const rows = await ctx.get(dbActionRowsSlot);
|
|
1806
|
-
const
|
|
2088
|
+
const existingRows = [];
|
|
2089
|
+
for (const row of rows) if (row !== void 0) existingRows.push(row);
|
|
2090
|
+
const verdicts = disabled(existingRows);
|
|
2091
|
+
assertVerdictLength(action, verdicts, existingRows.length);
|
|
2092
|
+
const failingIds = [];
|
|
1807
2093
|
const passingRows = [];
|
|
1808
|
-
const
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
2094
|
+
const passingIds = [];
|
|
2095
|
+
let verdictIndex = 0;
|
|
2096
|
+
for (let i = 0; i < ids.length; i++) {
|
|
2097
|
+
const row = rows[i];
|
|
2098
|
+
if (row === void 0 || verdicts[verdictIndex++]) failingIds.push(ids[i]);
|
|
2099
|
+
else {
|
|
2100
|
+
passingRows.push(row);
|
|
2101
|
+
passingIds.push(ids[i]);
|
|
2102
|
+
}
|
|
1813
2103
|
}
|
|
1814
2104
|
if (onDisabledRows === "skip") {
|
|
1815
|
-
if (passingRows.length === 0) throw new ActionDisabledError(action, void 0, [...
|
|
1816
|
-
if (
|
|
2105
|
+
if (passingRows.length === 0) throw new ActionDisabledError(action, void 0, [...ids]);
|
|
2106
|
+
if (failingIds.length > 0) {
|
|
1817
2107
|
ctx.set(dbActionRowsSlot, Promise.resolve(passingRows));
|
|
1818
|
-
ctx.set(
|
|
2108
|
+
ctx.set(dbActionIdsSlot, Promise.resolve(passingIds));
|
|
1819
2109
|
}
|
|
1820
2110
|
return;
|
|
1821
2111
|
}
|
|
1822
|
-
if (
|
|
2112
|
+
if (failingIds.length > 0) throw new ActionDisabledError(action, void 0, failingIds);
|
|
1823
2113
|
}, GATE_PRIORITY);
|
|
1824
2114
|
}
|
|
1825
2115
|
/** Thin interceptor for `@DbActionRow*` without `disabled` — injects only the bound table. */
|
|
@@ -1837,6 +2127,11 @@ function buildThinInterceptor(opts) {
|
|
|
1837
2127
|
* (gate when `disabled` is set, thin bound-table injector when only
|
|
1838
2128
|
* `@DbActionRow*` is present). Stacking two `@DbAction` on the same method
|
|
1839
2129
|
* is undefined and emits a warning.
|
|
2130
|
+
*
|
|
2131
|
+
* Generic over `TRow` (annotate at the call site: `@DbAction<Order>(...)`)
|
|
2132
|
+
* and `R` (the literal `requiredFields` tuple, inferred via `const R`).
|
|
2133
|
+
* The `disabled` predicate's `rows` argument is type-narrowed to
|
|
2134
|
+
* `Pick<FlatOf<TRow>, R[number]>[]`.
|
|
1840
2135
|
*/
|
|
1841
2136
|
function DbAction(name, opts = {}) {
|
|
1842
2137
|
const mate = (0, moost.getMoostMate)();
|
|
@@ -1855,14 +2150,15 @@ function DbAction(name, opts = {}) {
|
|
|
1855
2150
|
})(target, propertyKey, descriptor);
|
|
1856
2151
|
if (isAsValueHelpControllerSubclass(typeof target === "function" ? target : target.constructor)) return descriptor;
|
|
1857
2152
|
const scan = scanParamLevel(mate.read(target, propertyKey)?.params ?? []);
|
|
1858
|
-
|
|
2153
|
+
const rawOpts = opts;
|
|
2154
|
+
if (typeof rawOpts.disabled === "function" && (scan.level === "row" || scan.level === "rows")) (0, moost.Intercept)(buildGateInterceptor({
|
|
1859
2155
|
action: name,
|
|
1860
2156
|
level: scan.level,
|
|
1861
|
-
disabled:
|
|
1862
|
-
onDisabledRows:
|
|
1863
|
-
table:
|
|
2157
|
+
disabled: rawOpts.disabled,
|
|
2158
|
+
onDisabledRows: rawOpts.onDisabledRows ?? "reject",
|
|
2159
|
+
table: rawOpts.table
|
|
1864
2160
|
}))(target, propertyKey, descriptor);
|
|
1865
|
-
else if (scan.hasRowParam) (0, moost.Intercept)(buildThinInterceptor({ table:
|
|
2161
|
+
else if (scan.hasRowParam) (0, moost.Intercept)(buildThinInterceptor({ table: rawOpts.table }))(target, propertyKey, descriptor);
|
|
1866
2162
|
return descriptor;
|
|
1867
2163
|
});
|
|
1868
2164
|
}
|
|
@@ -1882,62 +2178,70 @@ function DbActionDefault() {
|
|
|
1882
2178
|
});
|
|
1883
2179
|
}
|
|
1884
2180
|
//#endregion
|
|
1885
|
-
//#region src/actions/
|
|
2181
|
+
//#region src/actions/id-source.ts
|
|
1886
2182
|
/**
|
|
1887
|
-
* Build a parameter decorator that reads its value from the cached
|
|
2183
|
+
* Build a parameter decorator that reads its value from the cached ID wook
|
|
1888
2184
|
* (single or multi). Validation runs inside the wook factory exactly once
|
|
1889
|
-
* per request, regardless of how many readers consume the value (`@
|
|
2185
|
+
* per request, regardless of how many readers consume the value (`@DbActionID*`
|
|
1890
2186
|
* resolver, gate interceptor, cached row wook, in-handler composables).
|
|
1891
2187
|
*
|
|
1892
2188
|
* Marks the param so {@link discoverActions} can infer the action's `level`.
|
|
1893
2189
|
*/
|
|
1894
|
-
function
|
|
2190
|
+
function createIdParamDecorator(kind) {
|
|
1895
2191
|
const mate = (0, moost.getMoostMate)();
|
|
1896
|
-
const
|
|
1897
|
-
const
|
|
1898
|
-
return (0, moost.ApplyDecorators)(mate.decorate(MOOST_DB_ACTION_PARAM, kind), (0, moost.Resolve)(
|
|
2192
|
+
const resolverName = kind === "id" ? "dbActionId" : "dbActionIds";
|
|
2193
|
+
const resolver = kind === "id" ? async () => (0, _wooksjs_event_core.current)().get(dbActionIdSlot) : async () => (0, _wooksjs_event_core.current)().get(dbActionIdsSlot);
|
|
2194
|
+
return (0, moost.ApplyDecorators)(mate.decorate(MOOST_DB_ACTION_PARAM, kind), (0, moost.Resolve)(resolver, resolverName));
|
|
1899
2195
|
}
|
|
1900
2196
|
//#endregion
|
|
1901
|
-
//#region src/actions/db-action-
|
|
2197
|
+
//#region src/actions/db-action-id.decorator.ts
|
|
1902
2198
|
/**
|
|
1903
|
-
* Parameter resolver that reads
|
|
1904
|
-
* and validates it against the bound table's
|
|
2199
|
+
* Parameter resolver that reads a row identifier from the JSON request body
|
|
2200
|
+
* and validates it against the bound table's legitimate identifiers.
|
|
2201
|
+
*
|
|
2202
|
+
* Body shape is always a JSON object — no scalar form. The object's key set
|
|
2203
|
+
* MUST exactly match one of the table's legitimate identifications:
|
|
1905
2204
|
*
|
|
1906
|
-
* - Single-field PK →
|
|
1907
|
-
* - Composite PK →
|
|
2205
|
+
* - Single-field PK → `{ id: "abc" }` (or whatever the PK prop is named).
|
|
2206
|
+
* - Composite PK → `{ tenantId: "...", userId: "..." }`.
|
|
2207
|
+
* - Single-field unique index → `{ slug: "alpha" }`.
|
|
2208
|
+
* - Compound unique index → `{ tenantId: "...", slug: "..." }`.
|
|
1908
2209
|
*
|
|
1909
|
-
*
|
|
2210
|
+
* Strict — unknown fields are rejected, no type coercion. Mismatches throw a
|
|
1910
2211
|
* `ValidatorError` which the existing validation interceptor surfaces as
|
|
1911
2212
|
* HTTP 400 with the same envelope as DTO failures.
|
|
1912
2213
|
*
|
|
1913
2214
|
* Marks the param so {@link discoverActions} can infer the action's `level`
|
|
1914
2215
|
* as `'row'`.
|
|
1915
2216
|
*
|
|
1916
|
-
* Implementation note: the resolver is a thin reader of the cached
|
|
2217
|
+
* Implementation note: the resolver is a thin reader of the cached ID wook
|
|
1917
2218
|
* — validation logic lives in the wook factory, which runs once per request
|
|
1918
2219
|
* regardless of how many readers consume the value.
|
|
1919
2220
|
*/
|
|
1920
|
-
function
|
|
1921
|
-
return
|
|
2221
|
+
function DbActionID() {
|
|
2222
|
+
return createIdParamDecorator("id");
|
|
1922
2223
|
}
|
|
1923
2224
|
//#endregion
|
|
1924
|
-
//#region src/actions/db-action-
|
|
2225
|
+
//#region src/actions/db-action-ids.decorator.ts
|
|
1925
2226
|
/**
|
|
1926
|
-
* Parameter resolver that reads a JSON array of
|
|
1927
|
-
* body and validates each entry against the bound table
|
|
2227
|
+
* Parameter resolver that reads a JSON array of row identifiers from the
|
|
2228
|
+
* request body and validates each entry against the bound table.
|
|
1928
2229
|
*
|
|
1929
|
-
*
|
|
1930
|
-
*
|
|
2230
|
+
* Body shape is always a JSON array of objects — no scalar form. Each
|
|
2231
|
+
* element's key set MUST exactly match one of the table's legitimate
|
|
2232
|
+
* identifications (PK or any unique index). Elements MAY mix shapes:
|
|
2233
|
+
* `[{ id: "1" }, { slug: "alpha" }]` is valid when both `id` is the PK
|
|
2234
|
+
* and `slug` is a unique index.
|
|
1931
2235
|
*
|
|
1932
|
-
*
|
|
1933
|
-
* {@link discoverActions} can infer the action's `level` as `'rows'`.
|
|
2236
|
+
* Strict — unknown fields are rejected, no type coercion. Marks the param
|
|
2237
|
+
* so {@link discoverActions} can infer the action's `level` as `'rows'`.
|
|
1934
2238
|
*
|
|
1935
2239
|
* In `'rows'` skip mode the resolved value reflects the gate interceptor's
|
|
1936
|
-
* filtered subset (the cached
|
|
1937
|
-
* {@link
|
|
2240
|
+
* filtered subset (the cached ID slot is overwritten in place); see
|
|
2241
|
+
* {@link dbActionIdsSlot} for precedence details.
|
|
1938
2242
|
*/
|
|
1939
|
-
function
|
|
1940
|
-
return
|
|
2243
|
+
function DbActionIDs() {
|
|
2244
|
+
return createIdParamDecorator("ids");
|
|
1941
2245
|
}
|
|
1942
2246
|
//#endregion
|
|
1943
2247
|
//#region src/actions/db-action-row.decorator.ts
|
|
@@ -1945,9 +2249,8 @@ function createRowParamDecorator(metaKey, slot, resolverName) {
|
|
|
1945
2249
|
return (0, moost.ApplyDecorators)((0, moost.getMoostMate)().decorate(metaKey, true), (0, moost.Resolve)(async () => (0, _wooksjs_event_core.current)().get(slot), resolverName));
|
|
1946
2250
|
}
|
|
1947
2251
|
/**
|
|
1948
|
-
* Parameter decorator that injects the row whose
|
|
1949
|
-
* request body.
|
|
1950
|
-
* shared with the gate interceptor when `disabled` is set).
|
|
2252
|
+
* Parameter decorator that injects the row whose identifier was supplied in
|
|
2253
|
+
* the request body.
|
|
1951
2254
|
*
|
|
1952
2255
|
* Marks the param so {@link discoverActions} infers the action's `level` as
|
|
1953
2256
|
* `'row'`. Co-occurrence with `@DbActionRows()` (or any multi-cardinality
|
|
@@ -1960,8 +2263,8 @@ function DbActionRow() {
|
|
|
1960
2263
|
return createRowParamDecorator(MOOST_DB_ACTION_ROW, dbActionRowSlot, "dbActionRow");
|
|
1961
2264
|
}
|
|
1962
2265
|
/**
|
|
1963
|
-
* Parameter decorator that injects the rows
|
|
1964
|
-
*
|
|
2266
|
+
* Parameter decorator that injects the rows fetched by the identifiers
|
|
2267
|
+
* supplied in the request body.
|
|
1965
2268
|
*
|
|
1966
2269
|
* Marks the param so {@link discoverActions} infers the action's `level` as
|
|
1967
2270
|
* `'rows'`. In `'rows'` + `'skip'` mode the resolved value contains only the
|
|
@@ -1979,10 +2282,9 @@ function DbActionRows() {
|
|
|
1979
2282
|
* the level-pinned shortcuts (`@DbTableActions`, `@DbRowActions`,
|
|
1980
2283
|
* `@DbRowsActions`) to avoid repeating `level`.
|
|
1981
2284
|
*
|
|
1982
|
-
*
|
|
1983
|
-
*
|
|
1984
|
-
* `
|
|
1985
|
-
* `@Post`-bound endpoint accepting the level-determined body shape.
|
|
2285
|
+
* Generic over `TRow` (annotate at the call site: `@DbActions<Order>(...)`)
|
|
2286
|
+
* and `D` (the literal dict, captured via `const D`). Each entry's
|
|
2287
|
+
* `disabled` predicate is type-narrowed by its own `requiredFields` literal.
|
|
1986
2288
|
*
|
|
1987
2289
|
* Multiple `@DbActions` (and shortcut) decorators on the same class
|
|
1988
2290
|
* accumulate.
|
|
@@ -2023,6 +2325,21 @@ function classLevelActions(dict, forcedLevel) {
|
|
|
2023
2325
|
});
|
|
2024
2326
|
}
|
|
2025
2327
|
//#endregion
|
|
2328
|
+
//#region src/actions/per-row.ts
|
|
2329
|
+
/**
|
|
2330
|
+
* Lift a per-row predicate into the batch shape required by
|
|
2331
|
+
* `@DbAction` opts.`disabled` and class-level dict `disabled`. Polarity is
|
|
2332
|
+
* preserved — `true` from `fn` means the action is disabled for that row.
|
|
2333
|
+
*
|
|
2334
|
+
* ```ts
|
|
2335
|
+
* @DbAction<Order>('archive', {
|
|
2336
|
+
* requiredFields: ['status'],
|
|
2337
|
+
* disabled: perRow(r => r.status === 'archived'),
|
|
2338
|
+
* })
|
|
2339
|
+
* ```
|
|
2340
|
+
*/
|
|
2341
|
+
const perRow = (fn) => (rows) => rows.map(fn);
|
|
2342
|
+
//#endregion
|
|
2026
2343
|
exports.ActionDisabledError = ActionDisabledError;
|
|
2027
2344
|
Object.defineProperty(exports, "AsDbController", {
|
|
2028
2345
|
enumerable: true,
|
|
@@ -2056,8 +2373,8 @@ Object.defineProperty(exports, "AsValueHelpController", {
|
|
|
2056
2373
|
});
|
|
2057
2374
|
exports.DbAction = DbAction;
|
|
2058
2375
|
exports.DbActionDefault = DbActionDefault;
|
|
2059
|
-
exports.
|
|
2060
|
-
exports.
|
|
2376
|
+
exports.DbActionID = DbActionID;
|
|
2377
|
+
exports.DbActionIDs = DbActionIDs;
|
|
2061
2378
|
exports.DbActionRow = DbActionRow;
|
|
2062
2379
|
exports.DbActionRows = DbActionRows;
|
|
2063
2380
|
exports.DbActions = DbActions;
|
|
@@ -2074,8 +2391,9 @@ exports.TableController = TableController;
|
|
|
2074
2391
|
exports.UseValidationErrorTransform = UseValidationErrorTransform;
|
|
2075
2392
|
exports.ViewController = ViewController;
|
|
2076
2393
|
exports.discoverActions = discoverActions;
|
|
2077
|
-
exports.
|
|
2078
|
-
exports.
|
|
2394
|
+
exports.perRow = perRow;
|
|
2395
|
+
exports.useDbActionId = useDbActionId;
|
|
2396
|
+
exports.useDbActionIds = useDbActionIds;
|
|
2079
2397
|
exports.useDbActionRow = useDbActionRow;
|
|
2080
2398
|
exports.useDbActionRows = useDbActionRows;
|
|
2081
2399
|
exports.validationErrorTransform = validationErrorTransform;
|