@atscript/moost-db 0.1.56 → 0.1.58
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/dist/index.cjs +501 -135
- package/dist/index.d.cts +214 -42
- package/dist/index.d.mts +214 -42
- package/dist/index.mjs +493 -137
- package/package.json +5 -2
package/dist/index.mjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { ValidatorError, defineAnnotatedType, serializeAnnotatedType, throwFeatureDisabled } from "@atscript/typescript/utils";
|
|
2
2
|
import { Body, Delete, Get, HttpError, Patch, Post, Put, Query, Url } from "@moostjs/event-http";
|
|
3
|
-
import { ApplyDecorators, Controller, Inherit, Inject, Intercept, Moost, Param, Provide, Resolve, TInterceptorPriority, defineInterceptor, getMoostMate, useControllerContext } from "moost";
|
|
3
|
+
import { ApplyDecorators, Controller, Inherit, Inject, Intercept, Moost, Param, Provide, Resolve, TInterceptorPriority, defineBeforeInterceptor, defineInterceptor, getMoostMate, useControllerContext } from "moost";
|
|
4
4
|
import { parseUrl } from "@uniqu/url";
|
|
5
5
|
import { DbError } from "@atscript/db";
|
|
6
|
+
import { cached, current, defineWook, key } from "@wooksjs/event-core";
|
|
6
7
|
import { useBody } from "@wooksjs/http-body";
|
|
7
8
|
//#region src/validation-interceptor.ts
|
|
8
9
|
const dbErrorCodeToStatus = { CONFLICT: 409 };
|
|
@@ -180,13 +181,39 @@ function findSortOffender(sort, isAllowed) {
|
|
|
180
181
|
}
|
|
181
182
|
}
|
|
182
183
|
//#endregion
|
|
184
|
+
//#region src/actions/controller-registry.ts
|
|
185
|
+
let asDbReadableCtor = null;
|
|
186
|
+
let asValueHelpCtor = null;
|
|
187
|
+
function registerAsDbReadableController(ctor) {
|
|
188
|
+
asDbReadableCtor = ctor;
|
|
189
|
+
}
|
|
190
|
+
function registerAsValueHelpController(ctor) {
|
|
191
|
+
asValueHelpCtor = ctor;
|
|
192
|
+
}
|
|
193
|
+
function isAsDbReadableControllerSubclass(ctor) {
|
|
194
|
+
if (!asDbReadableCtor) return false;
|
|
195
|
+
return asDbReadableCtor.prototype.isPrototypeOf(ctor.prototype);
|
|
196
|
+
}
|
|
197
|
+
function isAsValueHelpControllerSubclass(ctor) {
|
|
198
|
+
if (!asValueHelpCtor) return false;
|
|
199
|
+
return asValueHelpCtor.prototype.isPrototypeOf(ctor.prototype);
|
|
200
|
+
}
|
|
201
|
+
function isAsDbReadableControllerInstance(value) {
|
|
202
|
+
return !!asDbReadableCtor && value instanceof asDbReadableCtor;
|
|
203
|
+
}
|
|
204
|
+
//#endregion
|
|
183
205
|
//#region src/actions/keys.ts
|
|
206
|
+
/** Log-message prefix for warnings emitted from the actions subsystem. */
|
|
207
|
+
const WARN_PREFIX = "[moost-db actions]";
|
|
184
208
|
/** Method-level metadata key — written by `@DbAction(name, opts)`. */
|
|
185
209
|
const MOOST_DB_ACTION = "atscript_db_action";
|
|
186
210
|
/** Class-level metadata key — written by `@DbActions` and the level-pinned shortcuts. Stored as an array; decorators accumulate. */
|
|
187
211
|
const MOOST_DB_ACTIONS = "atscript_db_actions";
|
|
188
212
|
/** Param-level metadata key — written by `@DbActionPK()` / `@DbActionPKs()`. Drives level inference. */
|
|
189
213
|
const MOOST_DB_ACTION_PARAM = "atscript_db_action_param";
|
|
214
|
+
/** Param-level marker keys — written by `@DbActionRow()` / `@DbActionRows()`. */
|
|
215
|
+
const MOOST_DB_ACTION_ROW = "atscript_db_action_row";
|
|
216
|
+
const MOOST_DB_ACTION_ROWS = "atscript_db_action_rows";
|
|
190
217
|
/**
|
|
191
218
|
* Shared method-decorator update used by `@DbAction` and `@DbActionDefault`:
|
|
192
219
|
* read the existing `MOOST_DB_ACTION` slot, merge the patch (later-applied
|
|
@@ -204,17 +231,50 @@ function mergeActionMeta(current, patch) {
|
|
|
204
231
|
};
|
|
205
232
|
}
|
|
206
233
|
//#endregion
|
|
234
|
+
//#region src/actions/param-level.ts
|
|
235
|
+
function scanParamLevel(params) {
|
|
236
|
+
let single = false;
|
|
237
|
+
let multi = false;
|
|
238
|
+
let hasRowParam = false;
|
|
239
|
+
let hasBody = false;
|
|
240
|
+
for (const p of params) {
|
|
241
|
+
const kind = p[MOOST_DB_ACTION_PARAM];
|
|
242
|
+
if (kind === "pk") single = true;
|
|
243
|
+
else if (kind === "pks") multi = true;
|
|
244
|
+
if (p["atscript_db_action_row"]) {
|
|
245
|
+
single = true;
|
|
246
|
+
hasRowParam = true;
|
|
247
|
+
}
|
|
248
|
+
if (p["atscript_db_action_rows"]) {
|
|
249
|
+
multi = true;
|
|
250
|
+
hasRowParam = true;
|
|
251
|
+
}
|
|
252
|
+
if (p.paramSource === "BODY") hasBody = true;
|
|
253
|
+
}
|
|
254
|
+
return {
|
|
255
|
+
level: single && multi ? "table" : single ? "row" : multi ? "rows" : "table",
|
|
256
|
+
single,
|
|
257
|
+
multi,
|
|
258
|
+
hasRowParam,
|
|
259
|
+
hasBody
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
//#endregion
|
|
207
263
|
//#region src/actions/discover.ts
|
|
208
|
-
/**
|
|
264
|
+
/**
|
|
265
|
+
* Pure structural-copy fields. `disabled` and `requiredFields` are handled
|
|
266
|
+
* as special cases in {@link emitInfo} so the function-to-string transform
|
|
267
|
+
* stays out of the copy loop.
|
|
268
|
+
*/
|
|
209
269
|
const OPTIONAL_FIELDS = [
|
|
210
270
|
"icon",
|
|
211
271
|
"intent",
|
|
212
272
|
"description",
|
|
213
273
|
"order",
|
|
214
274
|
"default",
|
|
215
|
-
"promptText"
|
|
275
|
+
"promptText",
|
|
276
|
+
"shortcut"
|
|
216
277
|
];
|
|
217
|
-
const WARN_PREFIX = "[moost-db actions]";
|
|
218
278
|
const actionsCache = /* @__PURE__ */ new WeakMap();
|
|
219
279
|
/**
|
|
220
280
|
* Discover all actions declared on a controller and produce the `/meta` array.
|
|
@@ -255,9 +315,21 @@ function collectMethodActions(ctor, overview, logger, out) {
|
|
|
255
315
|
const levelInfer = inferMethodLevel(methodMeta.params ?? [], action.name, logger);
|
|
256
316
|
if (!levelInfer) continue;
|
|
257
317
|
if (levelInfer.bodyConflict) {
|
|
258
|
-
logger.warn(`${WARN_PREFIX} action "${action.name}" cannot mix @DbActionPK*/@DbActionPKs with @Body() — dropping`);
|
|
318
|
+
logger.warn(`${WARN_PREFIX} action "${action.name}" cannot mix @DbActionPK*/@DbActionPKs/@DbActionRow*/@DbActionRows with @Body() — dropping`);
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
if (levelInfer.level === "table" && action.opts.disabled !== void 0) {
|
|
322
|
+
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`);
|
|
259
323
|
continue;
|
|
260
324
|
}
|
|
325
|
+
if (action.opts.disabled !== void 0 || levelInfer.hasRowParam) {
|
|
326
|
+
const extendsReadable = isAsDbReadableControllerSubclass(ctor);
|
|
327
|
+
const hasOptsTable = action.opts.table != null;
|
|
328
|
+
if (!extendsReadable && !hasOptsTable) {
|
|
329
|
+
logger.warn(`${WARN_PREFIX} action "${action.name}" declares a gate or row injection but the controller does not extend AsDbReadableController and \`opts.table\` is not provided. Either extend AsDbReadableController / AsDbController or pass \`opts.table\` on @DbAction — dropping`);
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
261
333
|
const postEntry = handlers.find((h) => h.handler.type === "HTTP" && h.handler.method === "POST");
|
|
262
334
|
if (!postEntry) {
|
|
263
335
|
logger.warn(`${WARN_PREFIX} action "${action.name}" requires @Post(...); no POST handler bound to ${methodName} — dropping`);
|
|
@@ -280,40 +352,32 @@ function collectMethodActions(ctor, overview, logger, out) {
|
|
|
280
352
|
processor: "backend",
|
|
281
353
|
value: path
|
|
282
354
|
};
|
|
283
|
-
|
|
355
|
+
emitInfo(info, action.opts, action.name, logger);
|
|
284
356
|
out.push(info);
|
|
285
357
|
}
|
|
286
358
|
}
|
|
287
359
|
function inferMethodLevel(params, actionName, logger) {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
for (const p of params) {
|
|
292
|
-
const kind = p[MOOST_DB_ACTION_PARAM];
|
|
293
|
-
if (kind === "pk") hasPk = true;
|
|
294
|
-
else if (kind === "pks") hasPks = true;
|
|
295
|
-
if (p.paramSource === "BODY") hasBody = true;
|
|
296
|
-
}
|
|
297
|
-
if (hasPk && hasPks) {
|
|
298
|
-
logger.warn(`${WARN_PREFIX} action "${actionName}" has both @DbActionPK and @DbActionPKs — dropping`);
|
|
360
|
+
const scan = scanParamLevel(params);
|
|
361
|
+
if (scan.single && scan.multi) {
|
|
362
|
+
logger.warn(`${WARN_PREFIX} action "${actionName}" mixes single-cardinality and multi-cardinality decorators (@DbActionPK / @DbActionRow vs @DbActionPKs / @DbActionRows) — dropping`);
|
|
299
363
|
return null;
|
|
300
364
|
}
|
|
301
|
-
const level = hasPk ? "row" : hasPks ? "rows" : "table";
|
|
302
365
|
return {
|
|
303
|
-
level,
|
|
304
|
-
bodyConflict: hasBody && level !== "table"
|
|
366
|
+
level: scan.level,
|
|
367
|
+
bodyConflict: scan.hasBody && scan.level !== "table",
|
|
368
|
+
hasRowParam: scan.hasRowParam
|
|
305
369
|
};
|
|
306
370
|
}
|
|
307
371
|
function collectClassActions(ctor, logger, out) {
|
|
308
372
|
const list = getMoostMate().read(ctor)?.[MOOST_DB_ACTIONS];
|
|
309
373
|
if (!list) return;
|
|
310
|
-
for (const { name, entry
|
|
311
|
-
const built = buildClassEntry(name, entry,
|
|
374
|
+
for (const { name, entry } of list) {
|
|
375
|
+
const built = buildClassEntry(name, entry, logger);
|
|
312
376
|
if (built) out.push(built);
|
|
313
377
|
}
|
|
314
378
|
}
|
|
315
|
-
function buildClassEntry(name, entry,
|
|
316
|
-
const level =
|
|
379
|
+
function buildClassEntry(name, entry, logger) {
|
|
380
|
+
const level = entry.level;
|
|
317
381
|
if (!level) {
|
|
318
382
|
logger.warn(`${WARN_PREFIX} class-level action "${name}" requires a level — dropping. Use @DbTableActions/@DbRowActions/@DbRowsActions or set "level" explicitly.`);
|
|
319
383
|
return null;
|
|
@@ -322,6 +386,10 @@ function buildClassEntry(name, entry, forcedLevel, logger) {
|
|
|
322
386
|
logger.warn(`${WARN_PREFIX} class-level action "${name}" requires a label — dropping`);
|
|
323
387
|
return null;
|
|
324
388
|
}
|
|
389
|
+
if (level === "table" && entry.disabled !== void 0) {
|
|
390
|
+
logger.warn(`${WARN_PREFIX} class-level action "${name}" — \`disabled\` is not allowed at the 'table' level — dropping`);
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
325
393
|
const processor = entry.processor;
|
|
326
394
|
let value;
|
|
327
395
|
if (processor === "navigate" || processor === "backend") {
|
|
@@ -349,7 +417,7 @@ function buildClassEntry(name, entry, forcedLevel, logger) {
|
|
|
349
417
|
processor,
|
|
350
418
|
value
|
|
351
419
|
};
|
|
352
|
-
|
|
420
|
+
emitInfo(info, entry, name, logger);
|
|
353
421
|
return info;
|
|
354
422
|
}
|
|
355
423
|
function applyDefaultPerLevel(actions, logger) {
|
|
@@ -363,6 +431,26 @@ function applyDefaultPerLevel(actions, logger) {
|
|
|
363
431
|
} else winners.set(a.level, a.name);
|
|
364
432
|
}
|
|
365
433
|
}
|
|
434
|
+
/**
|
|
435
|
+
* Emit structural-copy fields plus `disabled` (stringified) and
|
|
436
|
+
* `requiredFields` (forwarded verbatim — server doesn't auto-derive).
|
|
437
|
+
* `requiredFields` without `disabled` is dropped with a warning before the
|
|
438
|
+
* structural copy runs, so method-decorator and class-level-dict origins
|
|
439
|
+
* stay symmetric.
|
|
440
|
+
*/
|
|
441
|
+
function emitInfo(info, source, name, logger) {
|
|
442
|
+
const disabled = source.disabled;
|
|
443
|
+
const hasDisabled = typeof disabled === "function";
|
|
444
|
+
let requiredFields = source.requiredFields;
|
|
445
|
+
if (!hasDisabled && requiredFields !== void 0) {
|
|
446
|
+
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.`);
|
|
447
|
+
requiredFields = void 0;
|
|
448
|
+
}
|
|
449
|
+
copyOptionalFields(info, source);
|
|
450
|
+
if (Array.isArray(info.promptText)) info.promptText = info.promptText.slice();
|
|
451
|
+
if (hasDisabled) info.disabled = disabled.toString();
|
|
452
|
+
if (Array.isArray(requiredFields)) info.requiredFields = requiredFields.slice();
|
|
453
|
+
}
|
|
366
454
|
function copyOptionalFields(info, source) {
|
|
367
455
|
for (const key of OPTIONAL_FIELDS) {
|
|
368
456
|
const value = source[key];
|
|
@@ -472,13 +560,6 @@ let AsReadableController = class AsReadableController {
|
|
|
472
560
|
}
|
|
473
561
|
};
|
|
474
562
|
}
|
|
475
|
-
/**
|
|
476
|
-
* Whether this controller is read-only (no write endpoints).
|
|
477
|
-
* Returns `true` by default; {@link AsDbController} overrides to `false`.
|
|
478
|
-
*/
|
|
479
|
-
_isReadOnly() {
|
|
480
|
-
return true;
|
|
481
|
-
}
|
|
482
563
|
_queryControlsValidator;
|
|
483
564
|
_pagesControlsValidator;
|
|
484
565
|
_getOneControlsValidator;
|
|
@@ -538,37 +619,31 @@ let AsReadableController = class AsReadableController {
|
|
|
538
619
|
return item;
|
|
539
620
|
}
|
|
540
621
|
/**
|
|
541
|
-
* **GET /meta** — returns the bound interface's metadata envelope.
|
|
542
|
-
*
|
|
543
|
-
*
|
|
544
|
-
* override to add source-specific fields (relations, searchable flags, etc.).
|
|
545
|
-
* The response is cached on the instance; async overrides must cache any
|
|
546
|
-
* extra enrichment themselves.
|
|
622
|
+
* **GET /meta** — returns the bound interface's metadata envelope. The
|
|
623
|
+
* static envelope is cached; {@link applyMetaOverlay} runs per request so
|
|
624
|
+
* subclasses can prune the response by principal.
|
|
547
625
|
*/
|
|
548
626
|
async meta() {
|
|
549
|
-
if (this._metaResponse)
|
|
550
|
-
|
|
551
|
-
this._metaResponse = response;
|
|
552
|
-
return response;
|
|
627
|
+
if (!this._metaResponse) this._metaResponse = this.buildMetaResponse();
|
|
628
|
+
return this.applyMetaOverlay(this._metaResponse);
|
|
553
629
|
}
|
|
554
630
|
/**
|
|
555
631
|
* Builds the `/meta` payload. Override in subclasses to populate source-specific
|
|
556
|
-
* fields.
|
|
557
|
-
*
|
|
558
|
-
*
|
|
559
|
-
* directly so `@DbAction*` decorators still surface.
|
|
632
|
+
* fields. Subclasses that fully replace the envelope must call
|
|
633
|
+
* {@link buildActions} and {@link buildCrud} directly so `@DbAction*`
|
|
634
|
+
* decorators and CRUD permissions still surface.
|
|
560
635
|
*/
|
|
561
|
-
|
|
636
|
+
buildMetaResponse() {
|
|
562
637
|
return {
|
|
563
638
|
searchable: false,
|
|
564
639
|
vectorSearchable: false,
|
|
565
640
|
searchIndexes: [],
|
|
566
641
|
primaryKeys: [],
|
|
567
|
-
readOnly: this._isReadOnly(),
|
|
568
642
|
relations: [],
|
|
569
643
|
fields: {},
|
|
570
644
|
type: this.getSerializedType(),
|
|
571
|
-
actions: this.buildActions()
|
|
645
|
+
actions: this.buildActions(),
|
|
646
|
+
crud: this.buildCrud()
|
|
572
647
|
};
|
|
573
648
|
}
|
|
574
649
|
/**
|
|
@@ -579,6 +654,25 @@ let AsReadableController = class AsReadableController {
|
|
|
579
654
|
buildActions() {
|
|
580
655
|
return discoverActions(this.constructor, this.app, this.logger);
|
|
581
656
|
}
|
|
657
|
+
/**
|
|
658
|
+
* Declares the built-in CRUD operations this controller exposes. Subclasses
|
|
659
|
+
* override to add their keys; the bare base only exposes `/meta`. See
|
|
660
|
+
* `docs/http/permissions.md` for the wire shape and overlay rules.
|
|
661
|
+
*/
|
|
662
|
+
buildCrud() {
|
|
663
|
+
return {};
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* Per-request overlay applied to the cached `/meta` envelope. Default no-op.
|
|
667
|
+
* Subclasses may shallow-clone and prune `crud` keys, `crud[op]` arrays, or
|
|
668
|
+
* `actions[]` based on the current request principal (read via Moost
|
|
669
|
+
* composables). The cached envelope MUST NOT be mutated — see
|
|
670
|
+
* `docs/http/permissions.md` for the full contract, including the
|
|
671
|
+
* "discoverability only" caveat.
|
|
672
|
+
*/
|
|
673
|
+
applyMetaOverlay(meta) {
|
|
674
|
+
return meta;
|
|
675
|
+
}
|
|
582
676
|
};
|
|
583
677
|
__decorate([
|
|
584
678
|
Get("meta"),
|
|
@@ -657,6 +751,23 @@ const ReadableController = (readable, prefix) => {
|
|
|
657
751
|
*/
|
|
658
752
|
const ViewController = ReadableController;
|
|
659
753
|
//#endregion
|
|
754
|
+
//#region src/permissions/crud-controls.ts
|
|
755
|
+
/**
|
|
756
|
+
* Static control whitelists per read op. Each list is the matching DTO's
|
|
757
|
+
* `$`-properties (stripped) plus the URL-grammar extras (`filter`,
|
|
758
|
+
* `insights`, `groupBy`) that bypass the DTO. The DTO is the single source of
|
|
759
|
+
* truth — adding a `$control` there auto-extends the whitelist here.
|
|
760
|
+
*/
|
|
761
|
+
const dtoControls = (Dto) => [...Dto.type.props.keys()].map((k) => k.startsWith("$") ? k.slice(1) : k);
|
|
762
|
+
const QUERY_CONTROLS = [
|
|
763
|
+
"filter",
|
|
764
|
+
"insights",
|
|
765
|
+
...dtoControls(QueryControlsDto),
|
|
766
|
+
"groupBy"
|
|
767
|
+
];
|
|
768
|
+
const PAGES_CONTROLS = ["filter", ...dtoControls(PagesControlsDto)];
|
|
769
|
+
const ONE_CONTROLS = dtoControls(GetOneControlsDto);
|
|
770
|
+
//#endregion
|
|
660
771
|
//#region \0@oxc-project+runtime@0.120.0/helpers/decorateParam.js
|
|
661
772
|
function __decorateParam(paramIndex, decorator) {
|
|
662
773
|
return function(target, key) {
|
|
@@ -906,7 +1017,7 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
|
|
|
906
1017
|
* vector-searchable flags, field-descriptor-derived filter/sort hints, and
|
|
907
1018
|
* the configured primary keys.
|
|
908
1019
|
*/
|
|
909
|
-
|
|
1020
|
+
buildMetaResponse() {
|
|
910
1021
|
const relations = [];
|
|
911
1022
|
for (const [name, rel] of this.readable.relations) relations.push({
|
|
912
1023
|
name,
|
|
@@ -931,11 +1042,19 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
|
|
|
931
1042
|
vectorSearchable: this.readable.isVectorSearchable(),
|
|
932
1043
|
searchIndexes: this.readable.getSearchIndexes(),
|
|
933
1044
|
primaryKeys: [...this.readable.primaryKeys],
|
|
934
|
-
readOnly: this._isReadOnly(),
|
|
935
1045
|
relations,
|
|
936
1046
|
fields,
|
|
937
1047
|
type: this.getSerializedType(),
|
|
938
|
-
actions: this.buildActions()
|
|
1048
|
+
actions: this.buildActions(),
|
|
1049
|
+
crud: this.buildCrud()
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
buildCrud() {
|
|
1053
|
+
return {
|
|
1054
|
+
...super.buildCrud(),
|
|
1055
|
+
query: [...QUERY_CONTROLS],
|
|
1056
|
+
pages: [...PAGES_CONTROLS],
|
|
1057
|
+
one: [...ONE_CONTROLS]
|
|
939
1058
|
};
|
|
940
1059
|
}
|
|
941
1060
|
};
|
|
@@ -974,6 +1093,7 @@ AsDbReadableController = __decorate([
|
|
|
974
1093
|
__decorateParam(0, Inject(READABLE_DEF)),
|
|
975
1094
|
__decorateMetadata("design:paramtypes", [Object, typeof (_ref$3 = typeof Moost !== "undefined" && Moost) === "function" ? _ref$3 : Object])
|
|
976
1095
|
], AsDbReadableController);
|
|
1096
|
+
registerAsDbReadableController(AsDbReadableController);
|
|
977
1097
|
//#endregion
|
|
978
1098
|
//#region src/as-db.controller.ts
|
|
979
1099
|
var _ref$2, _ref2$1;
|
|
@@ -985,8 +1105,14 @@ let AsDbController = class AsDbController extends AsDbReadableController {
|
|
|
985
1105
|
constructor(table, app) {
|
|
986
1106
|
super(table, app);
|
|
987
1107
|
}
|
|
988
|
-
|
|
989
|
-
return
|
|
1108
|
+
buildCrud() {
|
|
1109
|
+
return {
|
|
1110
|
+
...super.buildCrud(),
|
|
1111
|
+
insert: [],
|
|
1112
|
+
update: [],
|
|
1113
|
+
replace: [],
|
|
1114
|
+
remove: []
|
|
1115
|
+
};
|
|
990
1116
|
}
|
|
991
1117
|
/**
|
|
992
1118
|
* Intercepts write operations. Return `undefined` to abort.
|
|
@@ -1203,7 +1329,7 @@ let AsValueHelpController = class AsValueHelpController extends AsReadableContro
|
|
|
1203
1329
|
* client picker UI (which controls to render); the server does not enforce
|
|
1204
1330
|
* these flags at request time.
|
|
1205
1331
|
*/
|
|
1206
|
-
|
|
1332
|
+
buildMetaResponse() {
|
|
1207
1333
|
const fields = {};
|
|
1208
1334
|
for (const [path, meta] of this.fieldMeta) fields[path] = {
|
|
1209
1335
|
sortable: meta.has("ui.dict.sortable"),
|
|
@@ -1214,16 +1340,24 @@ let AsValueHelpController = class AsValueHelpController extends AsReadableContro
|
|
|
1214
1340
|
vectorSearchable: false,
|
|
1215
1341
|
searchIndexes: [],
|
|
1216
1342
|
primaryKeys: this.primaryKey ? [this.primaryKey] : [],
|
|
1217
|
-
readOnly: this._isReadOnly(),
|
|
1218
1343
|
relations: [],
|
|
1219
1344
|
fields,
|
|
1220
1345
|
type: this.getSerializedType(),
|
|
1221
|
-
actions: []
|
|
1346
|
+
actions: [],
|
|
1347
|
+
crud: this.buildCrud()
|
|
1222
1348
|
};
|
|
1223
1349
|
}
|
|
1224
1350
|
buildActions() {
|
|
1225
1351
|
return [];
|
|
1226
1352
|
}
|
|
1353
|
+
buildCrud() {
|
|
1354
|
+
return {
|
|
1355
|
+
...super.buildCrud(),
|
|
1356
|
+
query: [...QUERY_CONTROLS],
|
|
1357
|
+
pages: [...PAGES_CONTROLS],
|
|
1358
|
+
one: [...ONE_CONTROLS]
|
|
1359
|
+
};
|
|
1360
|
+
}
|
|
1227
1361
|
};
|
|
1228
1362
|
__decorate([
|
|
1229
1363
|
Get("query"),
|
|
@@ -1258,6 +1392,7 @@ AsValueHelpController = __decorate([Inherit(), __decorateMetadata("design:paramt
|
|
|
1258
1392
|
String,
|
|
1259
1393
|
typeof (_ref$1 = typeof Moost !== "undefined" && Moost) === "function" ? _ref$1 : Object
|
|
1260
1394
|
])], AsValueHelpController);
|
|
1395
|
+
registerAsValueHelpController(AsValueHelpController);
|
|
1261
1396
|
//#endregion
|
|
1262
1397
|
//#region src/as-json-value-help.controller.ts
|
|
1263
1398
|
var _ref;
|
|
@@ -1418,89 +1553,43 @@ function applySelect(rows, select) {
|
|
|
1418
1553
|
});
|
|
1419
1554
|
}
|
|
1420
1555
|
//#endregion
|
|
1421
|
-
//#region src/actions/
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
* Metadata-only — pair with `@Post(...)` for Moost to bind the route. The
|
|
1426
|
-
* meta builder reads this metadata plus the bound POST path lazily and
|
|
1427
|
-
* emits the action with `processor: 'backend'`. Order vs.
|
|
1428
|
-
* `@DbActionDefault()` does not matter — both merge into the same slot.
|
|
1429
|
-
*
|
|
1430
|
-
* @example
|
|
1431
|
-
* ```ts
|
|
1432
|
-
* @Post('actions/block')
|
|
1433
|
-
* @DbAction('block', { label: 'Block', icon: 'i-as-block', intent: 'negative' })
|
|
1434
|
-
* async blockUser(@DbActionPK() id: string) { ... }
|
|
1435
|
-
* ```
|
|
1436
|
-
*/
|
|
1437
|
-
function DbAction(name, opts = {}) {
|
|
1438
|
-
return getMoostMate().decorate((current) => {
|
|
1439
|
-
const meta = current;
|
|
1440
|
-
return {
|
|
1441
|
-
...current,
|
|
1442
|
-
[MOOST_DB_ACTION]: mergeActionMeta(meta, {
|
|
1443
|
-
name,
|
|
1444
|
-
opts
|
|
1445
|
-
})
|
|
1446
|
-
};
|
|
1447
|
-
});
|
|
1556
|
+
//#region src/actions/action-disabled-error.ts
|
|
1557
|
+
function buildMessage(action, pks) {
|
|
1558
|
+
if (pks !== void 0) return `Action "${action}" is disabled for ${pks.length} of the selected rows`;
|
|
1559
|
+
return `Action "${action}" is disabled for this row`;
|
|
1448
1560
|
}
|
|
1449
|
-
//#endregion
|
|
1450
|
-
//#region src/actions/db-action-default.decorator.ts
|
|
1451
1561
|
/**
|
|
1452
|
-
*
|
|
1453
|
-
*
|
|
1562
|
+
* Thrown by the gate interceptor when `disabled` returns truthy. Composes
|
|
1563
|
+
* with Moost's existing error mapper to produce HTTP 409 with the wire body
|
|
1564
|
+
* defined by {@link ActionDisabledErrorBody}.
|
|
1565
|
+
*
|
|
1566
|
+
* - `'row'`-level rejection: pass `(action, pk)` — the body emits `pk`.
|
|
1567
|
+
* - `'rows'`-level rejection: pass `(action, undefined, pks)` — the body
|
|
1568
|
+
* emits `pks` (the FULL list of failing PKs in reject mode; the FULL list
|
|
1569
|
+
* of request PKs in skip mode with zero survivors).
|
|
1454
1570
|
*/
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1571
|
+
var ActionDisabledError = class extends HttpError {
|
|
1572
|
+
name = "ActionDisabledError";
|
|
1573
|
+
constructor(action, pk, pks) {
|
|
1574
|
+
const body = {
|
|
1575
|
+
name: "ActionDisabledError",
|
|
1576
|
+
message: buildMessage(action, pks),
|
|
1577
|
+
statusCode: 409,
|
|
1578
|
+
action
|
|
1461
1579
|
};
|
|
1462
|
-
|
|
1463
|
-
|
|
1580
|
+
if (pks !== void 0) body.pks = pks;
|
|
1581
|
+
else if (pk !== void 0) body.pk = pk;
|
|
1582
|
+
super(409, body);
|
|
1583
|
+
}
|
|
1584
|
+
};
|
|
1464
1585
|
//#endregion
|
|
1465
|
-
//#region src/actions/pk-
|
|
1466
|
-
/**
|
|
1467
|
-
* Extract the PK validation source from a controller instance. Looks for
|
|
1468
|
-
* `readable` (set by {@link AsDbReadableController}) or `table` (set by
|
|
1469
|
-
* {@link AsDbController}).
|
|
1470
|
-
*
|
|
1471
|
-
* If the controller has no typed table attached (e.g. a value-help
|
|
1472
|
-
* controller, or a plain Moost controller without `@TableController`),
|
|
1473
|
-
* throws an HTTP 500 — this is a **server misconfiguration**, not a client
|
|
1474
|
-
* error. The body parser has nothing to validate against, so the request
|
|
1475
|
-
* cannot proceed. Use `@Body()` and parse the PK manually if you need to
|
|
1476
|
-
* accept PK-shaped bodies on a controller without an attached table.
|
|
1477
|
-
*/
|
|
1478
|
-
function resolvePkSource(controller) {
|
|
1479
|
-
const c = controller;
|
|
1480
|
-
const candidate = c.readable ?? c.table;
|
|
1481
|
-
if (!isPkValidationSource(candidate)) throw new HttpError(500, "@DbActionPK/@DbActionPKs requires a controller with an attached table (via @TableController / @ReadableController). Use @Body() instead if your controller has no typed table.");
|
|
1482
|
-
return candidate;
|
|
1483
|
-
}
|
|
1586
|
+
//#region src/actions/pk-validation.ts
|
|
1484
1587
|
function isPkValidationSource(value) {
|
|
1485
1588
|
if (!value || typeof value !== "object") return false;
|
|
1486
1589
|
const v = value;
|
|
1487
1590
|
return Array.isArray(v.primaryKeys) && Array.isArray(v.fieldDescriptors);
|
|
1488
1591
|
}
|
|
1489
1592
|
/**
|
|
1490
|
-
* Build a parameter decorator that parses the JSON request body, validates
|
|
1491
|
-
* it against the bound table's PK schema with `validate`, and tags the param
|
|
1492
|
-
* so {@link discoverActions} can infer the action's `level`.
|
|
1493
|
-
*/
|
|
1494
|
-
function createPkParamDecorator(kind, validate, resolverName) {
|
|
1495
|
-
return ApplyDecorators(getMoostMate().decorate(MOOST_DB_ACTION_PARAM, kind), Resolve(async () => {
|
|
1496
|
-
const body = await useBody().parseBody();
|
|
1497
|
-
validate(body, resolvePkSource(useControllerContext().getController()));
|
|
1498
|
-
return body;
|
|
1499
|
-
}, resolverName));
|
|
1500
|
-
}
|
|
1501
|
-
//#endregion
|
|
1502
|
-
//#region src/actions/pk-validation.ts
|
|
1503
|
-
/**
|
|
1504
1593
|
* Validate a JSON-decoded body against a single-row PK shape (scalar or
|
|
1505
1594
|
* composite). Throws {@link ValidatorError} with structured `errors` so the
|
|
1506
1595
|
* existing validation interceptor returns HTTP 400.
|
|
@@ -1585,6 +1674,229 @@ function isPlainObject(value) {
|
|
|
1585
1674
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1586
1675
|
}
|
|
1587
1676
|
//#endregion
|
|
1677
|
+
//#region src/actions/pk-cache.ts
|
|
1678
|
+
const boundTableKey = key("atscript_db_action_bound_table");
|
|
1679
|
+
function getActionTable(ctx) {
|
|
1680
|
+
if (ctx.has(boundTableKey)) {
|
|
1681
|
+
const fromSlot = ctx.get(boundTableKey);
|
|
1682
|
+
if (fromSlot) return fromSlot;
|
|
1683
|
+
}
|
|
1684
|
+
const ctrl = useControllerContext(ctx).getController();
|
|
1685
|
+
if (ctrl) {
|
|
1686
|
+
const t = ctrl.readable ?? ctrl.table;
|
|
1687
|
+
if (t) return t;
|
|
1688
|
+
}
|
|
1689
|
+
return null;
|
|
1690
|
+
}
|
|
1691
|
+
function noTableError(ctx) {
|
|
1692
|
+
const cc = useControllerContext(ctx);
|
|
1693
|
+
const ctrl = cc.getController();
|
|
1694
|
+
const methodName = cc.getMethod();
|
|
1695
|
+
let actionName;
|
|
1696
|
+
if (ctrl && methodName) actionName = getMoostMate().read(ctrl.constructor, methodName)?.[MOOST_DB_ACTION]?.name;
|
|
1697
|
+
return new 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.`);
|
|
1698
|
+
}
|
|
1699
|
+
async function resolveValidatedPk(ctx, validate) {
|
|
1700
|
+
const table = getActionTable(ctx);
|
|
1701
|
+
if (!isPkValidationSource(table)) throw noTableError(ctx);
|
|
1702
|
+
const body = await useBody(ctx).parseBody();
|
|
1703
|
+
validate(body, table);
|
|
1704
|
+
return body;
|
|
1705
|
+
}
|
|
1706
|
+
const dbActionPkSlot = cached((ctx) => resolveValidatedPk(ctx, validateSinglePk));
|
|
1707
|
+
const dbActionPksSlot = cached(async (ctx) => {
|
|
1708
|
+
return await resolveValidatedPk(ctx, validateMultiPk);
|
|
1709
|
+
});
|
|
1710
|
+
const useDbActionPk = defineWook((ctx) => ({ load: () => ctx.get(dbActionPkSlot) }));
|
|
1711
|
+
const useDbActionPks = defineWook((ctx) => ({ load: () => ctx.get(dbActionPksSlot) }));
|
|
1712
|
+
//#endregion
|
|
1713
|
+
//#region src/actions/row-cache.ts
|
|
1714
|
+
function asFetchTable(value) {
|
|
1715
|
+
if (!value || typeof value !== "object") return null;
|
|
1716
|
+
const v = value;
|
|
1717
|
+
return Array.isArray(v.primaryKeys) && typeof v.findById === "function" && typeof v.findMany === "function" ? v : null;
|
|
1718
|
+
}
|
|
1719
|
+
function noTable() {
|
|
1720
|
+
throw new HttpError(500, `${WARN_PREFIX} cached row wook: no bound table`);
|
|
1721
|
+
}
|
|
1722
|
+
async function loadRow(ctx) {
|
|
1723
|
+
const pk = await ctx.get(dbActionPkSlot);
|
|
1724
|
+
const row = await (asFetchTable(getActionTable(ctx)) ?? noTable()).findById(pk);
|
|
1725
|
+
if (row == null) throw new HttpError(404, "Row not found for action PK");
|
|
1726
|
+
return row;
|
|
1727
|
+
}
|
|
1728
|
+
async function loadRows(ctx) {
|
|
1729
|
+
const pks = await ctx.get(dbActionPksSlot);
|
|
1730
|
+
const table = asFetchTable(getActionTable(ctx)) ?? noTable();
|
|
1731
|
+
if (pks.length === 0) return [];
|
|
1732
|
+
const { primaryKeys } = table;
|
|
1733
|
+
const rows = await table.findMany({ filter: buildPksFilter(pks, primaryKeys) });
|
|
1734
|
+
if (primaryKeys.length === 1) {
|
|
1735
|
+
const field = primaryKeys[0];
|
|
1736
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
1737
|
+
for (const row of rows) byKey.set(row[field], row);
|
|
1738
|
+
const ordered = [];
|
|
1739
|
+
for (const pk of pks) {
|
|
1740
|
+
const found = byKey.get(pk);
|
|
1741
|
+
if (found !== void 0) ordered.push(found);
|
|
1742
|
+
}
|
|
1743
|
+
return ordered;
|
|
1744
|
+
}
|
|
1745
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
1746
|
+
for (const row of rows) byKey.set(compositeKey(row, primaryKeys), row);
|
|
1747
|
+
const ordered = [];
|
|
1748
|
+
for (const pk of pks) {
|
|
1749
|
+
const found = byKey.get(compositeKey(pk, primaryKeys));
|
|
1750
|
+
if (found !== void 0) ordered.push(found);
|
|
1751
|
+
}
|
|
1752
|
+
return ordered;
|
|
1753
|
+
}
|
|
1754
|
+
function buildPksFilter(pks, primaryKeys) {
|
|
1755
|
+
if (primaryKeys.length === 1) return { [primaryKeys[0]]: { $in: pks } };
|
|
1756
|
+
return { $or: pks.map((pk) => {
|
|
1757
|
+
const obj = pk;
|
|
1758
|
+
const clause = {};
|
|
1759
|
+
for (const field of primaryKeys) clause[field] = obj[field];
|
|
1760
|
+
return clause;
|
|
1761
|
+
}) };
|
|
1762
|
+
}
|
|
1763
|
+
function compositeKey(obj, primaryKeys) {
|
|
1764
|
+
let out = "";
|
|
1765
|
+
for (const f of primaryKeys) {
|
|
1766
|
+
if (out !== "") out += "\0";
|
|
1767
|
+
const v = obj[f];
|
|
1768
|
+
if (v === null) out += "n";
|
|
1769
|
+
else if (v === void 0) out += "u";
|
|
1770
|
+
else if (typeof v === "string") out += `s\x02${v}`;
|
|
1771
|
+
else if (typeof v === "number") out += `n\x02${v}`;
|
|
1772
|
+
else if (typeof v === "boolean") out += `b\x02${v}`;
|
|
1773
|
+
else out += `j\x02${JSON.stringify(v)}`;
|
|
1774
|
+
}
|
|
1775
|
+
return out;
|
|
1776
|
+
}
|
|
1777
|
+
const dbActionRowSlot = cached((ctx) => loadRow(ctx));
|
|
1778
|
+
const dbActionRowsSlot = cached((ctx) => loadRows(ctx));
|
|
1779
|
+
const useDbActionRow = defineWook((ctx) => ({ load: () => ctx.get(dbActionRowSlot) }));
|
|
1780
|
+
const useDbActionRows = defineWook((ctx) => ({ load: () => ctx.get(dbActionRowsSlot) }));
|
|
1781
|
+
//#endregion
|
|
1782
|
+
//#region src/actions/gate-interceptor.ts
|
|
1783
|
+
const GATE_PRIORITY = TInterceptorPriority.AFTER_GUARD;
|
|
1784
|
+
function injectBoundTable(table) {
|
|
1785
|
+
const ctx = current();
|
|
1786
|
+
if (ctx.has(boundTableKey)) return;
|
|
1787
|
+
const controller = useControllerContext(ctx).getController();
|
|
1788
|
+
if (isAsDbReadableControllerInstance(controller)) {
|
|
1789
|
+
ctx.set(boundTableKey, controller.readable);
|
|
1790
|
+
return;
|
|
1791
|
+
}
|
|
1792
|
+
if (table != null) ctx.set(boundTableKey, table);
|
|
1793
|
+
}
|
|
1794
|
+
function buildGateInterceptor(opts) {
|
|
1795
|
+
const { action, level, disabled, onDisabledRows, table } = opts;
|
|
1796
|
+
return defineBeforeInterceptor(async () => {
|
|
1797
|
+
injectBoundTable(table);
|
|
1798
|
+
const ctx = current();
|
|
1799
|
+
if (level === "row") {
|
|
1800
|
+
if (disabled(await ctx.get(dbActionRowSlot))) throw new ActionDisabledError(action, await ctx.get(dbActionPkSlot));
|
|
1801
|
+
return;
|
|
1802
|
+
}
|
|
1803
|
+
const pks = await ctx.get(dbActionPksSlot);
|
|
1804
|
+
const rows = await ctx.get(dbActionRowsSlot);
|
|
1805
|
+
const failingPks = [];
|
|
1806
|
+
const passingRows = [];
|
|
1807
|
+
const passingPks = [];
|
|
1808
|
+
for (let i = 0; i < rows.length; i++) if (disabled(rows[i])) failingPks.push(pks[i]);
|
|
1809
|
+
else {
|
|
1810
|
+
passingRows.push(rows[i]);
|
|
1811
|
+
passingPks.push(pks[i]);
|
|
1812
|
+
}
|
|
1813
|
+
if (onDisabledRows === "skip") {
|
|
1814
|
+
if (passingRows.length === 0) throw new ActionDisabledError(action, void 0, [...pks]);
|
|
1815
|
+
if (failingPks.length > 0) {
|
|
1816
|
+
ctx.set(dbActionRowsSlot, Promise.resolve(passingRows));
|
|
1817
|
+
ctx.set(dbActionPksSlot, Promise.resolve(passingPks));
|
|
1818
|
+
}
|
|
1819
|
+
return;
|
|
1820
|
+
}
|
|
1821
|
+
if (failingPks.length > 0) throw new ActionDisabledError(action, void 0, failingPks);
|
|
1822
|
+
}, GATE_PRIORITY);
|
|
1823
|
+
}
|
|
1824
|
+
/** Thin interceptor for `@DbActionRow*` without `disabled` — injects only the bound table. */
|
|
1825
|
+
function buildThinInterceptor(opts) {
|
|
1826
|
+
const { table } = opts;
|
|
1827
|
+
return defineBeforeInterceptor(() => {
|
|
1828
|
+
injectBoundTable(table);
|
|
1829
|
+
}, GATE_PRIORITY);
|
|
1830
|
+
}
|
|
1831
|
+
//#endregion
|
|
1832
|
+
//#region src/actions/db-action.decorator.ts
|
|
1833
|
+
/**
|
|
1834
|
+
* Mark a controller method as a database action surfaced via `/meta`. Writes
|
|
1835
|
+
* `MOOST_DB_ACTION` metadata and registers a Moost interceptor when needed
|
|
1836
|
+
* (gate when `disabled` is set, thin bound-table injector when only
|
|
1837
|
+
* `@DbActionRow*` is present). Stacking two `@DbAction` on the same method
|
|
1838
|
+
* is undefined and emits a warning.
|
|
1839
|
+
*/
|
|
1840
|
+
function DbAction(name, opts = {}) {
|
|
1841
|
+
const mate = getMoostMate();
|
|
1842
|
+
return ((target, propertyKey, descriptor) => {
|
|
1843
|
+
const priorName = mate.read(target, propertyKey)?.[MOOST_DB_ACTION]?.name;
|
|
1844
|
+
if (priorName) console.warn(`${WARN_PREFIX} stacking @DbAction on the same method is undefined; declare one per method. Detected: "${priorName}" and "${name}".`);
|
|
1845
|
+
mate.decorate((current) => {
|
|
1846
|
+
const meta = current;
|
|
1847
|
+
return {
|
|
1848
|
+
...current,
|
|
1849
|
+
[MOOST_DB_ACTION]: mergeActionMeta(meta, {
|
|
1850
|
+
name,
|
|
1851
|
+
opts
|
|
1852
|
+
})
|
|
1853
|
+
};
|
|
1854
|
+
})(target, propertyKey, descriptor);
|
|
1855
|
+
if (isAsValueHelpControllerSubclass(typeof target === "function" ? target : target.constructor)) return descriptor;
|
|
1856
|
+
const scan = scanParamLevel(mate.read(target, propertyKey)?.params ?? []);
|
|
1857
|
+
if (!!opts.disabled && (scan.level === "row" || scan.level === "rows")) Intercept(buildGateInterceptor({
|
|
1858
|
+
action: name,
|
|
1859
|
+
level: scan.level,
|
|
1860
|
+
disabled: opts.disabled,
|
|
1861
|
+
onDisabledRows: opts.onDisabledRows ?? "reject",
|
|
1862
|
+
table: opts.table
|
|
1863
|
+
}))(target, propertyKey, descriptor);
|
|
1864
|
+
else if (scan.hasRowParam) Intercept(buildThinInterceptor({ table: opts.table }))(target, propertyKey, descriptor);
|
|
1865
|
+
return descriptor;
|
|
1866
|
+
});
|
|
1867
|
+
}
|
|
1868
|
+
//#endregion
|
|
1869
|
+
//#region src/actions/db-action-default.decorator.ts
|
|
1870
|
+
/**
|
|
1871
|
+
* Sugar that flips `default: true` on the same method's `@DbAction` metadata.
|
|
1872
|
+
* Equivalent to passing `opts.default = true`. Decorator order does not matter.
|
|
1873
|
+
*/
|
|
1874
|
+
function DbActionDefault() {
|
|
1875
|
+
return getMoostMate().decorate((current) => {
|
|
1876
|
+
const meta = current;
|
|
1877
|
+
return {
|
|
1878
|
+
...current,
|
|
1879
|
+
[MOOST_DB_ACTION]: mergeActionMeta(meta, { opts: { default: true } })
|
|
1880
|
+
};
|
|
1881
|
+
});
|
|
1882
|
+
}
|
|
1883
|
+
//#endregion
|
|
1884
|
+
//#region src/actions/pk-source.ts
|
|
1885
|
+
/**
|
|
1886
|
+
* Build a parameter decorator that reads its value from the cached PK wook
|
|
1887
|
+
* (single or multi). Validation runs inside the wook factory exactly once
|
|
1888
|
+
* per request, regardless of how many readers consume the value (`@DbActionPK*`
|
|
1889
|
+
* resolver, gate interceptor, cached row wook, in-handler composables).
|
|
1890
|
+
*
|
|
1891
|
+
* Marks the param so {@link discoverActions} can infer the action's `level`.
|
|
1892
|
+
*/
|
|
1893
|
+
function createPkParamDecorator(kind) {
|
|
1894
|
+
const mate = getMoostMate();
|
|
1895
|
+
const slot = kind === "pk" ? dbActionPkSlot : dbActionPksSlot;
|
|
1896
|
+
const resolverName = kind === "pk" ? "dbActionPk" : "dbActionPks";
|
|
1897
|
+
return ApplyDecorators(mate.decorate(MOOST_DB_ACTION_PARAM, kind), Resolve(async () => current().get(slot), resolverName));
|
|
1898
|
+
}
|
|
1899
|
+
//#endregion
|
|
1588
1900
|
//#region src/actions/db-action-pk.decorator.ts
|
|
1589
1901
|
/**
|
|
1590
1902
|
* Parameter resolver that reads the primary key from the JSON request body
|
|
@@ -1599,9 +1911,13 @@ function isPlainObject(value) {
|
|
|
1599
1911
|
*
|
|
1600
1912
|
* Marks the param so {@link discoverActions} can infer the action's `level`
|
|
1601
1913
|
* as `'row'`.
|
|
1914
|
+
*
|
|
1915
|
+
* Implementation note: the resolver is a thin reader of the cached PK wook
|
|
1916
|
+
* — validation logic lives in the wook factory, which runs once per request
|
|
1917
|
+
* regardless of how many readers consume the value.
|
|
1602
1918
|
*/
|
|
1603
1919
|
function DbActionPK() {
|
|
1604
|
-
return createPkParamDecorator("pk"
|
|
1920
|
+
return createPkParamDecorator("pk");
|
|
1605
1921
|
}
|
|
1606
1922
|
//#endregion
|
|
1607
1923
|
//#region src/actions/db-action-pks.decorator.ts
|
|
@@ -1614,9 +1930,44 @@ function DbActionPK() {
|
|
|
1614
1930
|
*
|
|
1615
1931
|
* Validation is strict — no type coercion. Marks the param so
|
|
1616
1932
|
* {@link discoverActions} can infer the action's `level` as `'rows'`.
|
|
1933
|
+
*
|
|
1934
|
+
* In `'rows'` skip mode the resolved value reflects the gate interceptor's
|
|
1935
|
+
* filtered subset (the cached PK slot is overwritten in place); see
|
|
1936
|
+
* {@link dbActionPksSlot} for precedence details.
|
|
1617
1937
|
*/
|
|
1618
1938
|
function DbActionPKs() {
|
|
1619
|
-
return createPkParamDecorator("pks"
|
|
1939
|
+
return createPkParamDecorator("pks");
|
|
1940
|
+
}
|
|
1941
|
+
//#endregion
|
|
1942
|
+
//#region src/actions/db-action-row.decorator.ts
|
|
1943
|
+
function createRowParamDecorator(metaKey, slot, resolverName) {
|
|
1944
|
+
return ApplyDecorators(getMoostMate().decorate(metaKey, true), Resolve(async () => current().get(slot), resolverName));
|
|
1945
|
+
}
|
|
1946
|
+
/**
|
|
1947
|
+
* Parameter decorator that injects the row whose PK was supplied in the
|
|
1948
|
+
* request body. Reads from the cached row wook (fetched once per request,
|
|
1949
|
+
* shared with the gate interceptor when `disabled` is set).
|
|
1950
|
+
*
|
|
1951
|
+
* Marks the param so {@link discoverActions} infers the action's `level` as
|
|
1952
|
+
* `'row'`. Co-occurrence with `@DbActionRows()` (or any multi-cardinality
|
|
1953
|
+
* decorator) drops the action with a warning.
|
|
1954
|
+
*
|
|
1955
|
+
* In `'skip'` mode this returns the gate's filtered row; the original
|
|
1956
|
+
* request-body row is not retrievable.
|
|
1957
|
+
*/
|
|
1958
|
+
function DbActionRow() {
|
|
1959
|
+
return createRowParamDecorator(MOOST_DB_ACTION_ROW, dbActionRowSlot, "dbActionRow");
|
|
1960
|
+
}
|
|
1961
|
+
/**
|
|
1962
|
+
* Parameter decorator that injects the rows-array fetched by primary keys
|
|
1963
|
+
* from the request body. Reads from the cached row-array wook.
|
|
1964
|
+
*
|
|
1965
|
+
* Marks the param so {@link discoverActions} infers the action's `level` as
|
|
1966
|
+
* `'rows'`. In `'rows'` + `'skip'` mode the resolved value contains only the
|
|
1967
|
+
* gate's surviving rows.
|
|
1968
|
+
*/
|
|
1969
|
+
function DbActionRows() {
|
|
1970
|
+
return createRowParamDecorator(MOOST_DB_ACTION_ROWS, dbActionRowsSlot, "dbActionRows");
|
|
1620
1971
|
}
|
|
1621
1972
|
//#endregion
|
|
1622
1973
|
//#region src/actions/db-actions.decorator.ts
|
|
@@ -1652,11 +2003,16 @@ function DbRowsActions(dict) {
|
|
|
1652
2003
|
}
|
|
1653
2004
|
function classLevelActions(dict, forcedLevel) {
|
|
1654
2005
|
const entries = [];
|
|
1655
|
-
for (const [name, entry] of Object.entries(dict))
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
2006
|
+
for (const [name, entry] of Object.entries(dict)) {
|
|
2007
|
+
const merged = forcedLevel ? {
|
|
2008
|
+
...entry,
|
|
2009
|
+
level: forcedLevel
|
|
2010
|
+
} : entry;
|
|
2011
|
+
entries.push({
|
|
2012
|
+
name,
|
|
2013
|
+
entry: merged
|
|
2014
|
+
});
|
|
2015
|
+
}
|
|
1660
2016
|
return getMoostMate().decorate((current) => {
|
|
1661
2017
|
const existing = current["atscript_db_actions"] ?? [];
|
|
1662
2018
|
return {
|
|
@@ -1666,4 +2022,4 @@ function classLevelActions(dict, forcedLevel) {
|
|
|
1666
2022
|
});
|
|
1667
2023
|
}
|
|
1668
2024
|
//#endregion
|
|
1669
|
-
export { AsDbController, AsDbReadableController, AsJsonValueHelpController, AsReadableController, AsValueHelpController, DbAction, DbActionDefault, DbActionPK, DbActionPKs, DbActions, DbRowActions, DbRowsActions, DbTableActions, READABLE_DEF, ReadableController, TABLE_DEF, TableController, UseValidationErrorTransform, ViewController, discoverActions, validationErrorTransform };
|
|
2025
|
+
export { ActionDisabledError, AsDbController, AsDbReadableController, AsJsonValueHelpController, AsReadableController, AsValueHelpController, DbAction, DbActionDefault, DbActionPK, DbActionPKs, DbActionRow, DbActionRows, DbActions, DbRowActions, DbRowsActions, DbTableActions, ONE_CONTROLS, PAGES_CONTROLS, QUERY_CONTROLS, READABLE_DEF, ReadableController, TABLE_DEF, TableController, UseValidationErrorTransform, ViewController, discoverActions, useDbActionPk, useDbActionPks, useDbActionRow, useDbActionRows, validationErrorTransform };
|