@atscript/moost-db 0.1.57 → 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 +421 -103
- package/dist/index.d.cts +181 -22
- package/dist/index.d.mts +181 -22
- package/dist/index.mjs +416 -105
- package/package.json +5 -2
package/dist/index.cjs
CHANGED
|
@@ -4,6 +4,7 @@ let _moostjs_event_http = require("@moostjs/event-http");
|
|
|
4
4
|
let moost = require("moost");
|
|
5
5
|
let _uniqu_url = require("@uniqu/url");
|
|
6
6
|
let _atscript_db = require("@atscript/db");
|
|
7
|
+
let _wooksjs_event_core = require("@wooksjs/event-core");
|
|
7
8
|
let _wooksjs_http_body = require("@wooksjs/http-body");
|
|
8
9
|
//#region src/validation-interceptor.ts
|
|
9
10
|
const dbErrorCodeToStatus = { CONFLICT: 409 };
|
|
@@ -181,13 +182,39 @@ function findSortOffender(sort, isAllowed) {
|
|
|
181
182
|
}
|
|
182
183
|
}
|
|
183
184
|
//#endregion
|
|
185
|
+
//#region src/actions/controller-registry.ts
|
|
186
|
+
let asDbReadableCtor = null;
|
|
187
|
+
let asValueHelpCtor = null;
|
|
188
|
+
function registerAsDbReadableController(ctor) {
|
|
189
|
+
asDbReadableCtor = ctor;
|
|
190
|
+
}
|
|
191
|
+
function registerAsValueHelpController(ctor) {
|
|
192
|
+
asValueHelpCtor = ctor;
|
|
193
|
+
}
|
|
194
|
+
function isAsDbReadableControllerSubclass(ctor) {
|
|
195
|
+
if (!asDbReadableCtor) return false;
|
|
196
|
+
return asDbReadableCtor.prototype.isPrototypeOf(ctor.prototype);
|
|
197
|
+
}
|
|
198
|
+
function isAsValueHelpControllerSubclass(ctor) {
|
|
199
|
+
if (!asValueHelpCtor) return false;
|
|
200
|
+
return asValueHelpCtor.prototype.isPrototypeOf(ctor.prototype);
|
|
201
|
+
}
|
|
202
|
+
function isAsDbReadableControllerInstance(value) {
|
|
203
|
+
return !!asDbReadableCtor && value instanceof asDbReadableCtor;
|
|
204
|
+
}
|
|
205
|
+
//#endregion
|
|
184
206
|
//#region src/actions/keys.ts
|
|
207
|
+
/** Log-message prefix for warnings emitted from the actions subsystem. */
|
|
208
|
+
const WARN_PREFIX = "[moost-db actions]";
|
|
185
209
|
/** Method-level metadata key — written by `@DbAction(name, opts)`. */
|
|
186
210
|
const MOOST_DB_ACTION = "atscript_db_action";
|
|
187
211
|
/** Class-level metadata key — written by `@DbActions` and the level-pinned shortcuts. Stored as an array; decorators accumulate. */
|
|
188
212
|
const MOOST_DB_ACTIONS = "atscript_db_actions";
|
|
189
213
|
/** Param-level metadata key — written by `@DbActionPK()` / `@DbActionPKs()`. Drives level inference. */
|
|
190
214
|
const MOOST_DB_ACTION_PARAM = "atscript_db_action_param";
|
|
215
|
+
/** Param-level marker keys — written by `@DbActionRow()` / `@DbActionRows()`. */
|
|
216
|
+
const MOOST_DB_ACTION_ROW = "atscript_db_action_row";
|
|
217
|
+
const MOOST_DB_ACTION_ROWS = "atscript_db_action_rows";
|
|
191
218
|
/**
|
|
192
219
|
* Shared method-decorator update used by `@DbAction` and `@DbActionDefault`:
|
|
193
220
|
* read the existing `MOOST_DB_ACTION` slot, merge the patch (later-applied
|
|
@@ -205,17 +232,50 @@ function mergeActionMeta(current, patch) {
|
|
|
205
232
|
};
|
|
206
233
|
}
|
|
207
234
|
//#endregion
|
|
235
|
+
//#region src/actions/param-level.ts
|
|
236
|
+
function scanParamLevel(params) {
|
|
237
|
+
let single = false;
|
|
238
|
+
let multi = false;
|
|
239
|
+
let hasRowParam = false;
|
|
240
|
+
let hasBody = false;
|
|
241
|
+
for (const p of params) {
|
|
242
|
+
const kind = p[MOOST_DB_ACTION_PARAM];
|
|
243
|
+
if (kind === "pk") single = true;
|
|
244
|
+
else if (kind === "pks") multi = true;
|
|
245
|
+
if (p["atscript_db_action_row"]) {
|
|
246
|
+
single = true;
|
|
247
|
+
hasRowParam = true;
|
|
248
|
+
}
|
|
249
|
+
if (p["atscript_db_action_rows"]) {
|
|
250
|
+
multi = true;
|
|
251
|
+
hasRowParam = true;
|
|
252
|
+
}
|
|
253
|
+
if (p.paramSource === "BODY") hasBody = true;
|
|
254
|
+
}
|
|
255
|
+
return {
|
|
256
|
+
level: single && multi ? "table" : single ? "row" : multi ? "rows" : "table",
|
|
257
|
+
single,
|
|
258
|
+
multi,
|
|
259
|
+
hasRowParam,
|
|
260
|
+
hasBody
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
//#endregion
|
|
208
264
|
//#region src/actions/discover.ts
|
|
209
|
-
/**
|
|
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
|
+
*/
|
|
210
270
|
const OPTIONAL_FIELDS = [
|
|
211
271
|
"icon",
|
|
212
272
|
"intent",
|
|
213
273
|
"description",
|
|
214
274
|
"order",
|
|
215
275
|
"default",
|
|
216
|
-
"promptText"
|
|
276
|
+
"promptText",
|
|
277
|
+
"shortcut"
|
|
217
278
|
];
|
|
218
|
-
const WARN_PREFIX = "[moost-db actions]";
|
|
219
279
|
const actionsCache = /* @__PURE__ */ new WeakMap();
|
|
220
280
|
/**
|
|
221
281
|
* Discover all actions declared on a controller and produce the `/meta` array.
|
|
@@ -256,9 +316,21 @@ function collectMethodActions(ctor, overview, logger, out) {
|
|
|
256
316
|
const levelInfer = inferMethodLevel(methodMeta.params ?? [], action.name, logger);
|
|
257
317
|
if (!levelInfer) continue;
|
|
258
318
|
if (levelInfer.bodyConflict) {
|
|
259
|
-
logger.warn(`${WARN_PREFIX} action "${action.name}" cannot mix @DbActionPK*/@DbActionPKs with @Body() — dropping`);
|
|
319
|
+
logger.warn(`${WARN_PREFIX} action "${action.name}" cannot mix @DbActionPK*/@DbActionPKs/@DbActionRow*/@DbActionRows with @Body() — dropping`);
|
|
260
320
|
continue;
|
|
261
321
|
}
|
|
322
|
+
if (levelInfer.level === "table" && action.opts.disabled !== void 0) {
|
|
323
|
+
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
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
if (action.opts.disabled !== void 0 || levelInfer.hasRowParam) {
|
|
327
|
+
const extendsReadable = isAsDbReadableControllerSubclass(ctor);
|
|
328
|
+
const hasOptsTable = action.opts.table != null;
|
|
329
|
+
if (!extendsReadable && !hasOptsTable) {
|
|
330
|
+
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`);
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
262
334
|
const postEntry = handlers.find((h) => h.handler.type === "HTTP" && h.handler.method === "POST");
|
|
263
335
|
if (!postEntry) {
|
|
264
336
|
logger.warn(`${WARN_PREFIX} action "${action.name}" requires @Post(...); no POST handler bound to ${methodName} — dropping`);
|
|
@@ -281,40 +353,32 @@ function collectMethodActions(ctor, overview, logger, out) {
|
|
|
281
353
|
processor: "backend",
|
|
282
354
|
value: path
|
|
283
355
|
};
|
|
284
|
-
|
|
356
|
+
emitInfo(info, action.opts, action.name, logger);
|
|
285
357
|
out.push(info);
|
|
286
358
|
}
|
|
287
359
|
}
|
|
288
360
|
function inferMethodLevel(params, actionName, logger) {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
for (const p of params) {
|
|
293
|
-
const kind = p[MOOST_DB_ACTION_PARAM];
|
|
294
|
-
if (kind === "pk") hasPk = true;
|
|
295
|
-
else if (kind === "pks") hasPks = true;
|
|
296
|
-
if (p.paramSource === "BODY") hasBody = true;
|
|
297
|
-
}
|
|
298
|
-
if (hasPk && hasPks) {
|
|
299
|
-
logger.warn(`${WARN_PREFIX} action "${actionName}" has both @DbActionPK and @DbActionPKs — dropping`);
|
|
361
|
+
const scan = scanParamLevel(params);
|
|
362
|
+
if (scan.single && scan.multi) {
|
|
363
|
+
logger.warn(`${WARN_PREFIX} action "${actionName}" mixes single-cardinality and multi-cardinality decorators (@DbActionPK / @DbActionRow vs @DbActionPKs / @DbActionRows) — dropping`);
|
|
300
364
|
return null;
|
|
301
365
|
}
|
|
302
|
-
const level = hasPk ? "row" : hasPks ? "rows" : "table";
|
|
303
366
|
return {
|
|
304
|
-
level,
|
|
305
|
-
bodyConflict: hasBody && level !== "table"
|
|
367
|
+
level: scan.level,
|
|
368
|
+
bodyConflict: scan.hasBody && scan.level !== "table",
|
|
369
|
+
hasRowParam: scan.hasRowParam
|
|
306
370
|
};
|
|
307
371
|
}
|
|
308
372
|
function collectClassActions(ctor, logger, out) {
|
|
309
373
|
const list = (0, moost.getMoostMate)().read(ctor)?.[MOOST_DB_ACTIONS];
|
|
310
374
|
if (!list) return;
|
|
311
|
-
for (const { name, entry
|
|
312
|
-
const built = buildClassEntry(name, entry,
|
|
375
|
+
for (const { name, entry } of list) {
|
|
376
|
+
const built = buildClassEntry(name, entry, logger);
|
|
313
377
|
if (built) out.push(built);
|
|
314
378
|
}
|
|
315
379
|
}
|
|
316
|
-
function buildClassEntry(name, entry,
|
|
317
|
-
const level =
|
|
380
|
+
function buildClassEntry(name, entry, logger) {
|
|
381
|
+
const level = entry.level;
|
|
318
382
|
if (!level) {
|
|
319
383
|
logger.warn(`${WARN_PREFIX} class-level action "${name}" requires a level — dropping. Use @DbTableActions/@DbRowActions/@DbRowsActions or set "level" explicitly.`);
|
|
320
384
|
return null;
|
|
@@ -323,6 +387,10 @@ function buildClassEntry(name, entry, forcedLevel, logger) {
|
|
|
323
387
|
logger.warn(`${WARN_PREFIX} class-level action "${name}" requires a label — dropping`);
|
|
324
388
|
return null;
|
|
325
389
|
}
|
|
390
|
+
if (level === "table" && entry.disabled !== void 0) {
|
|
391
|
+
logger.warn(`${WARN_PREFIX} class-level action "${name}" — \`disabled\` is not allowed at the 'table' level — dropping`);
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
326
394
|
const processor = entry.processor;
|
|
327
395
|
let value;
|
|
328
396
|
if (processor === "navigate" || processor === "backend") {
|
|
@@ -350,7 +418,7 @@ function buildClassEntry(name, entry, forcedLevel, logger) {
|
|
|
350
418
|
processor,
|
|
351
419
|
value
|
|
352
420
|
};
|
|
353
|
-
|
|
421
|
+
emitInfo(info, entry, name, logger);
|
|
354
422
|
return info;
|
|
355
423
|
}
|
|
356
424
|
function applyDefaultPerLevel(actions, logger) {
|
|
@@ -364,6 +432,26 @@ function applyDefaultPerLevel(actions, logger) {
|
|
|
364
432
|
} else winners.set(a.level, a.name);
|
|
365
433
|
}
|
|
366
434
|
}
|
|
435
|
+
/**
|
|
436
|
+
* Emit structural-copy fields plus `disabled` (stringified) and
|
|
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) {
|
|
443
|
+
const disabled = source.disabled;
|
|
444
|
+
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
|
+
copyOptionalFields(info, source);
|
|
451
|
+
if (Array.isArray(info.promptText)) info.promptText = info.promptText.slice();
|
|
452
|
+
if (hasDisabled) info.disabled = disabled.toString();
|
|
453
|
+
if (Array.isArray(requiredFields)) info.requiredFields = requiredFields.slice();
|
|
454
|
+
}
|
|
367
455
|
function copyOptionalFields(info, source) {
|
|
368
456
|
for (const key of OPTIONAL_FIELDS) {
|
|
369
457
|
const value = source[key];
|
|
@@ -1006,6 +1094,7 @@ AsDbReadableController = __decorate([
|
|
|
1006
1094
|
__decorateParam(0, (0, moost.Inject)(READABLE_DEF)),
|
|
1007
1095
|
__decorateMetadata("design:paramtypes", [Object, typeof (_ref$3 = typeof moost.Moost !== "undefined" && moost.Moost) === "function" ? _ref$3 : Object])
|
|
1008
1096
|
], AsDbReadableController);
|
|
1097
|
+
registerAsDbReadableController(AsDbReadableController);
|
|
1009
1098
|
//#endregion
|
|
1010
1099
|
//#region src/as-db.controller.ts
|
|
1011
1100
|
var _ref$2, _ref2$1;
|
|
@@ -1304,6 +1393,7 @@ AsValueHelpController = __decorate([(0, moost.Inherit)(), __decorateMetadata("de
|
|
|
1304
1393
|
String,
|
|
1305
1394
|
typeof (_ref$1 = typeof moost.Moost !== "undefined" && moost.Moost) === "function" ? _ref$1 : Object
|
|
1306
1395
|
])], AsValueHelpController);
|
|
1396
|
+
registerAsValueHelpController(AsValueHelpController);
|
|
1307
1397
|
//#endregion
|
|
1308
1398
|
//#region src/as-json-value-help.controller.ts
|
|
1309
1399
|
var _ref;
|
|
@@ -1464,89 +1554,43 @@ function applySelect(rows, select) {
|
|
|
1464
1554
|
});
|
|
1465
1555
|
}
|
|
1466
1556
|
//#endregion
|
|
1467
|
-
//#region src/actions/
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
* Metadata-only — pair with `@Post(...)` for Moost to bind the route. The
|
|
1472
|
-
* meta builder reads this metadata plus the bound POST path lazily and
|
|
1473
|
-
* emits the action with `processor: 'backend'`. Order vs.
|
|
1474
|
-
* `@DbActionDefault()` does not matter — both merge into the same slot.
|
|
1475
|
-
*
|
|
1476
|
-
* @example
|
|
1477
|
-
* ```ts
|
|
1478
|
-
* @Post('actions/block')
|
|
1479
|
-
* @DbAction('block', { label: 'Block', icon: 'i-as-block', intent: 'negative' })
|
|
1480
|
-
* async blockUser(@DbActionPK() id: string) { ... }
|
|
1481
|
-
* ```
|
|
1482
|
-
*/
|
|
1483
|
-
function DbAction(name, opts = {}) {
|
|
1484
|
-
return (0, moost.getMoostMate)().decorate((current) => {
|
|
1485
|
-
const meta = current;
|
|
1486
|
-
return {
|
|
1487
|
-
...current,
|
|
1488
|
-
[MOOST_DB_ACTION]: mergeActionMeta(meta, {
|
|
1489
|
-
name,
|
|
1490
|
-
opts
|
|
1491
|
-
})
|
|
1492
|
-
};
|
|
1493
|
-
});
|
|
1557
|
+
//#region src/actions/action-disabled-error.ts
|
|
1558
|
+
function buildMessage(action, pks) {
|
|
1559
|
+
if (pks !== void 0) return `Action "${action}" is disabled for ${pks.length} of the selected rows`;
|
|
1560
|
+
return `Action "${action}" is disabled for this row`;
|
|
1494
1561
|
}
|
|
1495
|
-
//#endregion
|
|
1496
|
-
//#region src/actions/db-action-default.decorator.ts
|
|
1497
1562
|
/**
|
|
1498
|
-
*
|
|
1499
|
-
*
|
|
1563
|
+
* Thrown by the gate interceptor when `disabled` returns truthy. Composes
|
|
1564
|
+
* with Moost's existing error mapper to produce HTTP 409 with the wire body
|
|
1565
|
+
* defined by {@link ActionDisabledErrorBody}.
|
|
1566
|
+
*
|
|
1567
|
+
* - `'row'`-level rejection: pass `(action, pk)` — the body emits `pk`.
|
|
1568
|
+
* - `'rows'`-level rejection: pass `(action, undefined, pks)` — the body
|
|
1569
|
+
* emits `pks` (the FULL list of failing PKs in reject mode; the FULL list
|
|
1570
|
+
* of request PKs in skip mode with zero survivors).
|
|
1500
1571
|
*/
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1572
|
+
var ActionDisabledError = class extends _moostjs_event_http.HttpError {
|
|
1573
|
+
name = "ActionDisabledError";
|
|
1574
|
+
constructor(action, pk, pks) {
|
|
1575
|
+
const body = {
|
|
1576
|
+
name: "ActionDisabledError",
|
|
1577
|
+
message: buildMessage(action, pks),
|
|
1578
|
+
statusCode: 409,
|
|
1579
|
+
action
|
|
1507
1580
|
};
|
|
1508
|
-
|
|
1509
|
-
|
|
1581
|
+
if (pks !== void 0) body.pks = pks;
|
|
1582
|
+
else if (pk !== void 0) body.pk = pk;
|
|
1583
|
+
super(409, body);
|
|
1584
|
+
}
|
|
1585
|
+
};
|
|
1510
1586
|
//#endregion
|
|
1511
|
-
//#region src/actions/pk-
|
|
1512
|
-
/**
|
|
1513
|
-
* Extract the PK validation source from a controller instance. Looks for
|
|
1514
|
-
* `readable` (set by {@link AsDbReadableController}) or `table` (set by
|
|
1515
|
-
* {@link AsDbController}).
|
|
1516
|
-
*
|
|
1517
|
-
* If the controller has no typed table attached (e.g. a value-help
|
|
1518
|
-
* controller, or a plain Moost controller without `@TableController`),
|
|
1519
|
-
* throws an HTTP 500 — this is a **server misconfiguration**, not a client
|
|
1520
|
-
* error. The body parser has nothing to validate against, so the request
|
|
1521
|
-
* cannot proceed. Use `@Body()` and parse the PK manually if you need to
|
|
1522
|
-
* accept PK-shaped bodies on a controller without an attached table.
|
|
1523
|
-
*/
|
|
1524
|
-
function resolvePkSource(controller) {
|
|
1525
|
-
const c = controller;
|
|
1526
|
-
const candidate = c.readable ?? c.table;
|
|
1527
|
-
if (!isPkValidationSource(candidate)) throw new _moostjs_event_http.HttpError(500, "@DbActionPK/@DbActionPKs requires a controller with an attached table (via @TableController / @ReadableController). Use @Body() instead if your controller has no typed table.");
|
|
1528
|
-
return candidate;
|
|
1529
|
-
}
|
|
1587
|
+
//#region src/actions/pk-validation.ts
|
|
1530
1588
|
function isPkValidationSource(value) {
|
|
1531
1589
|
if (!value || typeof value !== "object") return false;
|
|
1532
1590
|
const v = value;
|
|
1533
1591
|
return Array.isArray(v.primaryKeys) && Array.isArray(v.fieldDescriptors);
|
|
1534
1592
|
}
|
|
1535
1593
|
/**
|
|
1536
|
-
* Build a parameter decorator that parses the JSON request body, validates
|
|
1537
|
-
* it against the bound table's PK schema with `validate`, and tags the param
|
|
1538
|
-
* so {@link discoverActions} can infer the action's `level`.
|
|
1539
|
-
*/
|
|
1540
|
-
function createPkParamDecorator(kind, validate, resolverName) {
|
|
1541
|
-
return (0, moost.ApplyDecorators)((0, moost.getMoostMate)().decorate(MOOST_DB_ACTION_PARAM, kind), (0, moost.Resolve)(async () => {
|
|
1542
|
-
const body = await (0, _wooksjs_http_body.useBody)().parseBody();
|
|
1543
|
-
validate(body, resolvePkSource((0, moost.useControllerContext)().getController()));
|
|
1544
|
-
return body;
|
|
1545
|
-
}, resolverName));
|
|
1546
|
-
}
|
|
1547
|
-
//#endregion
|
|
1548
|
-
//#region src/actions/pk-validation.ts
|
|
1549
|
-
/**
|
|
1550
1594
|
* Validate a JSON-decoded body against a single-row PK shape (scalar or
|
|
1551
1595
|
* composite). Throws {@link ValidatorError} with structured `errors` so the
|
|
1552
1596
|
* existing validation interceptor returns HTTP 400.
|
|
@@ -1631,6 +1675,229 @@ function isPlainObject(value) {
|
|
|
1631
1675
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1632
1676
|
}
|
|
1633
1677
|
//#endregion
|
|
1678
|
+
//#region src/actions/pk-cache.ts
|
|
1679
|
+
const boundTableKey = (0, _wooksjs_event_core.key)("atscript_db_action_bound_table");
|
|
1680
|
+
function getActionTable(ctx) {
|
|
1681
|
+
if (ctx.has(boundTableKey)) {
|
|
1682
|
+
const fromSlot = ctx.get(boundTableKey);
|
|
1683
|
+
if (fromSlot) return fromSlot;
|
|
1684
|
+
}
|
|
1685
|
+
const ctrl = (0, moost.useControllerContext)(ctx).getController();
|
|
1686
|
+
if (ctrl) {
|
|
1687
|
+
const t = ctrl.readable ?? ctrl.table;
|
|
1688
|
+
if (t) return t;
|
|
1689
|
+
}
|
|
1690
|
+
return null;
|
|
1691
|
+
}
|
|
1692
|
+
function noTableError(ctx) {
|
|
1693
|
+
const cc = (0, moost.useControllerContext)(ctx);
|
|
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;
|
|
1698
|
+
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
|
+
}
|
|
1700
|
+
async function resolveValidatedPk(ctx, validate) {
|
|
1701
|
+
const table = getActionTable(ctx);
|
|
1702
|
+
if (!isPkValidationSource(table)) throw noTableError(ctx);
|
|
1703
|
+
const body = await (0, _wooksjs_http_body.useBody)(ctx).parseBody();
|
|
1704
|
+
validate(body, table);
|
|
1705
|
+
return body;
|
|
1706
|
+
}
|
|
1707
|
+
const dbActionPkSlot = (0, _wooksjs_event_core.cached)((ctx) => resolveValidatedPk(ctx, validateSinglePk));
|
|
1708
|
+
const dbActionPksSlot = (0, _wooksjs_event_core.cached)(async (ctx) => {
|
|
1709
|
+
return await resolveValidatedPk(ctx, validateMultiPk);
|
|
1710
|
+
});
|
|
1711
|
+
const useDbActionPk = (0, _wooksjs_event_core.defineWook)((ctx) => ({ load: () => ctx.get(dbActionPkSlot) }));
|
|
1712
|
+
const useDbActionPks = (0, _wooksjs_event_core.defineWook)((ctx) => ({ load: () => ctx.get(dbActionPksSlot) }));
|
|
1713
|
+
//#endregion
|
|
1714
|
+
//#region src/actions/row-cache.ts
|
|
1715
|
+
function asFetchTable(value) {
|
|
1716
|
+
if (!value || typeof value !== "object") return null;
|
|
1717
|
+
const v = value;
|
|
1718
|
+
return Array.isArray(v.primaryKeys) && typeof v.findById === "function" && typeof v.findMany === "function" ? v : null;
|
|
1719
|
+
}
|
|
1720
|
+
function noTable() {
|
|
1721
|
+
throw new _moostjs_event_http.HttpError(500, `${WARN_PREFIX} cached row wook: no bound table`);
|
|
1722
|
+
}
|
|
1723
|
+
async function loadRow(ctx) {
|
|
1724
|
+
const pk = await ctx.get(dbActionPkSlot);
|
|
1725
|
+
const row = await (asFetchTable(getActionTable(ctx)) ?? noTable()).findById(pk);
|
|
1726
|
+
if (row == null) throw new _moostjs_event_http.HttpError(404, "Row not found for action PK");
|
|
1727
|
+
return row;
|
|
1728
|
+
}
|
|
1729
|
+
async function loadRows(ctx) {
|
|
1730
|
+
const pks = await ctx.get(dbActionPksSlot);
|
|
1731
|
+
const table = asFetchTable(getActionTable(ctx)) ?? noTable();
|
|
1732
|
+
if (pks.length === 0) return [];
|
|
1733
|
+
const { primaryKeys } = table;
|
|
1734
|
+
const rows = await table.findMany({ filter: buildPksFilter(pks, primaryKeys) });
|
|
1735
|
+
if (primaryKeys.length === 1) {
|
|
1736
|
+
const field = primaryKeys[0];
|
|
1737
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
1738
|
+
for (const row of rows) byKey.set(row[field], row);
|
|
1739
|
+
const ordered = [];
|
|
1740
|
+
for (const pk of pks) {
|
|
1741
|
+
const found = byKey.get(pk);
|
|
1742
|
+
if (found !== void 0) ordered.push(found);
|
|
1743
|
+
}
|
|
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
|
+
}
|
|
1776
|
+
return out;
|
|
1777
|
+
}
|
|
1778
|
+
const dbActionRowSlot = (0, _wooksjs_event_core.cached)((ctx) => loadRow(ctx));
|
|
1779
|
+
const dbActionRowsSlot = (0, _wooksjs_event_core.cached)((ctx) => loadRows(ctx));
|
|
1780
|
+
const useDbActionRow = (0, _wooksjs_event_core.defineWook)((ctx) => ({ load: () => ctx.get(dbActionRowSlot) }));
|
|
1781
|
+
const useDbActionRows = (0, _wooksjs_event_core.defineWook)((ctx) => ({ load: () => ctx.get(dbActionRowsSlot) }));
|
|
1782
|
+
//#endregion
|
|
1783
|
+
//#region src/actions/gate-interceptor.ts
|
|
1784
|
+
const GATE_PRIORITY = moost.TInterceptorPriority.AFTER_GUARD;
|
|
1785
|
+
function injectBoundTable(table) {
|
|
1786
|
+
const ctx = (0, _wooksjs_event_core.current)();
|
|
1787
|
+
if (ctx.has(boundTableKey)) return;
|
|
1788
|
+
const controller = (0, moost.useControllerContext)(ctx).getController();
|
|
1789
|
+
if (isAsDbReadableControllerInstance(controller)) {
|
|
1790
|
+
ctx.set(boundTableKey, controller.readable);
|
|
1791
|
+
return;
|
|
1792
|
+
}
|
|
1793
|
+
if (table != null) ctx.set(boundTableKey, table);
|
|
1794
|
+
}
|
|
1795
|
+
function buildGateInterceptor(opts) {
|
|
1796
|
+
const { action, level, disabled, onDisabledRows, table } = opts;
|
|
1797
|
+
return (0, moost.defineBeforeInterceptor)(async () => {
|
|
1798
|
+
injectBoundTable(table);
|
|
1799
|
+
const ctx = (0, _wooksjs_event_core.current)();
|
|
1800
|
+
if (level === "row") {
|
|
1801
|
+
if (disabled(await ctx.get(dbActionRowSlot))) throw new ActionDisabledError(action, await ctx.get(dbActionPkSlot));
|
|
1802
|
+
return;
|
|
1803
|
+
}
|
|
1804
|
+
const pks = await ctx.get(dbActionPksSlot);
|
|
1805
|
+
const rows = await ctx.get(dbActionRowsSlot);
|
|
1806
|
+
const failingPks = [];
|
|
1807
|
+
const passingRows = [];
|
|
1808
|
+
const passingPks = [];
|
|
1809
|
+
for (let i = 0; i < rows.length; i++) if (disabled(rows[i])) failingPks.push(pks[i]);
|
|
1810
|
+
else {
|
|
1811
|
+
passingRows.push(rows[i]);
|
|
1812
|
+
passingPks.push(pks[i]);
|
|
1813
|
+
}
|
|
1814
|
+
if (onDisabledRows === "skip") {
|
|
1815
|
+
if (passingRows.length === 0) throw new ActionDisabledError(action, void 0, [...pks]);
|
|
1816
|
+
if (failingPks.length > 0) {
|
|
1817
|
+
ctx.set(dbActionRowsSlot, Promise.resolve(passingRows));
|
|
1818
|
+
ctx.set(dbActionPksSlot, Promise.resolve(passingPks));
|
|
1819
|
+
}
|
|
1820
|
+
return;
|
|
1821
|
+
}
|
|
1822
|
+
if (failingPks.length > 0) throw new ActionDisabledError(action, void 0, failingPks);
|
|
1823
|
+
}, GATE_PRIORITY);
|
|
1824
|
+
}
|
|
1825
|
+
/** Thin interceptor for `@DbActionRow*` without `disabled` — injects only the bound table. */
|
|
1826
|
+
function buildThinInterceptor(opts) {
|
|
1827
|
+
const { table } = opts;
|
|
1828
|
+
return (0, moost.defineBeforeInterceptor)(() => {
|
|
1829
|
+
injectBoundTable(table);
|
|
1830
|
+
}, GATE_PRIORITY);
|
|
1831
|
+
}
|
|
1832
|
+
//#endregion
|
|
1833
|
+
//#region src/actions/db-action.decorator.ts
|
|
1834
|
+
/**
|
|
1835
|
+
* Mark a controller method as a database action surfaced via `/meta`. Writes
|
|
1836
|
+
* `MOOST_DB_ACTION` metadata and registers a Moost interceptor when needed
|
|
1837
|
+
* (gate when `disabled` is set, thin bound-table injector when only
|
|
1838
|
+
* `@DbActionRow*` is present). Stacking two `@DbAction` on the same method
|
|
1839
|
+
* is undefined and emits a warning.
|
|
1840
|
+
*/
|
|
1841
|
+
function DbAction(name, opts = {}) {
|
|
1842
|
+
const mate = (0, moost.getMoostMate)();
|
|
1843
|
+
return ((target, propertyKey, descriptor) => {
|
|
1844
|
+
const priorName = mate.read(target, propertyKey)?.[MOOST_DB_ACTION]?.name;
|
|
1845
|
+
if (priorName) console.warn(`${WARN_PREFIX} stacking @DbAction on the same method is undefined; declare one per method. Detected: "${priorName}" and "${name}".`);
|
|
1846
|
+
mate.decorate((current) => {
|
|
1847
|
+
const meta = current;
|
|
1848
|
+
return {
|
|
1849
|
+
...current,
|
|
1850
|
+
[MOOST_DB_ACTION]: mergeActionMeta(meta, {
|
|
1851
|
+
name,
|
|
1852
|
+
opts
|
|
1853
|
+
})
|
|
1854
|
+
};
|
|
1855
|
+
})(target, propertyKey, descriptor);
|
|
1856
|
+
if (isAsValueHelpControllerSubclass(typeof target === "function" ? target : target.constructor)) return descriptor;
|
|
1857
|
+
const scan = scanParamLevel(mate.read(target, propertyKey)?.params ?? []);
|
|
1858
|
+
if (!!opts.disabled && (scan.level === "row" || scan.level === "rows")) (0, moost.Intercept)(buildGateInterceptor({
|
|
1859
|
+
action: name,
|
|
1860
|
+
level: scan.level,
|
|
1861
|
+
disabled: opts.disabled,
|
|
1862
|
+
onDisabledRows: opts.onDisabledRows ?? "reject",
|
|
1863
|
+
table: opts.table
|
|
1864
|
+
}))(target, propertyKey, descriptor);
|
|
1865
|
+
else if (scan.hasRowParam) (0, moost.Intercept)(buildThinInterceptor({ table: opts.table }))(target, propertyKey, descriptor);
|
|
1866
|
+
return descriptor;
|
|
1867
|
+
});
|
|
1868
|
+
}
|
|
1869
|
+
//#endregion
|
|
1870
|
+
//#region src/actions/db-action-default.decorator.ts
|
|
1871
|
+
/**
|
|
1872
|
+
* Sugar that flips `default: true` on the same method's `@DbAction` metadata.
|
|
1873
|
+
* Equivalent to passing `opts.default = true`. Decorator order does not matter.
|
|
1874
|
+
*/
|
|
1875
|
+
function DbActionDefault() {
|
|
1876
|
+
return (0, moost.getMoostMate)().decorate((current) => {
|
|
1877
|
+
const meta = current;
|
|
1878
|
+
return {
|
|
1879
|
+
...current,
|
|
1880
|
+
[MOOST_DB_ACTION]: mergeActionMeta(meta, { opts: { default: true } })
|
|
1881
|
+
};
|
|
1882
|
+
});
|
|
1883
|
+
}
|
|
1884
|
+
//#endregion
|
|
1885
|
+
//#region src/actions/pk-source.ts
|
|
1886
|
+
/**
|
|
1887
|
+
* Build a parameter decorator that reads its value from the cached PK wook
|
|
1888
|
+
* (single or multi). Validation runs inside the wook factory exactly once
|
|
1889
|
+
* per request, regardless of how many readers consume the value (`@DbActionPK*`
|
|
1890
|
+
* resolver, gate interceptor, cached row wook, in-handler composables).
|
|
1891
|
+
*
|
|
1892
|
+
* Marks the param so {@link discoverActions} can infer the action's `level`.
|
|
1893
|
+
*/
|
|
1894
|
+
function createPkParamDecorator(kind) {
|
|
1895
|
+
const mate = (0, moost.getMoostMate)();
|
|
1896
|
+
const slot = kind === "pk" ? dbActionPkSlot : dbActionPksSlot;
|
|
1897
|
+
const resolverName = kind === "pk" ? "dbActionPk" : "dbActionPks";
|
|
1898
|
+
return (0, moost.ApplyDecorators)(mate.decorate(MOOST_DB_ACTION_PARAM, kind), (0, moost.Resolve)(async () => (0, _wooksjs_event_core.current)().get(slot), resolverName));
|
|
1899
|
+
}
|
|
1900
|
+
//#endregion
|
|
1634
1901
|
//#region src/actions/db-action-pk.decorator.ts
|
|
1635
1902
|
/**
|
|
1636
1903
|
* Parameter resolver that reads the primary key from the JSON request body
|
|
@@ -1645,9 +1912,13 @@ function isPlainObject(value) {
|
|
|
1645
1912
|
*
|
|
1646
1913
|
* Marks the param so {@link discoverActions} can infer the action's `level`
|
|
1647
1914
|
* as `'row'`.
|
|
1915
|
+
*
|
|
1916
|
+
* Implementation note: the resolver is a thin reader of the cached PK wook
|
|
1917
|
+
* — validation logic lives in the wook factory, which runs once per request
|
|
1918
|
+
* regardless of how many readers consume the value.
|
|
1648
1919
|
*/
|
|
1649
1920
|
function DbActionPK() {
|
|
1650
|
-
return createPkParamDecorator("pk"
|
|
1921
|
+
return createPkParamDecorator("pk");
|
|
1651
1922
|
}
|
|
1652
1923
|
//#endregion
|
|
1653
1924
|
//#region src/actions/db-action-pks.decorator.ts
|
|
@@ -1660,9 +1931,44 @@ function DbActionPK() {
|
|
|
1660
1931
|
*
|
|
1661
1932
|
* Validation is strict — no type coercion. Marks the param so
|
|
1662
1933
|
* {@link discoverActions} can infer the action's `level` as `'rows'`.
|
|
1934
|
+
*
|
|
1935
|
+
* In `'rows'` skip mode the resolved value reflects the gate interceptor's
|
|
1936
|
+
* filtered subset (the cached PK slot is overwritten in place); see
|
|
1937
|
+
* {@link dbActionPksSlot} for precedence details.
|
|
1663
1938
|
*/
|
|
1664
1939
|
function DbActionPKs() {
|
|
1665
|
-
return createPkParamDecorator("pks"
|
|
1940
|
+
return createPkParamDecorator("pks");
|
|
1941
|
+
}
|
|
1942
|
+
//#endregion
|
|
1943
|
+
//#region src/actions/db-action-row.decorator.ts
|
|
1944
|
+
function createRowParamDecorator(metaKey, slot, resolverName) {
|
|
1945
|
+
return (0, moost.ApplyDecorators)((0, moost.getMoostMate)().decorate(metaKey, true), (0, moost.Resolve)(async () => (0, _wooksjs_event_core.current)().get(slot), resolverName));
|
|
1946
|
+
}
|
|
1947
|
+
/**
|
|
1948
|
+
* Parameter decorator that injects the row whose PK was supplied in the
|
|
1949
|
+
* request body. Reads from the cached row wook (fetched once per request,
|
|
1950
|
+
* shared with the gate interceptor when `disabled` is set).
|
|
1951
|
+
*
|
|
1952
|
+
* Marks the param so {@link discoverActions} infers the action's `level` as
|
|
1953
|
+
* `'row'`. Co-occurrence with `@DbActionRows()` (or any multi-cardinality
|
|
1954
|
+
* decorator) drops the action with a warning.
|
|
1955
|
+
*
|
|
1956
|
+
* In `'skip'` mode this returns the gate's filtered row; the original
|
|
1957
|
+
* request-body row is not retrievable.
|
|
1958
|
+
*/
|
|
1959
|
+
function DbActionRow() {
|
|
1960
|
+
return createRowParamDecorator(MOOST_DB_ACTION_ROW, dbActionRowSlot, "dbActionRow");
|
|
1961
|
+
}
|
|
1962
|
+
/**
|
|
1963
|
+
* Parameter decorator that injects the rows-array fetched by primary keys
|
|
1964
|
+
* from the request body. Reads from the cached row-array wook.
|
|
1965
|
+
*
|
|
1966
|
+
* Marks the param so {@link discoverActions} infers the action's `level` as
|
|
1967
|
+
* `'rows'`. In `'rows'` + `'skip'` mode the resolved value contains only the
|
|
1968
|
+
* gate's surviving rows.
|
|
1969
|
+
*/
|
|
1970
|
+
function DbActionRows() {
|
|
1971
|
+
return createRowParamDecorator(MOOST_DB_ACTION_ROWS, dbActionRowsSlot, "dbActionRows");
|
|
1666
1972
|
}
|
|
1667
1973
|
//#endregion
|
|
1668
1974
|
//#region src/actions/db-actions.decorator.ts
|
|
@@ -1698,11 +2004,16 @@ function DbRowsActions(dict) {
|
|
|
1698
2004
|
}
|
|
1699
2005
|
function classLevelActions(dict, forcedLevel) {
|
|
1700
2006
|
const entries = [];
|
|
1701
|
-
for (const [name, entry] of Object.entries(dict))
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
2007
|
+
for (const [name, entry] of Object.entries(dict)) {
|
|
2008
|
+
const merged = forcedLevel ? {
|
|
2009
|
+
...entry,
|
|
2010
|
+
level: forcedLevel
|
|
2011
|
+
} : entry;
|
|
2012
|
+
entries.push({
|
|
2013
|
+
name,
|
|
2014
|
+
entry: merged
|
|
2015
|
+
});
|
|
2016
|
+
}
|
|
1706
2017
|
return (0, moost.getMoostMate)().decorate((current) => {
|
|
1707
2018
|
const existing = current["atscript_db_actions"] ?? [];
|
|
1708
2019
|
return {
|
|
@@ -1712,6 +2023,7 @@ function classLevelActions(dict, forcedLevel) {
|
|
|
1712
2023
|
});
|
|
1713
2024
|
}
|
|
1714
2025
|
//#endregion
|
|
2026
|
+
exports.ActionDisabledError = ActionDisabledError;
|
|
1715
2027
|
Object.defineProperty(exports, "AsDbController", {
|
|
1716
2028
|
enumerable: true,
|
|
1717
2029
|
get: function() {
|
|
@@ -1746,6 +2058,8 @@ exports.DbAction = DbAction;
|
|
|
1746
2058
|
exports.DbActionDefault = DbActionDefault;
|
|
1747
2059
|
exports.DbActionPK = DbActionPK;
|
|
1748
2060
|
exports.DbActionPKs = DbActionPKs;
|
|
2061
|
+
exports.DbActionRow = DbActionRow;
|
|
2062
|
+
exports.DbActionRows = DbActionRows;
|
|
1749
2063
|
exports.DbActions = DbActions;
|
|
1750
2064
|
exports.DbRowActions = DbRowActions;
|
|
1751
2065
|
exports.DbRowsActions = DbRowsActions;
|
|
@@ -1760,4 +2074,8 @@ exports.TableController = TableController;
|
|
|
1760
2074
|
exports.UseValidationErrorTransform = UseValidationErrorTransform;
|
|
1761
2075
|
exports.ViewController = ViewController;
|
|
1762
2076
|
exports.discoverActions = discoverActions;
|
|
2077
|
+
exports.useDbActionPk = useDbActionPk;
|
|
2078
|
+
exports.useDbActionPks = useDbActionPks;
|
|
2079
|
+
exports.useDbActionRow = useDbActionRow;
|
|
2080
|
+
exports.useDbActionRows = useDbActionRows;
|
|
1763
2081
|
exports.validationErrorTransform = validationErrorTransform;
|