@atscript/moost-db 0.1.57 → 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 +881 -245
- package/dist/index.d.cts +274 -71
- package/dist/index.d.mts +274 -71
- package/dist/index.mjs +873 -245
- 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 };
|
|
@@ -98,7 +99,7 @@ var SelectControlDto = class {
|
|
|
98
99
|
(0, _atscript_typescript_utils.throwFeatureDisabled)("JSON Schema", "jsonSchema", "emit.jsonSchema");
|
|
99
100
|
}
|
|
100
101
|
};
|
|
101
|
-
(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);
|
|
102
103
|
(0, _atscript_typescript_utils.defineAnnotatedType)("object", PagesControlsDto).prop("$page", (0, _atscript_typescript_utils.defineAnnotatedType)().designType("string").tags("string").annotate("expect.pattern", {
|
|
103
104
|
pattern: "^\\d+$",
|
|
104
105
|
flags: "u",
|
|
@@ -107,8 +108,8 @@ var SelectControlDto = class {
|
|
|
107
108
|
pattern: "^\\d+$",
|
|
108
109
|
flags: "u",
|
|
109
110
|
message: "Expected positive number"
|
|
110
|
-
}, 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);
|
|
111
|
-
(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);
|
|
112
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);
|
|
113
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);
|
|
114
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);
|
|
@@ -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
|
-
/** Param-level metadata key — written by `@
|
|
213
|
+
/** Param-level metadata key — written by `@DbActionID()` / `@DbActionIDs()`. 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,39 +232,70 @@ 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 === "id") single = true;
|
|
244
|
+
else if (kind === "ids") 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
|
+
/** Structural-copy fields; `disabled` is handled separately in {@link emitInfo} (function-to-string transform). */
|
|
210
266
|
const OPTIONAL_FIELDS = [
|
|
211
267
|
"icon",
|
|
212
268
|
"intent",
|
|
213
269
|
"description",
|
|
214
270
|
"order",
|
|
215
271
|
"default",
|
|
216
|
-
"promptText"
|
|
272
|
+
"promptText",
|
|
273
|
+
"shortcut"
|
|
217
274
|
];
|
|
218
|
-
const WARN_PREFIX = "[moost-db actions]";
|
|
219
275
|
const actionsCache = /* @__PURE__ */ new WeakMap();
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
* Reads class + method metadata via `getMoostMate()` and resolves bound POST
|
|
223
|
-
* paths through the Moost controller overview.
|
|
224
|
-
*
|
|
225
|
-
* Result is memoized per controller constructor — discovery walks every
|
|
226
|
-
* handler entry and reads decorator metadata, which is wasted work to repeat
|
|
227
|
-
* across instances.
|
|
228
|
-
*/
|
|
276
|
+
const rowLevelActionsCache = /* @__PURE__ */ new WeakMap();
|
|
277
|
+
/** Discover actions on a controller, memoized per ctor. `info`-only callers map `e => e.info`. */
|
|
229
278
|
function discoverActions(controllerCtor, app, logger) {
|
|
230
279
|
const cached = actionsCache.get(controllerCtor);
|
|
231
280
|
if (cached) return cached;
|
|
232
281
|
const overview = app.getControllersOverview?.()?.find((o) => o.type === controllerCtor);
|
|
233
282
|
const out = [];
|
|
234
|
-
|
|
235
|
-
|
|
283
|
+
const seen = /* @__PURE__ */ new Set();
|
|
284
|
+
collectMethodActions(controllerCtor, overview, logger, out, seen);
|
|
285
|
+
collectClassActions(controllerCtor, logger, out, seen);
|
|
236
286
|
applyDefaultPerLevel(out, logger);
|
|
237
287
|
actionsCache.set(controllerCtor, out);
|
|
238
288
|
return out;
|
|
239
289
|
}
|
|
240
|
-
|
|
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) {
|
|
241
299
|
if (!overview) return;
|
|
242
300
|
const byMethod = /* @__PURE__ */ new Map();
|
|
243
301
|
for (const h of overview.handlers) {
|
|
@@ -256,9 +314,25 @@ function collectMethodActions(ctor, overview, logger, out) {
|
|
|
256
314
|
const levelInfer = inferMethodLevel(methodMeta.params ?? [], action.name, logger);
|
|
257
315
|
if (!levelInfer) continue;
|
|
258
316
|
if (levelInfer.bodyConflict) {
|
|
259
|
-
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`);
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
if (levelInfer.level === "table" && action.opts.disabled !== void 0) {
|
|
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`);
|
|
260
322
|
continue;
|
|
261
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
|
+
}
|
|
328
|
+
if (action.opts.disabled !== void 0 || levelInfer.hasRowParam) {
|
|
329
|
+
const extendsReadable = isAsDbReadableControllerSubclass(ctor);
|
|
330
|
+
const hasOptsTable = action.opts.table != null;
|
|
331
|
+
if (!extendsReadable && !hasOptsTable) {
|
|
332
|
+
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`);
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
262
336
|
const postEntry = handlers.find((h) => h.handler.type === "HTTP" && h.handler.method === "POST");
|
|
263
337
|
if (!postEntry) {
|
|
264
338
|
logger.warn(`${WARN_PREFIX} action "${action.name}" requires @Post(...); no POST handler bound to ${methodName} — dropping`);
|
|
@@ -274,6 +348,10 @@ function collectMethodActions(ctor, overview, logger, out) {
|
|
|
274
348
|
logger.warn(`${WARN_PREFIX} action "${action.name}" requires a label (opts.label or @Label) — dropping`);
|
|
275
349
|
continue;
|
|
276
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
|
+
}
|
|
277
355
|
const info = {
|
|
278
356
|
name: action.name,
|
|
279
357
|
label,
|
|
@@ -281,40 +359,46 @@ function collectMethodActions(ctor, overview, logger, out) {
|
|
|
281
359
|
processor: "backend",
|
|
282
360
|
value: path
|
|
283
361
|
};
|
|
284
|
-
|
|
285
|
-
|
|
362
|
+
emitInfo(info, action.opts);
|
|
363
|
+
seen.add(action.name);
|
|
364
|
+
out.push({
|
|
365
|
+
info,
|
|
366
|
+
raw: action.opts
|
|
367
|
+
});
|
|
286
368
|
}
|
|
287
369
|
}
|
|
288
370
|
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`);
|
|
371
|
+
const scan = scanParamLevel(params);
|
|
372
|
+
if (scan.single && scan.multi) {
|
|
373
|
+
logger.warn(`${WARN_PREFIX} action "${actionName}" mixes single-cardinality and multi-cardinality decorators (@DbActionID / @DbActionRow vs @DbActionIDs / @DbActionRows) — dropping`);
|
|
300
374
|
return null;
|
|
301
375
|
}
|
|
302
|
-
const level = hasPk ? "row" : hasPks ? "rows" : "table";
|
|
303
376
|
return {
|
|
304
|
-
level,
|
|
305
|
-
bodyConflict: hasBody && level !== "table"
|
|
377
|
+
level: scan.level,
|
|
378
|
+
bodyConflict: scan.hasBody && scan.level !== "table",
|
|
379
|
+
hasRowParam: scan.hasRowParam
|
|
306
380
|
};
|
|
307
381
|
}
|
|
308
|
-
function collectClassActions(ctor, logger, out) {
|
|
382
|
+
function collectClassActions(ctor, logger, out, seen) {
|
|
309
383
|
const list = (0, moost.getMoostMate)().read(ctor)?.[MOOST_DB_ACTIONS];
|
|
310
384
|
if (!list) return;
|
|
311
|
-
for (const { name, entry
|
|
312
|
-
|
|
313
|
-
|
|
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
|
+
}
|
|
390
|
+
const built = buildClassEntry(name, entry, logger);
|
|
391
|
+
if (built) {
|
|
392
|
+
seen.add(name);
|
|
393
|
+
out.push({
|
|
394
|
+
info: built,
|
|
395
|
+
raw: entry
|
|
396
|
+
});
|
|
397
|
+
}
|
|
314
398
|
}
|
|
315
399
|
}
|
|
316
|
-
function buildClassEntry(name, entry,
|
|
317
|
-
const level =
|
|
400
|
+
function buildClassEntry(name, entry, logger) {
|
|
401
|
+
const level = entry.level;
|
|
318
402
|
if (!level) {
|
|
319
403
|
logger.warn(`${WARN_PREFIX} class-level action "${name}" requires a level — dropping. Use @DbTableActions/@DbRowActions/@DbRowsActions or set "level" explicitly.`);
|
|
320
404
|
return null;
|
|
@@ -323,6 +407,14 @@ function buildClassEntry(name, entry, forcedLevel, logger) {
|
|
|
323
407
|
logger.warn(`${WARN_PREFIX} class-level action "${name}" requires a label — dropping`);
|
|
324
408
|
return null;
|
|
325
409
|
}
|
|
410
|
+
if (level === "table" && entry.disabled !== void 0) {
|
|
411
|
+
logger.warn(`${WARN_PREFIX} class-level action "${name}" — \`disabled\` is not allowed at the 'table' level — dropping`);
|
|
412
|
+
return null;
|
|
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
|
+
}
|
|
326
418
|
const processor = entry.processor;
|
|
327
419
|
let value;
|
|
328
420
|
if (processor === "navigate" || processor === "backend") {
|
|
@@ -350,26 +442,37 @@ function buildClassEntry(name, entry, forcedLevel, logger) {
|
|
|
350
442
|
processor,
|
|
351
443
|
value
|
|
352
444
|
};
|
|
353
|
-
|
|
445
|
+
emitInfo(info, entry);
|
|
354
446
|
return info;
|
|
355
447
|
}
|
|
356
|
-
function applyDefaultPerLevel(
|
|
448
|
+
function applyDefaultPerLevel(envelopes, logger) {
|
|
357
449
|
const winners = /* @__PURE__ */ new Map();
|
|
358
|
-
for (const
|
|
359
|
-
if (!
|
|
360
|
-
const existing = winners.get(
|
|
450
|
+
for (const { info } of envelopes) {
|
|
451
|
+
if (!info.default) continue;
|
|
452
|
+
const existing = winners.get(info.level);
|
|
361
453
|
if (existing) {
|
|
362
|
-
|
|
363
|
-
logger.warn(`${WARN_PREFIX} duplicate default action at level "${
|
|
364
|
-
} 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);
|
|
365
457
|
}
|
|
366
458
|
}
|
|
459
|
+
/** Emit structural-copy fields plus stringified `disabled`. `requiredFields` is server-internal (never on the wire). */
|
|
460
|
+
function emitInfo(info, source) {
|
|
461
|
+
const disabled = source.disabled;
|
|
462
|
+
const hasDisabled = typeof disabled === "function";
|
|
463
|
+
copyOptionalFields(info, source);
|
|
464
|
+
if (Array.isArray(info.promptText)) info.promptText = info.promptText.slice();
|
|
465
|
+
if (hasDisabled) info.disabled = disabled.toString();
|
|
466
|
+
}
|
|
367
467
|
function copyOptionalFields(info, source) {
|
|
368
468
|
for (const key of OPTIONAL_FIELDS) {
|
|
369
469
|
const value = source[key];
|
|
370
470
|
if (value !== void 0) info[key] = value;
|
|
371
471
|
}
|
|
372
472
|
}
|
|
473
|
+
function isNonEmptyStringArray(value) {
|
|
474
|
+
return Array.isArray(value) && value.length > 0 && value.every((v) => typeof v === "string");
|
|
475
|
+
}
|
|
373
476
|
//#endregion
|
|
374
477
|
//#region \0@oxc-project+runtime@0.120.0/helpers/decorateMetadata.js
|
|
375
478
|
function __decorateMetadata(k, v) {
|
|
@@ -552,6 +655,7 @@ let AsReadableController = class AsReadableController {
|
|
|
552
655
|
vectorSearchable: false,
|
|
553
656
|
searchIndexes: [],
|
|
554
657
|
primaryKeys: [],
|
|
658
|
+
preferredId: [],
|
|
555
659
|
relations: [],
|
|
556
660
|
fields: {},
|
|
557
661
|
type: this.getSerializedType(),
|
|
@@ -565,7 +669,7 @@ let AsReadableController = class AsReadableController {
|
|
|
565
669
|
* controllers — see {@link AsValueHelpController#buildMetaResponse}.
|
|
566
670
|
*/
|
|
567
671
|
buildActions() {
|
|
568
|
-
return discoverActions(this.constructor, this.app, this.logger);
|
|
672
|
+
return discoverActions(this.constructor, this.app, this.logger).map((e) => e.info);
|
|
569
673
|
}
|
|
570
674
|
/**
|
|
571
675
|
* Declares the built-in CRUD operations this controller exposes. Subclasses
|
|
@@ -600,6 +704,80 @@ AsReadableController = __decorate([UseValidationErrorTransform(), __decorateMeta
|
|
|
600
704
|
Object
|
|
601
705
|
])], AsReadableController);
|
|
602
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
|
|
603
781
|
//#region src/decorators.ts
|
|
604
782
|
/**
|
|
605
783
|
* DI token under which the {@link AtscriptDbReadable} instance
|
|
@@ -694,10 +872,17 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
|
|
|
694
872
|
/** Reference to the underlying readable (table or view). */
|
|
695
873
|
readable;
|
|
696
874
|
_gates;
|
|
875
|
+
_preferredIdSet;
|
|
876
|
+
_compositeIdShapes;
|
|
877
|
+
_overlayIsNoOp;
|
|
697
878
|
constructor(readable, app) {
|
|
698
879
|
super(readable.type, readable.tableName, app, readable.isView ? "view" : "table");
|
|
699
880
|
this.readable = readable;
|
|
700
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;
|
|
701
886
|
}
|
|
702
887
|
_buildGates() {
|
|
703
888
|
const meta = this.readable.type.metadata;
|
|
@@ -765,16 +950,169 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
|
|
|
765
950
|
transformProjection(projection) {
|
|
766
951
|
return projection;
|
|
767
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
|
+
}
|
|
768
1107
|
/**
|
|
769
1108
|
* Extracts a composite identifier object from query params.
|
|
770
1109
|
* Tries composite primary key first, then compound unique indexes.
|
|
771
1110
|
*/
|
|
772
1111
|
extractCompositeId(query) {
|
|
773
|
-
const
|
|
774
|
-
if (pkFields.length > 1) {
|
|
1112
|
+
for (const id of this._compositeIdShapes) {
|
|
775
1113
|
const idObj = {};
|
|
776
1114
|
let allPresent = true;
|
|
777
|
-
for (const field of
|
|
1115
|
+
for (const field of id.fields) {
|
|
778
1116
|
if (query[field] === void 0) {
|
|
779
1117
|
allPresent = false;
|
|
780
1118
|
break;
|
|
@@ -783,19 +1121,6 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
|
|
|
783
1121
|
}
|
|
784
1122
|
if (allPresent) return idObj;
|
|
785
1123
|
}
|
|
786
|
-
for (const index of this.readable.indexes.values()) {
|
|
787
|
-
if (index.type !== "unique" || index.fields.length < 2) continue;
|
|
788
|
-
const idObj = {};
|
|
789
|
-
let allPresent = true;
|
|
790
|
-
for (const indexField of index.fields) {
|
|
791
|
-
if (query[indexField.name] === void 0) {
|
|
792
|
-
allPresent = false;
|
|
793
|
-
break;
|
|
794
|
-
}
|
|
795
|
-
idObj[indexField.name] = query[indexField.name];
|
|
796
|
-
}
|
|
797
|
-
if (allPresent) return idObj;
|
|
798
|
-
}
|
|
799
1124
|
return new _moostjs_event_http.HttpError(400, "Query params do not match any composite primary key or compound unique index");
|
|
800
1125
|
}
|
|
801
1126
|
/**
|
|
@@ -804,6 +1129,7 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
|
|
|
804
1129
|
async query(url) {
|
|
805
1130
|
const parsed = this.parseQueryString(url);
|
|
806
1131
|
const controls = parsed.controls;
|
|
1132
|
+
this._coerceActionsControl(controls);
|
|
807
1133
|
if (controls.$groupBy?.length) {
|
|
808
1134
|
if (controls.$with?.length) return new _moostjs_event_http.HttpError(400, "Cannot combine $with and $groupBy in the same query");
|
|
809
1135
|
if (parsed.insights) {
|
|
@@ -821,17 +1147,16 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
|
|
|
821
1147
|
if (error) return error;
|
|
822
1148
|
const gateError = this.checkGates(parsed.filter, controls, this._gates);
|
|
823
1149
|
if (gateError) return gateError;
|
|
824
|
-
const [filter,
|
|
1150
|
+
const [filter, rawSelect] = await Promise.all([this.transformFilter(parsed.filter), this.transformProjection(controls.$select)]);
|
|
825
1151
|
if (controls.$count) return this.readable.count({
|
|
826
1152
|
filter,
|
|
827
1153
|
controls: {
|
|
828
1154
|
...controls,
|
|
829
|
-
$select:
|
|
1155
|
+
$select: rawSelect
|
|
830
1156
|
}
|
|
831
1157
|
});
|
|
832
|
-
const
|
|
833
|
-
|
|
834
|
-
const vectorField = controls.$vector;
|
|
1158
|
+
const select = this.widenPreferredIdProjection(rawSelect);
|
|
1159
|
+
if (select instanceof _moostjs_event_http.HttpError) return select;
|
|
835
1160
|
const threshold = controls.$threshold ? Number(controls.$threshold) : void 0;
|
|
836
1161
|
const queryObj = {
|
|
837
1162
|
filter,
|
|
@@ -842,19 +1167,20 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
|
|
|
842
1167
|
$threshold: threshold
|
|
843
1168
|
}
|
|
844
1169
|
};
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
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;
|
|
852
1177
|
}
|
|
853
1178
|
/**
|
|
854
1179
|
* **GET /pages** — returns paginated records with metadata.
|
|
855
1180
|
*/
|
|
856
1181
|
async pages(url) {
|
|
857
1182
|
const parsed = this.parseQueryString(url);
|
|
1183
|
+
this._coerceActionsControl(parsed.controls);
|
|
858
1184
|
const error = this.validateParsed(parsed, "pages");
|
|
859
1185
|
if (error) return error;
|
|
860
1186
|
const controls = parsed.controls;
|
|
@@ -863,10 +1189,9 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
|
|
|
863
1189
|
const page = Math.max(Number(controls.$page || 1), 1);
|
|
864
1190
|
const size = Math.max(Number(controls.$size || 10), 1);
|
|
865
1191
|
const skip = (page - 1) * size;
|
|
866
|
-
const [filter,
|
|
867
|
-
const
|
|
868
|
-
|
|
869
|
-
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;
|
|
870
1195
|
const threshold = controls.$threshold ? Number(controls.$threshold) : void 0;
|
|
871
1196
|
const query = {
|
|
872
1197
|
filter,
|
|
@@ -878,13 +1203,13 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
|
|
|
878
1203
|
$threshold: threshold
|
|
879
1204
|
}
|
|
880
1205
|
};
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
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
|
+
});
|
|
888
1213
|
return {
|
|
889
1214
|
data: result.data,
|
|
890
1215
|
page,
|
|
@@ -898,15 +1223,14 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
|
|
|
898
1223
|
*/
|
|
899
1224
|
async getOne(id, url) {
|
|
900
1225
|
const parsed = this.parseQueryString(url);
|
|
1226
|
+
this._coerceActionsControl(parsed.controls);
|
|
901
1227
|
if (Object.keys(parsed.filter).length > 0) return new _moostjs_event_http.HttpError(400, "Filtering is not allowed for \"one\" endpoint");
|
|
902
1228
|
const error = this.validateParsed(parsed, "getOne");
|
|
903
1229
|
if (error) return error;
|
|
904
|
-
const
|
|
905
|
-
const
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
};
|
|
909
|
-
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);
|
|
910
1234
|
}
|
|
911
1235
|
/**
|
|
912
1236
|
* **GET /one?field1=val1&field2=val2** — retrieves a single record by composite key
|
|
@@ -916,12 +1240,29 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
|
|
|
916
1240
|
const idObj = this.extractCompositeId(query);
|
|
917
1241
|
if (idObj instanceof _moostjs_event_http.HttpError) return idObj;
|
|
918
1242
|
const parsed = this.parseQueryString(url);
|
|
919
|
-
|
|
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;
|
|
920
1252
|
const controls = {
|
|
921
|
-
...
|
|
922
|
-
$select:
|
|
1253
|
+
...parsedControls,
|
|
1254
|
+
$select: initialSelect
|
|
923
1255
|
};
|
|
924
|
-
|
|
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;
|
|
925
1266
|
}
|
|
926
1267
|
/**
|
|
927
1268
|
* **GET /meta** — returns table/view metadata for UI.
|
|
@@ -955,6 +1296,7 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
|
|
|
955
1296
|
vectorSearchable: this.readable.isVectorSearchable(),
|
|
956
1297
|
searchIndexes: this.readable.getSearchIndexes(),
|
|
957
1298
|
primaryKeys: [...this.readable.primaryKeys],
|
|
1299
|
+
preferredId: [...this.readable.preferredId],
|
|
958
1300
|
relations,
|
|
959
1301
|
fields,
|
|
960
1302
|
type: this.getSerializedType(),
|
|
@@ -1006,6 +1348,7 @@ AsDbReadableController = __decorate([
|
|
|
1006
1348
|
__decorateParam(0, (0, moost.Inject)(READABLE_DEF)),
|
|
1007
1349
|
__decorateMetadata("design:paramtypes", [Object, typeof (_ref$3 = typeof moost.Moost !== "undefined" && moost.Moost) === "function" ? _ref$3 : Object])
|
|
1008
1350
|
], AsDbReadableController);
|
|
1351
|
+
registerAsDbReadableController(AsDbReadableController);
|
|
1009
1352
|
//#endregion
|
|
1010
1353
|
//#region src/as-db.controller.ts
|
|
1011
1354
|
var _ref$2, _ref2$1;
|
|
@@ -1252,6 +1595,7 @@ let AsValueHelpController = class AsValueHelpController extends AsReadableContro
|
|
|
1252
1595
|
vectorSearchable: false,
|
|
1253
1596
|
searchIndexes: [],
|
|
1254
1597
|
primaryKeys: this.primaryKey ? [this.primaryKey] : [],
|
|
1598
|
+
preferredId: this.primaryKey ? [this.primaryKey] : [],
|
|
1255
1599
|
relations: [],
|
|
1256
1600
|
fields,
|
|
1257
1601
|
type: this.getSerializedType(),
|
|
@@ -1304,6 +1648,7 @@ AsValueHelpController = __decorate([(0, moost.Inherit)(), __decorateMetadata("de
|
|
|
1304
1648
|
String,
|
|
1305
1649
|
typeof (_ref$1 = typeof moost.Moost !== "undefined" && moost.Moost) === "function" ? _ref$1 : Object
|
|
1306
1650
|
])], AsValueHelpController);
|
|
1651
|
+
registerAsValueHelpController(AsValueHelpController);
|
|
1307
1652
|
//#endregion
|
|
1308
1653
|
//#region src/as-json-value-help.controller.ts
|
|
1309
1654
|
var _ref;
|
|
@@ -1464,151 +1809,120 @@ function applySelect(rows, select) {
|
|
|
1464
1809
|
});
|
|
1465
1810
|
}
|
|
1466
1811
|
//#endregion
|
|
1467
|
-
//#region src/actions/
|
|
1812
|
+
//#region src/actions/action-disabled-error.ts
|
|
1813
|
+
function buildMessage(action, ids) {
|
|
1814
|
+
if (ids !== void 0) return `Action "${action}" is disabled for ${ids.length} of the selected rows`;
|
|
1815
|
+
return `Action "${action}" is disabled for this row`;
|
|
1816
|
+
}
|
|
1468
1817
|
/**
|
|
1469
|
-
*
|
|
1818
|
+
* Thrown by the gate interceptor when `disabled` returns truthy. Composes
|
|
1819
|
+
* with Moost's existing error mapper to produce HTTP 409 with the wire body
|
|
1820
|
+
* defined by {@link ActionDisabledErrorBody}.
|
|
1470
1821
|
*
|
|
1471
|
-
*
|
|
1472
|
-
*
|
|
1473
|
-
*
|
|
1474
|
-
*
|
|
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
|
-
* ```
|
|
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).
|
|
1482
1826
|
*/
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
})
|
|
1827
|
+
var ActionDisabledError = class extends _moostjs_event_http.HttpError {
|
|
1828
|
+
name = "ActionDisabledError";
|
|
1829
|
+
constructor(action, id, ids) {
|
|
1830
|
+
const body = {
|
|
1831
|
+
name: "ActionDisabledError",
|
|
1832
|
+
message: buildMessage(action, ids),
|
|
1833
|
+
statusCode: 409,
|
|
1834
|
+
action
|
|
1492
1835
|
};
|
|
1493
|
-
|
|
1494
|
-
|
|
1836
|
+
if (ids !== void 0) body.ids = ids;
|
|
1837
|
+
else if (id !== void 0) body.id = id;
|
|
1838
|
+
super(409, body);
|
|
1839
|
+
}
|
|
1840
|
+
};
|
|
1495
1841
|
//#endregion
|
|
1496
|
-
//#region src/actions/
|
|
1497
|
-
/**
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
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];
|
|
1509
1856
|
}
|
|
1510
1857
|
//#endregion
|
|
1511
|
-
//#region src/actions/
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
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("");
|
|
1529
1878
|
}
|
|
1530
|
-
function
|
|
1879
|
+
function isIdValidationSource(value) {
|
|
1531
1880
|
if (!value || typeof value !== "object") return false;
|
|
1532
1881
|
const v = value;
|
|
1533
|
-
return
|
|
1534
|
-
}
|
|
1535
|
-
/**
|
|
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));
|
|
1882
|
+
return typeof v.getIdentifications === "function" && Array.isArray(v.fieldDescriptors);
|
|
1546
1883
|
}
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
/**
|
|
1550
|
-
* Validate a JSON-decoded body against a single-row PK shape (scalar or
|
|
1551
|
-
* composite). Throws {@link ValidatorError} with structured `errors` so the
|
|
1552
|
-
* existing validation interceptor returns HTTP 400.
|
|
1553
|
-
*/
|
|
1554
|
-
function validateSinglePk(body, source, path = "") {
|
|
1555
|
-
const errors = collectPkErrors(body, source, path);
|
|
1884
|
+
function validateSingleId(body, source, path = "") {
|
|
1885
|
+
const errors = collectIdErrors(body, source, path);
|
|
1556
1886
|
if (errors.length > 0) throw new _atscript_typescript_utils.ValidatorError(errors);
|
|
1887
|
+
return body;
|
|
1557
1888
|
}
|
|
1558
|
-
|
|
1559
|
-
* Validate a JSON-decoded body against an array of PK shapes (`@DbActionPKs`).
|
|
1560
|
-
* The body MUST be an array; each element is validated against the PK schema.
|
|
1561
|
-
*/
|
|
1562
|
-
function validateMultiPk(body, source) {
|
|
1889
|
+
function validateMultiId(body, source) {
|
|
1563
1890
|
if (!Array.isArray(body)) throw new _atscript_typescript_utils.ValidatorError([{
|
|
1564
1891
|
path: "",
|
|
1565
|
-
message: "Expected JSON array of
|
|
1892
|
+
message: "Expected JSON array of identifier objects",
|
|
1566
1893
|
details: []
|
|
1567
1894
|
}]);
|
|
1568
1895
|
const errors = [];
|
|
1569
|
-
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}]`));
|
|
1570
1897
|
if (errors.length > 0) throw new _atscript_typescript_utils.ValidatorError(errors);
|
|
1898
|
+
return body;
|
|
1571
1899
|
}
|
|
1572
|
-
function
|
|
1573
|
-
|
|
1574
|
-
|
|
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 [{
|
|
1575
1908
|
path: pathPrefix,
|
|
1576
|
-
message: "Table has no
|
|
1909
|
+
message: "Table has no identifier configured",
|
|
1910
|
+
details: []
|
|
1911
|
+
}];
|
|
1912
|
+
const match = cache.byKeySig.get(fieldsSig(Object.keys(value)));
|
|
1913
|
+
if (!match) return [{
|
|
1914
|
+
path: pathPrefix,
|
|
1915
|
+
message: `Identifier fields must exactly match one of: ${cache.formatted}`,
|
|
1577
1916
|
details: []
|
|
1578
1917
|
}];
|
|
1579
1918
|
const errors = [];
|
|
1580
|
-
|
|
1581
|
-
const err = checkScalar(value, findFieldDescriptor(source, pkFields[0]), pathPrefix);
|
|
1582
|
-
if (err) errors.push(err);
|
|
1583
|
-
return errors;
|
|
1584
|
-
}
|
|
1585
|
-
if (!isPlainObject(value)) {
|
|
1586
|
-
errors.push({
|
|
1587
|
-
path: pathPrefix,
|
|
1588
|
-
message: "Expected JSON object for composite primary key",
|
|
1589
|
-
details: []
|
|
1590
|
-
});
|
|
1591
|
-
return errors;
|
|
1592
|
-
}
|
|
1593
|
-
for (const fieldName of pkFields) {
|
|
1919
|
+
for (const fieldName of match.fields) {
|
|
1594
1920
|
const sub = pathPrefix ? `${pathPrefix}.${fieldName}` : fieldName;
|
|
1595
|
-
|
|
1596
|
-
errors.push({
|
|
1597
|
-
path: sub,
|
|
1598
|
-
message: `Missing primary-key field "${fieldName}"`,
|
|
1599
|
-
details: []
|
|
1600
|
-
});
|
|
1601
|
-
continue;
|
|
1602
|
-
}
|
|
1603
|
-
const fd = findFieldDescriptor(source, fieldName);
|
|
1604
|
-
const err = checkScalar(value[fieldName], fd, sub);
|
|
1921
|
+
const err = checkScalar(value[fieldName], cache.fieldByName.get(fieldName), sub);
|
|
1605
1922
|
if (err) errors.push(err);
|
|
1606
1923
|
}
|
|
1607
1924
|
return errors;
|
|
1608
1925
|
}
|
|
1609
|
-
function findFieldDescriptor(source, name) {
|
|
1610
|
-
for (const fd of source.fieldDescriptors) if (fd.path === name) return fd;
|
|
1611
|
-
}
|
|
1612
1926
|
function checkScalar(value, fd, path) {
|
|
1613
1927
|
const expected = fd?.designType ?? "string";
|
|
1614
1928
|
if (expected === "string" && typeof value !== "string") return scalarMismatch(path, expected, value);
|
|
@@ -1618,7 +1932,7 @@ function checkScalar(value, fd, path) {
|
|
|
1618
1932
|
function scalarMismatch(path, expected, value) {
|
|
1619
1933
|
return {
|
|
1620
1934
|
path,
|
|
1621
|
-
message: `Expected
|
|
1935
|
+
message: `Expected identifier value to be ${expected}, got ${describe(value)}`,
|
|
1622
1936
|
details: []
|
|
1623
1937
|
};
|
|
1624
1938
|
}
|
|
@@ -1631,38 +1945,333 @@ function isPlainObject(value) {
|
|
|
1631
1945
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1632
1946
|
}
|
|
1633
1947
|
//#endregion
|
|
1634
|
-
//#region src/actions/
|
|
1948
|
+
//#region src/actions/id-cache.ts
|
|
1949
|
+
const boundTableKey = (0, _wooksjs_event_core.key)("atscript_db_action_bound_table");
|
|
1950
|
+
function getActionTable(ctx) {
|
|
1951
|
+
const fromSlot = ctx.has(boundTableKey) ? ctx.get(boundTableKey) : void 0;
|
|
1952
|
+
if (fromSlot) return fromSlot;
|
|
1953
|
+
const ctrl = (0, moost.useControllerContext)(ctx).getController();
|
|
1954
|
+
return ctrl?.readable ?? ctrl?.table ?? null;
|
|
1955
|
+
}
|
|
1956
|
+
function noTableError(ctx) {
|
|
1957
|
+
const actionName = readCurrentActionMeta(ctx)?.name;
|
|
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.`);
|
|
1959
|
+
}
|
|
1960
|
+
async function resolveValidatedId(ctx, validate) {
|
|
1961
|
+
const table = getActionTable(ctx);
|
|
1962
|
+
if (!isIdValidationSource(table)) throw noTableError(ctx);
|
|
1963
|
+
const body = await (0, _wooksjs_http_body.useBody)(ctx).parseBody();
|
|
1964
|
+
validate(body, table);
|
|
1965
|
+
return body;
|
|
1966
|
+
}
|
|
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);
|
|
1970
|
+
});
|
|
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) }));
|
|
1973
|
+
//#endregion
|
|
1974
|
+
//#region src/actions/row-cache.ts
|
|
1975
|
+
function asFetchTable(value) {
|
|
1976
|
+
if (!value || typeof value !== "object") return null;
|
|
1977
|
+
const v = value;
|
|
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;
|
|
1992
|
+
}
|
|
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;
|
|
1999
|
+
}
|
|
2000
|
+
async function loadRow(ctx) {
|
|
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");
|
|
2011
|
+
return row;
|
|
2012
|
+
}
|
|
2013
|
+
async function loadRows(ctx) {
|
|
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);
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
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]));
|
|
2057
|
+
}
|
|
2058
|
+
const dbActionRowSlot = (0, _wooksjs_event_core.cached)((ctx) => loadRow(ctx));
|
|
2059
|
+
const dbActionRowsSlot = (0, _wooksjs_event_core.cached)((ctx) => loadRows(ctx));
|
|
2060
|
+
const useDbActionRow = (0, _wooksjs_event_core.defineWook)((ctx) => ({ load: () => ctx.get(dbActionRowSlot) }));
|
|
2061
|
+
const useDbActionRows = (0, _wooksjs_event_core.defineWook)((ctx) => ({ load: () => ctx.get(dbActionRowsSlot) }));
|
|
2062
|
+
//#endregion
|
|
2063
|
+
//#region src/actions/gate-interceptor.ts
|
|
2064
|
+
const GATE_PRIORITY = moost.TInterceptorPriority.AFTER_GUARD;
|
|
2065
|
+
function injectBoundTable(table) {
|
|
2066
|
+
const ctx = (0, _wooksjs_event_core.current)();
|
|
2067
|
+
if (ctx.has(boundTableKey)) return;
|
|
2068
|
+
const controller = (0, moost.useControllerContext)(ctx).getController();
|
|
2069
|
+
if (isAsDbReadableControllerInstance(controller)) {
|
|
2070
|
+
ctx.set(boundTableKey, controller.readable);
|
|
2071
|
+
return;
|
|
2072
|
+
}
|
|
2073
|
+
if (table != null) ctx.set(boundTableKey, table);
|
|
2074
|
+
}
|
|
2075
|
+
function buildGateInterceptor(opts) {
|
|
2076
|
+
const { action, level, disabled, onDisabledRows, table } = opts;
|
|
2077
|
+
return (0, moost.defineBeforeInterceptor)(async () => {
|
|
2078
|
+
injectBoundTable(table);
|
|
2079
|
+
const ctx = (0, _wooksjs_event_core.current)();
|
|
2080
|
+
if (level === "row") {
|
|
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));
|
|
2084
|
+
return;
|
|
2085
|
+
}
|
|
2086
|
+
const ids = await ctx.get(dbActionIdsSlot);
|
|
2087
|
+
const rows = await ctx.get(dbActionRowsSlot);
|
|
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 = [];
|
|
2093
|
+
const passingRows = [];
|
|
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
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
if (onDisabledRows === "skip") {
|
|
2105
|
+
if (passingRows.length === 0) throw new ActionDisabledError(action, void 0, [...ids]);
|
|
2106
|
+
if (failingIds.length > 0) {
|
|
2107
|
+
ctx.set(dbActionRowsSlot, Promise.resolve(passingRows));
|
|
2108
|
+
ctx.set(dbActionIdsSlot, Promise.resolve(passingIds));
|
|
2109
|
+
}
|
|
2110
|
+
return;
|
|
2111
|
+
}
|
|
2112
|
+
if (failingIds.length > 0) throw new ActionDisabledError(action, void 0, failingIds);
|
|
2113
|
+
}, GATE_PRIORITY);
|
|
2114
|
+
}
|
|
2115
|
+
/** Thin interceptor for `@DbActionRow*` without `disabled` — injects only the bound table. */
|
|
2116
|
+
function buildThinInterceptor(opts) {
|
|
2117
|
+
const { table } = opts;
|
|
2118
|
+
return (0, moost.defineBeforeInterceptor)(() => {
|
|
2119
|
+
injectBoundTable(table);
|
|
2120
|
+
}, GATE_PRIORITY);
|
|
2121
|
+
}
|
|
2122
|
+
//#endregion
|
|
2123
|
+
//#region src/actions/db-action.decorator.ts
|
|
2124
|
+
/**
|
|
2125
|
+
* Mark a controller method as a database action surfaced via `/meta`. Writes
|
|
2126
|
+
* `MOOST_DB_ACTION` metadata and registers a Moost interceptor when needed
|
|
2127
|
+
* (gate when `disabled` is set, thin bound-table injector when only
|
|
2128
|
+
* `@DbActionRow*` is present). Stacking two `@DbAction` on the same method
|
|
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]>[]`.
|
|
2135
|
+
*/
|
|
2136
|
+
function DbAction(name, opts = {}) {
|
|
2137
|
+
const mate = (0, moost.getMoostMate)();
|
|
2138
|
+
return ((target, propertyKey, descriptor) => {
|
|
2139
|
+
const priorName = mate.read(target, propertyKey)?.[MOOST_DB_ACTION]?.name;
|
|
2140
|
+
if (priorName) console.warn(`${WARN_PREFIX} stacking @DbAction on the same method is undefined; declare one per method. Detected: "${priorName}" and "${name}".`);
|
|
2141
|
+
mate.decorate((current) => {
|
|
2142
|
+
const meta = current;
|
|
2143
|
+
return {
|
|
2144
|
+
...current,
|
|
2145
|
+
[MOOST_DB_ACTION]: mergeActionMeta(meta, {
|
|
2146
|
+
name,
|
|
2147
|
+
opts
|
|
2148
|
+
})
|
|
2149
|
+
};
|
|
2150
|
+
})(target, propertyKey, descriptor);
|
|
2151
|
+
if (isAsValueHelpControllerSubclass(typeof target === "function" ? target : target.constructor)) return descriptor;
|
|
2152
|
+
const scan = scanParamLevel(mate.read(target, propertyKey)?.params ?? []);
|
|
2153
|
+
const rawOpts = opts;
|
|
2154
|
+
if (typeof rawOpts.disabled === "function" && (scan.level === "row" || scan.level === "rows")) (0, moost.Intercept)(buildGateInterceptor({
|
|
2155
|
+
action: name,
|
|
2156
|
+
level: scan.level,
|
|
2157
|
+
disabled: rawOpts.disabled,
|
|
2158
|
+
onDisabledRows: rawOpts.onDisabledRows ?? "reject",
|
|
2159
|
+
table: rawOpts.table
|
|
2160
|
+
}))(target, propertyKey, descriptor);
|
|
2161
|
+
else if (scan.hasRowParam) (0, moost.Intercept)(buildThinInterceptor({ table: rawOpts.table }))(target, propertyKey, descriptor);
|
|
2162
|
+
return descriptor;
|
|
2163
|
+
});
|
|
2164
|
+
}
|
|
2165
|
+
//#endregion
|
|
2166
|
+
//#region src/actions/db-action-default.decorator.ts
|
|
2167
|
+
/**
|
|
2168
|
+
* Sugar that flips `default: true` on the same method's `@DbAction` metadata.
|
|
2169
|
+
* Equivalent to passing `opts.default = true`. Decorator order does not matter.
|
|
2170
|
+
*/
|
|
2171
|
+
function DbActionDefault() {
|
|
2172
|
+
return (0, moost.getMoostMate)().decorate((current) => {
|
|
2173
|
+
const meta = current;
|
|
2174
|
+
return {
|
|
2175
|
+
...current,
|
|
2176
|
+
[MOOST_DB_ACTION]: mergeActionMeta(meta, { opts: { default: true } })
|
|
2177
|
+
};
|
|
2178
|
+
});
|
|
2179
|
+
}
|
|
2180
|
+
//#endregion
|
|
2181
|
+
//#region src/actions/id-source.ts
|
|
1635
2182
|
/**
|
|
1636
|
-
*
|
|
1637
|
-
*
|
|
2183
|
+
* Build a parameter decorator that reads its value from the cached ID wook
|
|
2184
|
+
* (single or multi). Validation runs inside the wook factory exactly once
|
|
2185
|
+
* per request, regardless of how many readers consume the value (`@DbActionID*`
|
|
2186
|
+
* resolver, gate interceptor, cached row wook, in-handler composables).
|
|
2187
|
+
*
|
|
2188
|
+
* Marks the param so {@link discoverActions} can infer the action's `level`.
|
|
2189
|
+
*/
|
|
2190
|
+
function createIdParamDecorator(kind) {
|
|
2191
|
+
const mate = (0, moost.getMoostMate)();
|
|
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));
|
|
2195
|
+
}
|
|
2196
|
+
//#endregion
|
|
2197
|
+
//#region src/actions/db-action-id.decorator.ts
|
|
2198
|
+
/**
|
|
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:
|
|
1638
2204
|
*
|
|
1639
|
-
* - Single-field PK →
|
|
1640
|
-
* - 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: "..." }`.
|
|
1641
2209
|
*
|
|
1642
|
-
*
|
|
2210
|
+
* Strict — unknown fields are rejected, no type coercion. Mismatches throw a
|
|
1643
2211
|
* `ValidatorError` which the existing validation interceptor surfaces as
|
|
1644
2212
|
* HTTP 400 with the same envelope as DTO failures.
|
|
1645
2213
|
*
|
|
1646
2214
|
* Marks the param so {@link discoverActions} can infer the action's `level`
|
|
1647
2215
|
* as `'row'`.
|
|
2216
|
+
*
|
|
2217
|
+
* Implementation note: the resolver is a thin reader of the cached ID wook
|
|
2218
|
+
* — validation logic lives in the wook factory, which runs once per request
|
|
2219
|
+
* regardless of how many readers consume the value.
|
|
1648
2220
|
*/
|
|
1649
|
-
function
|
|
1650
|
-
return
|
|
2221
|
+
function DbActionID() {
|
|
2222
|
+
return createIdParamDecorator("id");
|
|
1651
2223
|
}
|
|
1652
2224
|
//#endregion
|
|
1653
|
-
//#region src/actions/db-action-
|
|
2225
|
+
//#region src/actions/db-action-ids.decorator.ts
|
|
1654
2226
|
/**
|
|
1655
|
-
* Parameter resolver that reads a JSON array of
|
|
1656
|
-
* 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.
|
|
2229
|
+
*
|
|
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.
|
|
1657
2235
|
*
|
|
1658
|
-
*
|
|
1659
|
-
*
|
|
2236
|
+
* Strict — unknown fields are rejected, no type coercion. Marks the param
|
|
2237
|
+
* so {@link discoverActions} can infer the action's `level` as `'rows'`.
|
|
1660
2238
|
*
|
|
1661
|
-
*
|
|
1662
|
-
*
|
|
2239
|
+
* In `'rows'` skip mode the resolved value reflects the gate interceptor's
|
|
2240
|
+
* filtered subset (the cached ID slot is overwritten in place); see
|
|
2241
|
+
* {@link dbActionIdsSlot} for precedence details.
|
|
1663
2242
|
*/
|
|
1664
|
-
function
|
|
1665
|
-
return
|
|
2243
|
+
function DbActionIDs() {
|
|
2244
|
+
return createIdParamDecorator("ids");
|
|
2245
|
+
}
|
|
2246
|
+
//#endregion
|
|
2247
|
+
//#region src/actions/db-action-row.decorator.ts
|
|
2248
|
+
function createRowParamDecorator(metaKey, slot, resolverName) {
|
|
2249
|
+
return (0, moost.ApplyDecorators)((0, moost.getMoostMate)().decorate(metaKey, true), (0, moost.Resolve)(async () => (0, _wooksjs_event_core.current)().get(slot), resolverName));
|
|
2250
|
+
}
|
|
2251
|
+
/**
|
|
2252
|
+
* Parameter decorator that injects the row whose identifier was supplied in
|
|
2253
|
+
* the request body.
|
|
2254
|
+
*
|
|
2255
|
+
* Marks the param so {@link discoverActions} infers the action's `level` as
|
|
2256
|
+
* `'row'`. Co-occurrence with `@DbActionRows()` (or any multi-cardinality
|
|
2257
|
+
* decorator) drops the action with a warning.
|
|
2258
|
+
*
|
|
2259
|
+
* In `'skip'` mode this returns the gate's filtered row; the original
|
|
2260
|
+
* request-body row is not retrievable.
|
|
2261
|
+
*/
|
|
2262
|
+
function DbActionRow() {
|
|
2263
|
+
return createRowParamDecorator(MOOST_DB_ACTION_ROW, dbActionRowSlot, "dbActionRow");
|
|
2264
|
+
}
|
|
2265
|
+
/**
|
|
2266
|
+
* Parameter decorator that injects the rows fetched by the identifiers
|
|
2267
|
+
* supplied in the request body.
|
|
2268
|
+
*
|
|
2269
|
+
* Marks the param so {@link discoverActions} infers the action's `level` as
|
|
2270
|
+
* `'rows'`. In `'rows'` + `'skip'` mode the resolved value contains only the
|
|
2271
|
+
* gate's surviving rows.
|
|
2272
|
+
*/
|
|
2273
|
+
function DbActionRows() {
|
|
2274
|
+
return createRowParamDecorator(MOOST_DB_ACTION_ROWS, dbActionRowsSlot, "dbActionRows");
|
|
1666
2275
|
}
|
|
1667
2276
|
//#endregion
|
|
1668
2277
|
//#region src/actions/db-actions.decorator.ts
|
|
@@ -1673,10 +2282,9 @@ function DbActionPKs() {
|
|
|
1673
2282
|
* the level-pinned shortcuts (`@DbTableActions`, `@DbRowActions`,
|
|
1674
2283
|
* `@DbRowsActions`) to avoid repeating `level`.
|
|
1675
2284
|
*
|
|
1676
|
-
*
|
|
1677
|
-
*
|
|
1678
|
-
* `
|
|
1679
|
-
* `@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.
|
|
1680
2288
|
*
|
|
1681
2289
|
* Multiple `@DbActions` (and shortcut) decorators on the same class
|
|
1682
2290
|
* accumulate.
|
|
@@ -1698,11 +2306,16 @@ function DbRowsActions(dict) {
|
|
|
1698
2306
|
}
|
|
1699
2307
|
function classLevelActions(dict, forcedLevel) {
|
|
1700
2308
|
const entries = [];
|
|
1701
|
-
for (const [name, entry] of Object.entries(dict))
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
2309
|
+
for (const [name, entry] of Object.entries(dict)) {
|
|
2310
|
+
const merged = forcedLevel ? {
|
|
2311
|
+
...entry,
|
|
2312
|
+
level: forcedLevel
|
|
2313
|
+
} : entry;
|
|
2314
|
+
entries.push({
|
|
2315
|
+
name,
|
|
2316
|
+
entry: merged
|
|
2317
|
+
});
|
|
2318
|
+
}
|
|
1706
2319
|
return (0, moost.getMoostMate)().decorate((current) => {
|
|
1707
2320
|
const existing = current["atscript_db_actions"] ?? [];
|
|
1708
2321
|
return {
|
|
@@ -1712,6 +2325,22 @@ function classLevelActions(dict, forcedLevel) {
|
|
|
1712
2325
|
});
|
|
1713
2326
|
}
|
|
1714
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
|
|
2343
|
+
exports.ActionDisabledError = ActionDisabledError;
|
|
1715
2344
|
Object.defineProperty(exports, "AsDbController", {
|
|
1716
2345
|
enumerable: true,
|
|
1717
2346
|
get: function() {
|
|
@@ -1744,8 +2373,10 @@ Object.defineProperty(exports, "AsValueHelpController", {
|
|
|
1744
2373
|
});
|
|
1745
2374
|
exports.DbAction = DbAction;
|
|
1746
2375
|
exports.DbActionDefault = DbActionDefault;
|
|
1747
|
-
exports.
|
|
1748
|
-
exports.
|
|
2376
|
+
exports.DbActionID = DbActionID;
|
|
2377
|
+
exports.DbActionIDs = DbActionIDs;
|
|
2378
|
+
exports.DbActionRow = DbActionRow;
|
|
2379
|
+
exports.DbActionRows = DbActionRows;
|
|
1749
2380
|
exports.DbActions = DbActions;
|
|
1750
2381
|
exports.DbRowActions = DbRowActions;
|
|
1751
2382
|
exports.DbRowsActions = DbRowsActions;
|
|
@@ -1760,4 +2391,9 @@ exports.TableController = TableController;
|
|
|
1760
2391
|
exports.UseValidationErrorTransform = UseValidationErrorTransform;
|
|
1761
2392
|
exports.ViewController = ViewController;
|
|
1762
2393
|
exports.discoverActions = discoverActions;
|
|
2394
|
+
exports.perRow = perRow;
|
|
2395
|
+
exports.useDbActionId = useDbActionId;
|
|
2396
|
+
exports.useDbActionIds = useDbActionIds;
|
|
2397
|
+
exports.useDbActionRow = useDbActionRow;
|
|
2398
|
+
exports.useDbActionRows = useDbActionRows;
|
|
1763
2399
|
exports.validationErrorTransform = validationErrorTransform;
|