@atscript/moost-db 0.1.61 → 0.1.63
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 +202 -15
- package/dist/index.d.cts +150 -4
- package/dist/index.d.mts +150 -4
- package/dist/index.mjs +196 -16
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -213,6 +213,21 @@ const MOOST_DB_ACTION_PARAM = "atscript_db_action_param";
|
|
|
213
213
|
const MOOST_DB_ACTION_ROW = "atscript_db_action_row";
|
|
214
214
|
const MOOST_DB_ACTION_ROWS = "atscript_db_action_rows";
|
|
215
215
|
/**
|
|
216
|
+
* Param-level metadata key — written by `@InputForm(FormType)`. Carries the
|
|
217
|
+
* compiled `.as` class plus its `.name` so {@link discoverActions} can both
|
|
218
|
+
* emit `inputForm` on `/meta` and register the type in the controller's form
|
|
219
|
+
* registry for `GET /meta/form/:name`.
|
|
220
|
+
*/
|
|
221
|
+
const MOOST_DB_ACTION_INPUT_FORM = "atscript_db_action_input_form";
|
|
222
|
+
/**
|
|
223
|
+
* Generic param-level metadata key — written by `@InputForm(FormType)`
|
|
224
|
+
* alongside {@link MOOST_DB_ACTION_INPUT_FORM}. Holds just the type ref so a
|
|
225
|
+
* generic atscript-aware Moost pipe (installed globally via
|
|
226
|
+
* `app.applyGlobalPipes(...)` or scoped via `@Pipe(...)`) can validate the
|
|
227
|
+
* resolved value without knowing about the moost-db-specific key.
|
|
228
|
+
*/
|
|
229
|
+
const MOOST_ATSCRIPT_TYPE = "atscript_type";
|
|
230
|
+
/**
|
|
216
231
|
* Shared method-decorator update used by `@DbAction` and `@DbActionDefault`:
|
|
217
232
|
* read the existing `MOOST_DB_ACTION` slot, merge the patch (later-applied
|
|
218
233
|
* fields win), and write it back. `name` is empty until `@DbAction` provides
|
|
@@ -235,6 +250,8 @@ function scanParamLevel(params) {
|
|
|
235
250
|
let multi = false;
|
|
236
251
|
let hasRowParam = false;
|
|
237
252
|
let hasBody = false;
|
|
253
|
+
let inputForm;
|
|
254
|
+
let hasDuplicateInputForm = false;
|
|
238
255
|
for (const p of params) {
|
|
239
256
|
const kind = p[MOOST_DB_ACTION_PARAM];
|
|
240
257
|
if (kind === "id") single = true;
|
|
@@ -248,13 +265,18 @@ function scanParamLevel(params) {
|
|
|
248
265
|
hasRowParam = true;
|
|
249
266
|
}
|
|
250
267
|
if (p.paramSource === "BODY") hasBody = true;
|
|
268
|
+
const form = p[MOOST_DB_ACTION_INPUT_FORM];
|
|
269
|
+
if (form) if (inputForm) hasDuplicateInputForm = true;
|
|
270
|
+
else inputForm = form;
|
|
251
271
|
}
|
|
252
272
|
return {
|
|
253
273
|
level: single && multi ? "table" : single ? "row" : multi ? "rows" : "table",
|
|
254
274
|
single,
|
|
255
275
|
multi,
|
|
256
276
|
hasRowParam,
|
|
257
|
-
hasBody
|
|
277
|
+
hasBody,
|
|
278
|
+
inputForm,
|
|
279
|
+
hasDuplicateInputForm
|
|
258
280
|
};
|
|
259
281
|
}
|
|
260
282
|
//#endregion
|
|
@@ -271,6 +293,34 @@ const OPTIONAL_FIELDS = [
|
|
|
271
293
|
];
|
|
272
294
|
const actionsCache = /* @__PURE__ */ new WeakMap();
|
|
273
295
|
const rowLevelActionsCache = /* @__PURE__ */ new WeakMap();
|
|
296
|
+
/**
|
|
297
|
+
* Per-controller registry of form names → compiled `.as` classes, populated
|
|
298
|
+
* during {@link discoverActions} when a method param carries
|
|
299
|
+
* {@link MOOST_DB_ACTION_INPUT_FORM}. Backs `GET /meta/form/:name`.
|
|
300
|
+
*
|
|
301
|
+
* Same name + same type ref across multiple actions is fine (forms can be
|
|
302
|
+
* reused). Same name + *different* type refs is an ambiguity — discovery
|
|
303
|
+
* warns and drops the second action.
|
|
304
|
+
*/
|
|
305
|
+
const formRegistry = /* @__PURE__ */ new WeakMap();
|
|
306
|
+
/** Lookup helper for `AsReadableController.metaForm()`. */
|
|
307
|
+
function getControllerFormType(ctor, name) {
|
|
308
|
+
return formRegistry.get(ctor)?.get(name);
|
|
309
|
+
}
|
|
310
|
+
function registerFormType(ctor, meta, actionName, logger) {
|
|
311
|
+
let map = formRegistry.get(ctor);
|
|
312
|
+
if (!map) {
|
|
313
|
+
map = /* @__PURE__ */ new Map();
|
|
314
|
+
formRegistry.set(ctor, map);
|
|
315
|
+
}
|
|
316
|
+
const existing = map.get(meta.name);
|
|
317
|
+
if (existing && existing !== meta.type) {
|
|
318
|
+
logger.warn(`${WARN_PREFIX} action "${actionName}" — form name "${meta.name}" already registered on this controller with a different type. Reusing the same FormType across actions is fine; clashing names are not — dropping`);
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
if (!existing) map.set(meta.name, meta.type);
|
|
322
|
+
return true;
|
|
323
|
+
}
|
|
274
324
|
/** Discover actions on a controller, memoized per ctor. `info`-only callers map `e => e.info`. */
|
|
275
325
|
function discoverActions(controllerCtor, app, logger) {
|
|
276
326
|
const cached = actionsCache.get(controllerCtor);
|
|
@@ -356,6 +406,10 @@ function collectMethodActions(ctor, overview, logger, out, seen) {
|
|
|
356
406
|
processor: "backend",
|
|
357
407
|
value: path
|
|
358
408
|
};
|
|
409
|
+
if (levelInfer.inputForm) {
|
|
410
|
+
if (!registerFormType(ctor, levelInfer.inputForm, action.name, logger)) continue;
|
|
411
|
+
info.inputForm = levelInfer.inputForm.name;
|
|
412
|
+
}
|
|
359
413
|
emitInfo(info, action.opts);
|
|
360
414
|
seen.add(action.name);
|
|
361
415
|
out.push({
|
|
@@ -370,10 +424,12 @@ function inferMethodLevel(params, actionName, logger) {
|
|
|
370
424
|
logger.warn(`${WARN_PREFIX} action "${actionName}" mixes single-cardinality and multi-cardinality decorators (@DbActionID / @DbActionRow vs @DbActionIDs / @DbActionRows) — dropping`);
|
|
371
425
|
return null;
|
|
372
426
|
}
|
|
427
|
+
if (scan.hasDuplicateInputForm) logger.warn(`${WARN_PREFIX} action "${actionName}" has more than one @InputForm() param — only the first is honored. Compose multiple inputs into a single form interface.`);
|
|
373
428
|
return {
|
|
374
429
|
level: scan.level,
|
|
375
430
|
bodyConflict: scan.hasBody && scan.level !== "table",
|
|
376
|
-
hasRowParam: scan.hasRowParam
|
|
431
|
+
hasRowParam: scan.hasRowParam,
|
|
432
|
+
inputForm: scan.inputForm
|
|
377
433
|
};
|
|
378
434
|
}
|
|
379
435
|
function collectClassActions(ctor, logger, out, seen) {
|
|
@@ -484,6 +540,13 @@ function __decorate(decorators, target, key, desc) {
|
|
|
484
540
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
485
541
|
}
|
|
486
542
|
//#endregion
|
|
543
|
+
//#region \0@oxc-project+runtime@0.120.0/helpers/decorateParam.js
|
|
544
|
+
function __decorateParam(paramIndex, decorator) {
|
|
545
|
+
return function(target, key) {
|
|
546
|
+
decorator(target, key, paramIndex);
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
//#endregion
|
|
487
550
|
//#region src/as-readable.controller.ts
|
|
488
551
|
var _ref$4;
|
|
489
552
|
let AsReadableController = class AsReadableController {
|
|
@@ -499,6 +562,8 @@ let AsReadableController = class AsReadableController {
|
|
|
499
562
|
_serializedType;
|
|
500
563
|
/** Cached full meta response (computed lazily on first meta() call). */
|
|
501
564
|
_metaResponse;
|
|
565
|
+
/** Cached serialized form schemas keyed by `FormType.name` — populated lazily by {@link metaForm}. */
|
|
566
|
+
_formSchemas = /* @__PURE__ */ new Map();
|
|
502
567
|
constructor(boundType, controllerName, app, kindTag = "readable") {
|
|
503
568
|
this.boundType = boundType;
|
|
504
569
|
this.controllerName = controllerName;
|
|
@@ -626,6 +691,42 @@ let AsReadableController = class AsReadableController {
|
|
|
626
691
|
const idx = url.indexOf("?");
|
|
627
692
|
return (0, _uniqu_url.parseUrl)(idx >= 0 ? url.slice(idx + 1) : "");
|
|
628
693
|
}
|
|
694
|
+
/**
|
|
695
|
+
* Parse a URL keeping only `$*` control keywords; report whether any
|
|
696
|
+
* non-control parts were present. Used by `/one` routes where the
|
|
697
|
+
* uniquery lexer cannot tokenise PK values containing `-` and other
|
|
698
|
+
* reserved chars, so non-control parts must be stripped before lexing.
|
|
699
|
+
* `/one/:id` rejects stray filter params with 400 via `hasNonControl`;
|
|
700
|
+
* `/one` (composite) ignores it because the composite-key params have
|
|
701
|
+
* already been extracted via `@Query()`.
|
|
702
|
+
*/
|
|
703
|
+
parseControlsOnlyFromUrl(url) {
|
|
704
|
+
const idx = url.indexOf("?");
|
|
705
|
+
const qs = idx >= 0 ? url.slice(idx + 1) : "";
|
|
706
|
+
if (!qs) return {
|
|
707
|
+
parsed: (0, _uniqu_url.parseUrl)(""),
|
|
708
|
+
hasNonControl: false
|
|
709
|
+
};
|
|
710
|
+
const kept = [];
|
|
711
|
+
let hasNonControl = false;
|
|
712
|
+
for (const part of qs.split("&")) {
|
|
713
|
+
if (!part) continue;
|
|
714
|
+
const eq = part.indexOf("=");
|
|
715
|
+
const rawKey = eq === -1 ? part : part.slice(0, eq);
|
|
716
|
+
let key;
|
|
717
|
+
try {
|
|
718
|
+
key = decodeURIComponent(rawKey);
|
|
719
|
+
} catch {
|
|
720
|
+
key = rawKey;
|
|
721
|
+
}
|
|
722
|
+
if (key.startsWith("$")) kept.push(part);
|
|
723
|
+
else hasNonControl = true;
|
|
724
|
+
}
|
|
725
|
+
return {
|
|
726
|
+
parsed: (0, _uniqu_url.parseUrl)(kept.join("&")),
|
|
727
|
+
hasNonControl
|
|
728
|
+
};
|
|
729
|
+
}
|
|
629
730
|
async returnOne(result) {
|
|
630
731
|
const item = await result;
|
|
631
732
|
if (!item) return new _moostjs_event_http.HttpError(404);
|
|
@@ -641,6 +742,25 @@ let AsReadableController = class AsReadableController {
|
|
|
641
742
|
return this.applyMetaOverlay(this._metaResponse);
|
|
642
743
|
}
|
|
643
744
|
/**
|
|
745
|
+
* **GET /meta/form/:name** — returns the serialized schema of a form
|
|
746
|
+
* referenced by an action's `inputForm` field. The form name is the
|
|
747
|
+
* compiled `.as` class's `.name`, registered when an action's parameter is
|
|
748
|
+
* decorated with `@InputForm(FormType)`. Schemas are serialized once and
|
|
749
|
+
* cached per controller; the response uses the same annotation-allowlist
|
|
750
|
+
* policy as {@link getSerializeOptions}.
|
|
751
|
+
*/
|
|
752
|
+
async metaForm(name) {
|
|
753
|
+
discoverActions(this.constructor, this.app, this.logger);
|
|
754
|
+
const formType = getControllerFormType(this.constructor, name);
|
|
755
|
+
if (!formType) throw new _moostjs_event_http.HttpError(404, `Unknown form "${name}"`);
|
|
756
|
+
let cached = this._formSchemas.get(name);
|
|
757
|
+
if (!cached) {
|
|
758
|
+
cached = (0, _atscript_typescript_utils.serializeAnnotatedType)(formType, this.getSerializeOptions());
|
|
759
|
+
this._formSchemas.set(name, cached);
|
|
760
|
+
}
|
|
761
|
+
return cached;
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
644
764
|
* Builds the `/meta` payload. Override in subclasses to populate source-specific
|
|
645
765
|
* fields. Subclasses that fully replace the envelope must call
|
|
646
766
|
* {@link buildActions} and {@link buildCrud} directly so `@DbAction*`
|
|
@@ -694,6 +814,13 @@ __decorate([
|
|
|
694
814
|
__decorateMetadata("design:paramtypes", []),
|
|
695
815
|
__decorateMetadata("design:returntype", Promise)
|
|
696
816
|
], AsReadableController.prototype, "meta", null);
|
|
817
|
+
__decorate([
|
|
818
|
+
(0, _moostjs_event_http.Get)("meta/form/:name"),
|
|
819
|
+
__decorateParam(0, (0, moost.Param)("name")),
|
|
820
|
+
__decorateMetadata("design:type", Function),
|
|
821
|
+
__decorateMetadata("design:paramtypes", [String]),
|
|
822
|
+
__decorateMetadata("design:returntype", Promise)
|
|
823
|
+
], AsReadableController.prototype, "metaForm", null);
|
|
697
824
|
AsReadableController = __decorate([UseValidationErrorTransform(), __decorateMetadata("design:paramtypes", [
|
|
698
825
|
Object,
|
|
699
826
|
String,
|
|
@@ -856,13 +983,6 @@ const QUERY_CONTROLS = [
|
|
|
856
983
|
const PAGES_CONTROLS = ["filter", ...dtoControls(PagesControlsDto)];
|
|
857
984
|
const ONE_CONTROLS = dtoControls(GetOneControlsDto);
|
|
858
985
|
//#endregion
|
|
859
|
-
//#region \0@oxc-project+runtime@0.120.0/helpers/decorateParam.js
|
|
860
|
-
function __decorateParam(paramIndex, decorator) {
|
|
861
|
-
return function(target, key) {
|
|
862
|
-
decorator(target, key, paramIndex);
|
|
863
|
-
};
|
|
864
|
-
}
|
|
865
|
-
//#endregion
|
|
866
986
|
//#region src/as-db-readable.controller.ts
|
|
867
987
|
var _ref$3, _ref2$2;
|
|
868
988
|
let AsDbReadableController = class AsDbReadableController extends AsReadableController {
|
|
@@ -1214,9 +1334,9 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
|
|
|
1214
1334
|
* **GET /one/:id** — retrieves a single record by ID or unique property.
|
|
1215
1335
|
*/
|
|
1216
1336
|
async getOne(id, url) {
|
|
1217
|
-
const parsed = this.
|
|
1337
|
+
const { parsed, hasNonControl } = this.parseControlsOnlyFromUrl(url);
|
|
1338
|
+
if (hasNonControl) return new _moostjs_event_http.HttpError(400, "Filtering is not allowed for \"one\" endpoint");
|
|
1218
1339
|
this._coerceActionsControl(parsed.controls);
|
|
1219
|
-
if (Object.keys(parsed.filter).length > 0) return new _moostjs_event_http.HttpError(400, "Filtering is not allowed for \"one\" endpoint");
|
|
1220
1340
|
const error = this.validateParsed(parsed, "getOne");
|
|
1221
1341
|
if (error) return error;
|
|
1222
1342
|
const rawSelect = await this.transformProjection(parsed.controls.$select);
|
|
@@ -1231,7 +1351,7 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
|
|
|
1231
1351
|
async getOneComposite(query, url) {
|
|
1232
1352
|
const idObj = this.extractIdShape(query);
|
|
1233
1353
|
if (idObj instanceof _moostjs_event_http.HttpError) return idObj;
|
|
1234
|
-
const parsed = this.
|
|
1354
|
+
const { parsed } = this.parseControlsOnlyFromUrl(url);
|
|
1235
1355
|
this._coerceActionsControl(parsed.controls);
|
|
1236
1356
|
const rawSelect = await this.transformProjection(parsed.controls.$select);
|
|
1237
1357
|
const select = this.widenPreferredIdProjection(rawSelect);
|
|
@@ -1847,6 +1967,29 @@ function readCurrentActionMeta(ctx) {
|
|
|
1847
1967
|
return (0, moost.getMoostMate)().read(ctrl.constructor, methodName)?.[MOOST_DB_ACTION];
|
|
1848
1968
|
}
|
|
1849
1969
|
//#endregion
|
|
1970
|
+
//#region src/actions/input-form-cache.ts
|
|
1971
|
+
/**
|
|
1972
|
+
* Cached parse of the action request body. Centralises the shape check so
|
|
1973
|
+
* every per-param resolver (`@DbActionID*`, `@DbActionRow*`, `@InputForm`)
|
|
1974
|
+
* reads through the same gate. An array or scalar root is rejected with the
|
|
1975
|
+
* same `ValidatorError` envelope as today's strict-shape ID failures.
|
|
1976
|
+
*/
|
|
1977
|
+
const dbActionBodySlot = (0, _wooksjs_event_core.cached)(async (ctx) => {
|
|
1978
|
+
const raw = await (0, _wooksjs_http_body.useBody)(ctx).parseBody();
|
|
1979
|
+
if (raw == null) return {};
|
|
1980
|
+
if (typeof raw !== "object" || Array.isArray(raw)) throw new _atscript_typescript_utils.ValidatorError([{
|
|
1981
|
+
path: "",
|
|
1982
|
+
message: "Action body must be an object of shape { ids?, input? }"
|
|
1983
|
+
}]);
|
|
1984
|
+
return raw;
|
|
1985
|
+
});
|
|
1986
|
+
/** Cached `body.input` slot — consumed by `@InputForm()` and `useDbActionInput()`. */
|
|
1987
|
+
const dbActionInputSlot = (0, _wooksjs_event_core.cached)(async (ctx) => {
|
|
1988
|
+
return (await ctx.get(dbActionBodySlot)).input;
|
|
1989
|
+
});
|
|
1990
|
+
/** Composable for in-handler reads of the form input. */
|
|
1991
|
+
const useDbActionInput = (0, _wooksjs_event_core.defineWook)((ctx) => ({ load: () => ctx.get(dbActionInputSlot) }));
|
|
1992
|
+
//#endregion
|
|
1850
1993
|
//#region src/actions/id-validation.ts
|
|
1851
1994
|
const SOURCE_CACHE = /* @__PURE__ */ new WeakMap();
|
|
1852
1995
|
function getSourceCache(source) {
|
|
@@ -1964,9 +2107,9 @@ function noTableError(ctx) {
|
|
|
1964
2107
|
async function resolveValidatedId(ctx, validate) {
|
|
1965
2108
|
const table = getActionTable(ctx);
|
|
1966
2109
|
if (!isIdValidationSource(table)) throw noTableError(ctx);
|
|
1967
|
-
const
|
|
1968
|
-
validate(
|
|
1969
|
-
return
|
|
2110
|
+
const env = await ctx.get(dbActionBodySlot);
|
|
2111
|
+
validate(env.ids, table);
|
|
2112
|
+
return env.ids;
|
|
1970
2113
|
}
|
|
1971
2114
|
const dbActionIdSlot = (0, _wooksjs_event_core.cached)((ctx) => resolveValidatedId(ctx, validateSingleId));
|
|
1972
2115
|
const dbActionIdsSlot = (0, _wooksjs_event_core.cached)(async (ctx) => {
|
|
@@ -2325,6 +2468,43 @@ function classLevelActions(dict, forcedLevel) {
|
|
|
2325
2468
|
});
|
|
2326
2469
|
}
|
|
2327
2470
|
//#endregion
|
|
2471
|
+
//#region src/actions/db-action-input-form.decorator.ts
|
|
2472
|
+
/**
|
|
2473
|
+
* Parameter decorator that injects the `input` field of the action request
|
|
2474
|
+
* envelope (`{ ids?, input? }`) into the handler.
|
|
2475
|
+
*
|
|
2476
|
+
* Pairs the resolved value with two pieces of param-level metadata:
|
|
2477
|
+
*
|
|
2478
|
+
* 1. {@link MOOST_DB_ACTION_INPUT_FORM} — the compiled `.as` class plus its
|
|
2479
|
+
* name, consumed by {@link discoverActions} to:
|
|
2480
|
+
* - emit `inputForm: FormType.name` on the action's `/meta` entry, and
|
|
2481
|
+
* - register the type in the controller's form registry so
|
|
2482
|
+
* `GET /meta/form/:name` can serve the serialized schema.
|
|
2483
|
+
* 2. {@link MOOST_ATSCRIPT_TYPE} — just the type ref, providing a generic
|
|
2484
|
+
* hook any atscript-aware Moost pipe can read without knowing about the
|
|
2485
|
+
* moost-db-specific key.
|
|
2486
|
+
*
|
|
2487
|
+
* Validation is intentionally *not* performed here. To validate `input`
|
|
2488
|
+
* against `FormType`, install an atscript validator pipe globally
|
|
2489
|
+
* (`app.applyGlobalPipes(...)`) or scope it via `@Pipe(...)`. The pipe reads
|
|
2490
|
+
* `MOOST_ATSCRIPT_TYPE` off the param and runs `FormType.validator()`.
|
|
2491
|
+
*
|
|
2492
|
+
* Only one `@InputForm()` per action is supported. To collect multiple
|
|
2493
|
+
* structured inputs, compose them into a single `.as` interface and pass an
|
|
2494
|
+
* array form on the field whose user-facing intent is "list of items".
|
|
2495
|
+
*
|
|
2496
|
+
* @param formType A compiled `.as` interface class (carries `.validator()`,
|
|
2497
|
+
* `.metadata`, etc.).
|
|
2498
|
+
*/
|
|
2499
|
+
function InputForm(formType) {
|
|
2500
|
+
const mate = (0, moost.getMoostMate)();
|
|
2501
|
+
const meta = {
|
|
2502
|
+
type: formType,
|
|
2503
|
+
name: formType.name
|
|
2504
|
+
};
|
|
2505
|
+
return (0, moost.ApplyDecorators)(mate.decorate(MOOST_DB_ACTION_INPUT_FORM, meta), mate.decorate(MOOST_ATSCRIPT_TYPE, formType), (0, moost.Resolve)(async () => (0, _wooksjs_event_core.current)().get(dbActionInputSlot), "dbActionInputForm"));
|
|
2506
|
+
}
|
|
2507
|
+
//#endregion
|
|
2328
2508
|
//#region src/actions/per-row.ts
|
|
2329
2509
|
/**
|
|
2330
2510
|
* Lift a per-row predicate into the batch shape required by
|
|
@@ -2381,6 +2561,9 @@ exports.DbActions = DbActions;
|
|
|
2381
2561
|
exports.DbRowActions = DbRowActions;
|
|
2382
2562
|
exports.DbRowsActions = DbRowsActions;
|
|
2383
2563
|
exports.DbTableActions = DbTableActions;
|
|
2564
|
+
exports.InputForm = InputForm;
|
|
2565
|
+
exports.MOOST_ATSCRIPT_TYPE = MOOST_ATSCRIPT_TYPE;
|
|
2566
|
+
exports.MOOST_DB_ACTION_INPUT_FORM = MOOST_DB_ACTION_INPUT_FORM;
|
|
2384
2567
|
exports.ONE_CONTROLS = ONE_CONTROLS;
|
|
2385
2568
|
exports.PAGES_CONTROLS = PAGES_CONTROLS;
|
|
2386
2569
|
exports.QUERY_CONTROLS = QUERY_CONTROLS;
|
|
@@ -2390,10 +2573,14 @@ exports.TABLE_DEF = TABLE_DEF;
|
|
|
2390
2573
|
exports.TableController = TableController;
|
|
2391
2574
|
exports.UseValidationErrorTransform = UseValidationErrorTransform;
|
|
2392
2575
|
exports.ViewController = ViewController;
|
|
2576
|
+
exports.dbActionBodySlot = dbActionBodySlot;
|
|
2577
|
+
exports.dbActionInputSlot = dbActionInputSlot;
|
|
2393
2578
|
exports.discoverActions = discoverActions;
|
|
2579
|
+
exports.getControllerFormType = getControllerFormType;
|
|
2394
2580
|
exports.perRow = perRow;
|
|
2395
2581
|
exports.useDbActionId = useDbActionId;
|
|
2396
2582
|
exports.useDbActionIds = useDbActionIds;
|
|
2583
|
+
exports.useDbActionInput = useDbActionInput;
|
|
2397
2584
|
exports.useDbActionRow = useDbActionRow;
|
|
2398
2585
|
exports.useDbActionRows = useDbActionRows;
|
|
2399
2586
|
exports.validationErrorTransform = validationErrorTransform;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import * as _atscript_typescript_utils0 from "@atscript/typescript/utils";
|
|
2
|
-
import { TAtscriptAnnotatedType, TAtscriptDataType, TSerializeOptions, Validator } from "@atscript/typescript/utils";
|
|
3
1
|
import * as _uniqu_url0 from "@uniqu/url";
|
|
2
|
+
import { parseUrl } from "@uniqu/url";
|
|
3
|
+
import { TAtscriptAnnotatedType, TAtscriptDataType, TSerializeOptions, TSerializedAnnotatedType, Validator } from "@atscript/typescript/utils";
|
|
4
4
|
import { AtscriptDbReadable, AtscriptDbTable, FilterExpr, FlatOf, TCrudOp, TCrudPermissions, TCrudPermissions as TCrudPermissions$1, TDbActionInfo, TDbActionInfo as TDbActionInfo$1, TDbActionIntent, TDbActionIntent as TDbActionIntent$1, TDbActionLevel, TDbActionLevel as TDbActionLevel$1, TDbActionProcessor, TDbFieldMeta, TIdentification, TMetaResponse, Uniquery, UniqueryControls } from "@atscript/db";
|
|
5
5
|
import { HttpError } from "@moostjs/event-http";
|
|
6
6
|
import * as moost from "moost";
|
|
@@ -61,13 +61,15 @@ declare abstract class AsReadableController<T extends TAtscriptAnnotatedType = T
|
|
|
61
61
|
private _serializedType?;
|
|
62
62
|
/** Cached full meta response (computed lazily on first meta() call). */
|
|
63
63
|
private _metaResponse?;
|
|
64
|
+
/** Cached serialized form schemas keyed by `FormType.name` — populated lazily by {@link metaForm}. */
|
|
65
|
+
private _formSchemas;
|
|
64
66
|
constructor(boundType: T, controllerName: string, app: Moost, kindTag?: string);
|
|
65
67
|
/** Subclass contract: return `true` if `path` addresses a valid field on the bound source. */
|
|
66
68
|
protected abstract hasField(path: string): boolean;
|
|
67
69
|
/** Sets @db.http.path on the type metadata from the controller's computed prefix. */
|
|
68
70
|
private _resolveHttpPath;
|
|
69
71
|
/** Lazily serializes the bound type (after all controllers have set @db.http.path). */
|
|
70
|
-
protected getSerializedType():
|
|
72
|
+
protected getSerializedType(): TSerializedAnnotatedType;
|
|
71
73
|
/**
|
|
72
74
|
* One-time initialization hook. Override to seed data, register watchers, etc.
|
|
73
75
|
*/
|
|
@@ -105,6 +107,19 @@ declare abstract class AsReadableController<T extends TAtscriptAnnotatedType = T
|
|
|
105
107
|
*/
|
|
106
108
|
protected checkGates(filter: FilterExpr | undefined, controls: Record<string, unknown>, gates: ReadableGates): HttpError | undefined;
|
|
107
109
|
protected parseQueryString(url: string): _uniqu_url0.UrlQuery;
|
|
110
|
+
/**
|
|
111
|
+
* Parse a URL keeping only `$*` control keywords; report whether any
|
|
112
|
+
* non-control parts were present. Used by `/one` routes where the
|
|
113
|
+
* uniquery lexer cannot tokenise PK values containing `-` and other
|
|
114
|
+
* reserved chars, so non-control parts must be stripped before lexing.
|
|
115
|
+
* `/one/:id` rejects stray filter params with 400 via `hasNonControl`;
|
|
116
|
+
* `/one` (composite) ignores it because the composite-key params have
|
|
117
|
+
* already been extracted via `@Query()`.
|
|
118
|
+
*/
|
|
119
|
+
protected parseControlsOnlyFromUrl(url: string): {
|
|
120
|
+
parsed: ReturnType<typeof parseUrl>;
|
|
121
|
+
hasNonControl: boolean;
|
|
122
|
+
};
|
|
108
123
|
protected returnOne(result: Promise<DataType | null>): Promise<DataType | HttpError>;
|
|
109
124
|
/**
|
|
110
125
|
* **GET /meta** — returns the bound interface's metadata envelope. The
|
|
@@ -112,6 +127,15 @@ declare abstract class AsReadableController<T extends TAtscriptAnnotatedType = T
|
|
|
112
127
|
* subclasses can prune the response by principal.
|
|
113
128
|
*/
|
|
114
129
|
meta(): Promise<TMetaResponse>;
|
|
130
|
+
/**
|
|
131
|
+
* **GET /meta/form/:name** — returns the serialized schema of a form
|
|
132
|
+
* referenced by an action's `inputForm` field. The form name is the
|
|
133
|
+
* compiled `.as` class's `.name`, registered when an action's parameter is
|
|
134
|
+
* decorated with `@InputForm(FormType)`. Schemas are serialized once and
|
|
135
|
+
* cached per controller; the response uses the same annotation-allowlist
|
|
136
|
+
* policy as {@link getSerializeOptions}.
|
|
137
|
+
*/
|
|
138
|
+
metaForm(name: string): Promise<TSerializedAnnotatedType>;
|
|
115
139
|
/**
|
|
116
140
|
* Builds the `/meta` payload. Override in subclasses to populate source-specific
|
|
117
141
|
* fields. Subclasses that fully replace the envelope must call
|
|
@@ -716,6 +740,38 @@ declare function DbRowActions<TRow = unknown, const D extends Record<string, unk
|
|
|
716
740
|
/** Sugar for `@DbActions` with `level: 'rows'` injected into each entry. */
|
|
717
741
|
declare function DbRowsActions<TRow = unknown, const D extends Record<string, unknown> = {}>(dict: D & ValidatedUnpinnedDict<TRow, D>): ClassDecorator;
|
|
718
742
|
//#endregion
|
|
743
|
+
//#region src/actions/db-action-input-form.decorator.d.ts
|
|
744
|
+
/**
|
|
745
|
+
* Parameter decorator that injects the `input` field of the action request
|
|
746
|
+
* envelope (`{ ids?, input? }`) into the handler.
|
|
747
|
+
*
|
|
748
|
+
* Pairs the resolved value with two pieces of param-level metadata:
|
|
749
|
+
*
|
|
750
|
+
* 1. {@link MOOST_DB_ACTION_INPUT_FORM} — the compiled `.as` class plus its
|
|
751
|
+
* name, consumed by {@link discoverActions} to:
|
|
752
|
+
* - emit `inputForm: FormType.name` on the action's `/meta` entry, and
|
|
753
|
+
* - register the type in the controller's form registry so
|
|
754
|
+
* `GET /meta/form/:name` can serve the serialized schema.
|
|
755
|
+
* 2. {@link MOOST_ATSCRIPT_TYPE} — just the type ref, providing a generic
|
|
756
|
+
* hook any atscript-aware Moost pipe can read without knowing about the
|
|
757
|
+
* moost-db-specific key.
|
|
758
|
+
*
|
|
759
|
+
* Validation is intentionally *not* performed here. To validate `input`
|
|
760
|
+
* against `FormType`, install an atscript validator pipe globally
|
|
761
|
+
* (`app.applyGlobalPipes(...)`) or scope it via `@Pipe(...)`. The pipe reads
|
|
762
|
+
* `MOOST_ATSCRIPT_TYPE` off the param and runs `FormType.validator()`.
|
|
763
|
+
*
|
|
764
|
+
* Only one `@InputForm()` per action is supported. To collect multiple
|
|
765
|
+
* structured inputs, compose them into a single `.as` interface and pass an
|
|
766
|
+
* array form on the field whose user-facing intent is "list of items".
|
|
767
|
+
*
|
|
768
|
+
* @param formType A compiled `.as` interface class (carries `.validator()`,
|
|
769
|
+
* `.metadata`, etc.).
|
|
770
|
+
*/
|
|
771
|
+
declare function InputForm<T extends TAtscriptAnnotatedType & {
|
|
772
|
+
readonly name: string;
|
|
773
|
+
}>(formType: T): ParameterDecorator;
|
|
774
|
+
//#endregion
|
|
719
775
|
//#region src/actions/discover.d.ts
|
|
720
776
|
/**
|
|
721
777
|
* Pairs the wire-shaped `info` with the original decorator opts / dict entry,
|
|
@@ -726,6 +782,8 @@ interface TDbActionEnvelope {
|
|
|
726
782
|
info: TDbActionInfo$1;
|
|
727
783
|
raw: DbActionOpts | TDbActionsEntry;
|
|
728
784
|
}
|
|
785
|
+
/** Lookup helper for `AsReadableController.metaForm()`. */
|
|
786
|
+
declare function getControllerFormType(ctor: Function, name: string): TAtscriptAnnotatedType | undefined;
|
|
729
787
|
/** Discover actions on a controller, memoized per ctor. `info`-only callers map `e => e.info`. */
|
|
730
788
|
declare function discoverActions(controllerCtor: Function, app: Moost, logger: TConsoleBase): TDbActionEnvelope[];
|
|
731
789
|
//#endregion
|
|
@@ -752,6 +810,94 @@ declare const useDbActionRows: _wooksjs_event_core0.WookComposable<{
|
|
|
752
810
|
load: () => Promise<(Record<string, unknown> | undefined)[]>;
|
|
753
811
|
}>;
|
|
754
812
|
//#endregion
|
|
813
|
+
//#region src/actions/input-form-cache.d.ts
|
|
814
|
+
/**
|
|
815
|
+
* Wire-shape of an action request body. Both fields are optional:
|
|
816
|
+
*
|
|
817
|
+
* - `ids` — what previously sat at the body root: a single identifier object
|
|
818
|
+
* (`'row'`-level), an array of identifier objects (`'rows'`-level), or
|
|
819
|
+
* absent (`'table'`-level).
|
|
820
|
+
* - `input` — present only when the action declares an `@InputForm()`
|
|
821
|
+
* parameter; carries the form payload the user filled out.
|
|
822
|
+
*/
|
|
823
|
+
interface DbActionEnvelope {
|
|
824
|
+
ids?: unknown;
|
|
825
|
+
input?: unknown;
|
|
826
|
+
}
|
|
827
|
+
/**
|
|
828
|
+
* Cached parse of the action request body. Centralises the shape check so
|
|
829
|
+
* every per-param resolver (`@DbActionID*`, `@DbActionRow*`, `@InputForm`)
|
|
830
|
+
* reads through the same gate. An array or scalar root is rejected with the
|
|
831
|
+
* same `ValidatorError` envelope as today's strict-shape ID failures.
|
|
832
|
+
*/
|
|
833
|
+
declare const dbActionBodySlot: moost.Cached<Promise<DbActionEnvelope>>;
|
|
834
|
+
/** Cached `body.input` slot — consumed by `@InputForm()` and `useDbActionInput()`. */
|
|
835
|
+
declare const dbActionInputSlot: moost.Cached<Promise<unknown>>;
|
|
836
|
+
/** Composable for in-handler reads of the form input. */
|
|
837
|
+
declare const useDbActionInput: _wooksjs_event_core0.WookComposable<{
|
|
838
|
+
load: () => Promise<unknown>;
|
|
839
|
+
}>;
|
|
840
|
+
//#endregion
|
|
841
|
+
//#region src/actions/keys.d.ts
|
|
842
|
+
/** Method-level metadata key — written by `@DbAction(name, opts)`. */
|
|
843
|
+
declare const MOOST_DB_ACTION = "atscript_db_action";
|
|
844
|
+
/** Class-level metadata key — written by `@DbActions` and the level-pinned shortcuts. Stored as an array; decorators accumulate. */
|
|
845
|
+
declare const MOOST_DB_ACTIONS = "atscript_db_actions";
|
|
846
|
+
/** Param-level metadata key — written by `@DbActionID()` / `@DbActionIDs()`. Drives level inference. */
|
|
847
|
+
declare const MOOST_DB_ACTION_PARAM = "atscript_db_action_param";
|
|
848
|
+
/** Param-level marker keys — written by `@DbActionRow()` / `@DbActionRows()`. */
|
|
849
|
+
declare const MOOST_DB_ACTION_ROW = "atscript_db_action_row";
|
|
850
|
+
declare const MOOST_DB_ACTION_ROWS = "atscript_db_action_rows";
|
|
851
|
+
/**
|
|
852
|
+
* Param-level metadata key — written by `@InputForm(FormType)`. Carries the
|
|
853
|
+
* compiled `.as` class plus its `.name` so {@link discoverActions} can both
|
|
854
|
+
* emit `inputForm` on `/meta` and register the type in the controller's form
|
|
855
|
+
* registry for `GET /meta/form/:name`.
|
|
856
|
+
*/
|
|
857
|
+
declare const MOOST_DB_ACTION_INPUT_FORM = "atscript_db_action_input_form";
|
|
858
|
+
/**
|
|
859
|
+
* Generic param-level metadata key — written by `@InputForm(FormType)`
|
|
860
|
+
* alongside {@link MOOST_DB_ACTION_INPUT_FORM}. Holds just the type ref so a
|
|
861
|
+
* generic atscript-aware Moost pipe (installed globally via
|
|
862
|
+
* `app.applyGlobalPipes(...)` or scoped via `@Pipe(...)`) can validate the
|
|
863
|
+
* resolved value without knowing about the moost-db-specific key.
|
|
864
|
+
*/
|
|
865
|
+
declare const MOOST_ATSCRIPT_TYPE = "atscript_type";
|
|
866
|
+
type TDbActionRowMarker = true;
|
|
867
|
+
/** Stamped by `@InputForm(FormType)` — the compiled `.as` class + the wire name (`FormType.name`). */
|
|
868
|
+
interface TDbActionInputFormMeta {
|
|
869
|
+
type: TAtscriptAnnotatedType;
|
|
870
|
+
name: string;
|
|
871
|
+
}
|
|
872
|
+
/** Method-level action metadata written by `@DbAction(name, opts)`. */
|
|
873
|
+
interface TDbActionMeta {
|
|
874
|
+
name: string;
|
|
875
|
+
opts: DbActionOpts;
|
|
876
|
+
}
|
|
877
|
+
/** Class-level entry — a `TDbActionsEntry` plus its dictionary key. */
|
|
878
|
+
interface TDbClassActionMeta {
|
|
879
|
+
name: string;
|
|
880
|
+
entry: TDbActionsEntry;
|
|
881
|
+
}
|
|
882
|
+
/** Param marker kind — informs level inference and ID-resolution shape. */
|
|
883
|
+
type TDbActionParamKind = "id" | "ids";
|
|
884
|
+
declare module "moost" {
|
|
885
|
+
interface TMoostMetadata {
|
|
886
|
+
[MOOST_DB_ACTION]?: TDbActionMeta;
|
|
887
|
+
[MOOST_DB_ACTIONS]?: TDbClassActionMeta[];
|
|
888
|
+
[MOOST_DB_ACTION_PARAM]?: TDbActionParamKind;
|
|
889
|
+
[MOOST_DB_ACTION_ROW]?: TDbActionRowMarker;
|
|
890
|
+
[MOOST_DB_ACTION_ROWS]?: TDbActionRowMarker;
|
|
891
|
+
}
|
|
892
|
+
interface TMoostParamsMetadata {
|
|
893
|
+
[MOOST_DB_ACTION_PARAM]?: TDbActionParamKind;
|
|
894
|
+
[MOOST_DB_ACTION_ROW]?: TDbActionRowMarker;
|
|
895
|
+
[MOOST_DB_ACTION_ROWS]?: TDbActionRowMarker;
|
|
896
|
+
[MOOST_DB_ACTION_INPUT_FORM]?: TDbActionInputFormMeta;
|
|
897
|
+
[MOOST_ATSCRIPT_TYPE]?: TAtscriptAnnotatedType;
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
//#endregion
|
|
755
901
|
//#region src/actions/action-disabled-error.d.ts
|
|
756
902
|
/**
|
|
757
903
|
* Wire-body shape for server-side gate rejections. The `name` discriminator
|
|
@@ -812,4 +958,4 @@ declare const QUERY_CONTROLS: readonly string[];
|
|
|
812
958
|
declare const PAGES_CONTROLS: readonly string[];
|
|
813
959
|
declare const ONE_CONTROLS: readonly string[];
|
|
814
960
|
//#endregion
|
|
815
|
-
export { ActionDisabledError, ActionDisabledErrorBody, AsDbController, AsDbReadableController, AsJsonValueHelpController, AsReadableController, AsValueHelpController, DbAction, DbActionDefault, DbActionID, DbActionIDs, DbActionOpts, DbActionRow, DbActionRows, DbActions, DbRowActions, DbRowsActions, DbTableActions, IdValidationSource, ONE_CONTROLS, PAGES_CONTROLS, QUERY_CONTROLS, READABLE_DEF, ReadableController, ReadableGates, TABLE_DEF, type TCrudOp, type TCrudPermissions, type TDbActionInfo, type TDbActionIntent, type TDbActionLevel, type TDbActionProcessor, TDbActionsEntry, TDbActionsEntryUnpinned, TableController, UseValidationErrorTransform, ValueHelpQuery, ViewController, discoverActions, perRow, useDbActionId, useDbActionIds, useDbActionRow, useDbActionRows, validationErrorTransform };
|
|
961
|
+
export { ActionDisabledError, type ActionDisabledErrorBody, AsDbController, AsDbReadableController, AsJsonValueHelpController, AsReadableController, AsValueHelpController, DbAction, DbActionDefault, type DbActionEnvelope, DbActionID, DbActionIDs, type DbActionOpts, DbActionRow, DbActionRows, DbActions, DbRowActions, DbRowsActions, DbTableActions, type IdValidationSource, InputForm, MOOST_ATSCRIPT_TYPE, MOOST_DB_ACTION_INPUT_FORM, ONE_CONTROLS, PAGES_CONTROLS, QUERY_CONTROLS, READABLE_DEF, ReadableController, ReadableGates, TABLE_DEF, type TCrudOp, type TCrudPermissions, type TDbActionInfo, type TDbActionInputFormMeta, type TDbActionIntent, type TDbActionLevel, type TDbActionProcessor, type TDbActionsEntry, type TDbActionsEntryUnpinned, TableController, UseValidationErrorTransform, ValueHelpQuery, ViewController, dbActionBodySlot, dbActionInputSlot, discoverActions, getControllerFormType, perRow, useDbActionId, useDbActionIds, useDbActionInput, useDbActionRow, useDbActionRows, validationErrorTransform };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { TAtscriptAnnotatedType, TAtscriptDataType, TSerializeOptions, Validator } from "@atscript/typescript/utils";
|
|
1
|
+
import { TAtscriptAnnotatedType, TAtscriptDataType, TSerializeOptions, TSerializedAnnotatedType, Validator } from "@atscript/typescript/utils";
|
|
3
2
|
import { HttpError } from "@moostjs/event-http";
|
|
4
3
|
import * as moost from "moost";
|
|
5
4
|
import { Moost, TConsoleBase } from "moost";
|
|
6
5
|
import * as _uniqu_url0 from "@uniqu/url";
|
|
6
|
+
import { parseUrl } from "@uniqu/url";
|
|
7
7
|
import { AtscriptDbReadable, AtscriptDbTable, FilterExpr, FlatOf, TCrudOp, TCrudPermissions, TCrudPermissions as TCrudPermissions$1, TDbActionInfo, TDbActionInfo as TDbActionInfo$1, TDbActionIntent, TDbActionIntent as TDbActionIntent$1, TDbActionLevel, TDbActionLevel as TDbActionLevel$1, TDbActionProcessor, TDbFieldMeta, TIdentification, TMetaResponse, Uniquery, UniqueryControls } from "@atscript/db";
|
|
8
8
|
import * as _wooksjs_event_core0 from "@wooksjs/event-core";
|
|
9
9
|
|
|
@@ -61,13 +61,15 @@ declare abstract class AsReadableController<T extends TAtscriptAnnotatedType = T
|
|
|
61
61
|
private _serializedType?;
|
|
62
62
|
/** Cached full meta response (computed lazily on first meta() call). */
|
|
63
63
|
private _metaResponse?;
|
|
64
|
+
/** Cached serialized form schemas keyed by `FormType.name` — populated lazily by {@link metaForm}. */
|
|
65
|
+
private _formSchemas;
|
|
64
66
|
constructor(boundType: T, controllerName: string, app: Moost, kindTag?: string);
|
|
65
67
|
/** Subclass contract: return `true` if `path` addresses a valid field on the bound source. */
|
|
66
68
|
protected abstract hasField(path: string): boolean;
|
|
67
69
|
/** Sets @db.http.path on the type metadata from the controller's computed prefix. */
|
|
68
70
|
private _resolveHttpPath;
|
|
69
71
|
/** Lazily serializes the bound type (after all controllers have set @db.http.path). */
|
|
70
|
-
protected getSerializedType():
|
|
72
|
+
protected getSerializedType(): TSerializedAnnotatedType;
|
|
71
73
|
/**
|
|
72
74
|
* One-time initialization hook. Override to seed data, register watchers, etc.
|
|
73
75
|
*/
|
|
@@ -105,6 +107,19 @@ declare abstract class AsReadableController<T extends TAtscriptAnnotatedType = T
|
|
|
105
107
|
*/
|
|
106
108
|
protected checkGates(filter: FilterExpr | undefined, controls: Record<string, unknown>, gates: ReadableGates): HttpError | undefined;
|
|
107
109
|
protected parseQueryString(url: string): _uniqu_url0.UrlQuery;
|
|
110
|
+
/**
|
|
111
|
+
* Parse a URL keeping only `$*` control keywords; report whether any
|
|
112
|
+
* non-control parts were present. Used by `/one` routes where the
|
|
113
|
+
* uniquery lexer cannot tokenise PK values containing `-` and other
|
|
114
|
+
* reserved chars, so non-control parts must be stripped before lexing.
|
|
115
|
+
* `/one/:id` rejects stray filter params with 400 via `hasNonControl`;
|
|
116
|
+
* `/one` (composite) ignores it because the composite-key params have
|
|
117
|
+
* already been extracted via `@Query()`.
|
|
118
|
+
*/
|
|
119
|
+
protected parseControlsOnlyFromUrl(url: string): {
|
|
120
|
+
parsed: ReturnType<typeof parseUrl>;
|
|
121
|
+
hasNonControl: boolean;
|
|
122
|
+
};
|
|
108
123
|
protected returnOne(result: Promise<DataType | null>): Promise<DataType | HttpError>;
|
|
109
124
|
/**
|
|
110
125
|
* **GET /meta** — returns the bound interface's metadata envelope. The
|
|
@@ -112,6 +127,15 @@ declare abstract class AsReadableController<T extends TAtscriptAnnotatedType = T
|
|
|
112
127
|
* subclasses can prune the response by principal.
|
|
113
128
|
*/
|
|
114
129
|
meta(): Promise<TMetaResponse>;
|
|
130
|
+
/**
|
|
131
|
+
* **GET /meta/form/:name** — returns the serialized schema of a form
|
|
132
|
+
* referenced by an action's `inputForm` field. The form name is the
|
|
133
|
+
* compiled `.as` class's `.name`, registered when an action's parameter is
|
|
134
|
+
* decorated with `@InputForm(FormType)`. Schemas are serialized once and
|
|
135
|
+
* cached per controller; the response uses the same annotation-allowlist
|
|
136
|
+
* policy as {@link getSerializeOptions}.
|
|
137
|
+
*/
|
|
138
|
+
metaForm(name: string): Promise<TSerializedAnnotatedType>;
|
|
115
139
|
/**
|
|
116
140
|
* Builds the `/meta` payload. Override in subclasses to populate source-specific
|
|
117
141
|
* fields. Subclasses that fully replace the envelope must call
|
|
@@ -716,6 +740,38 @@ declare function DbRowActions<TRow = unknown, const D extends Record<string, unk
|
|
|
716
740
|
/** Sugar for `@DbActions` with `level: 'rows'` injected into each entry. */
|
|
717
741
|
declare function DbRowsActions<TRow = unknown, const D extends Record<string, unknown> = {}>(dict: D & ValidatedUnpinnedDict<TRow, D>): ClassDecorator;
|
|
718
742
|
//#endregion
|
|
743
|
+
//#region src/actions/db-action-input-form.decorator.d.ts
|
|
744
|
+
/**
|
|
745
|
+
* Parameter decorator that injects the `input` field of the action request
|
|
746
|
+
* envelope (`{ ids?, input? }`) into the handler.
|
|
747
|
+
*
|
|
748
|
+
* Pairs the resolved value with two pieces of param-level metadata:
|
|
749
|
+
*
|
|
750
|
+
* 1. {@link MOOST_DB_ACTION_INPUT_FORM} — the compiled `.as` class plus its
|
|
751
|
+
* name, consumed by {@link discoverActions} to:
|
|
752
|
+
* - emit `inputForm: FormType.name` on the action's `/meta` entry, and
|
|
753
|
+
* - register the type in the controller's form registry so
|
|
754
|
+
* `GET /meta/form/:name` can serve the serialized schema.
|
|
755
|
+
* 2. {@link MOOST_ATSCRIPT_TYPE} — just the type ref, providing a generic
|
|
756
|
+
* hook any atscript-aware Moost pipe can read without knowing about the
|
|
757
|
+
* moost-db-specific key.
|
|
758
|
+
*
|
|
759
|
+
* Validation is intentionally *not* performed here. To validate `input`
|
|
760
|
+
* against `FormType`, install an atscript validator pipe globally
|
|
761
|
+
* (`app.applyGlobalPipes(...)`) or scope it via `@Pipe(...)`. The pipe reads
|
|
762
|
+
* `MOOST_ATSCRIPT_TYPE` off the param and runs `FormType.validator()`.
|
|
763
|
+
*
|
|
764
|
+
* Only one `@InputForm()` per action is supported. To collect multiple
|
|
765
|
+
* structured inputs, compose them into a single `.as` interface and pass an
|
|
766
|
+
* array form on the field whose user-facing intent is "list of items".
|
|
767
|
+
*
|
|
768
|
+
* @param formType A compiled `.as` interface class (carries `.validator()`,
|
|
769
|
+
* `.metadata`, etc.).
|
|
770
|
+
*/
|
|
771
|
+
declare function InputForm<T extends TAtscriptAnnotatedType & {
|
|
772
|
+
readonly name: string;
|
|
773
|
+
}>(formType: T): ParameterDecorator;
|
|
774
|
+
//#endregion
|
|
719
775
|
//#region src/actions/discover.d.ts
|
|
720
776
|
/**
|
|
721
777
|
* Pairs the wire-shaped `info` with the original decorator opts / dict entry,
|
|
@@ -726,6 +782,8 @@ interface TDbActionEnvelope {
|
|
|
726
782
|
info: TDbActionInfo$1;
|
|
727
783
|
raw: DbActionOpts | TDbActionsEntry;
|
|
728
784
|
}
|
|
785
|
+
/** Lookup helper for `AsReadableController.metaForm()`. */
|
|
786
|
+
declare function getControllerFormType(ctor: Function, name: string): TAtscriptAnnotatedType | undefined;
|
|
729
787
|
/** Discover actions on a controller, memoized per ctor. `info`-only callers map `e => e.info`. */
|
|
730
788
|
declare function discoverActions(controllerCtor: Function, app: Moost, logger: TConsoleBase): TDbActionEnvelope[];
|
|
731
789
|
//#endregion
|
|
@@ -752,6 +810,94 @@ declare const useDbActionRows: _wooksjs_event_core0.WookComposable<{
|
|
|
752
810
|
load: () => Promise<(Record<string, unknown> | undefined)[]>;
|
|
753
811
|
}>;
|
|
754
812
|
//#endregion
|
|
813
|
+
//#region src/actions/input-form-cache.d.ts
|
|
814
|
+
/**
|
|
815
|
+
* Wire-shape of an action request body. Both fields are optional:
|
|
816
|
+
*
|
|
817
|
+
* - `ids` — what previously sat at the body root: a single identifier object
|
|
818
|
+
* (`'row'`-level), an array of identifier objects (`'rows'`-level), or
|
|
819
|
+
* absent (`'table'`-level).
|
|
820
|
+
* - `input` — present only when the action declares an `@InputForm()`
|
|
821
|
+
* parameter; carries the form payload the user filled out.
|
|
822
|
+
*/
|
|
823
|
+
interface DbActionEnvelope {
|
|
824
|
+
ids?: unknown;
|
|
825
|
+
input?: unknown;
|
|
826
|
+
}
|
|
827
|
+
/**
|
|
828
|
+
* Cached parse of the action request body. Centralises the shape check so
|
|
829
|
+
* every per-param resolver (`@DbActionID*`, `@DbActionRow*`, `@InputForm`)
|
|
830
|
+
* reads through the same gate. An array or scalar root is rejected with the
|
|
831
|
+
* same `ValidatorError` envelope as today's strict-shape ID failures.
|
|
832
|
+
*/
|
|
833
|
+
declare const dbActionBodySlot: moost.Cached<Promise<DbActionEnvelope>>;
|
|
834
|
+
/** Cached `body.input` slot — consumed by `@InputForm()` and `useDbActionInput()`. */
|
|
835
|
+
declare const dbActionInputSlot: moost.Cached<Promise<unknown>>;
|
|
836
|
+
/** Composable for in-handler reads of the form input. */
|
|
837
|
+
declare const useDbActionInput: _wooksjs_event_core0.WookComposable<{
|
|
838
|
+
load: () => Promise<unknown>;
|
|
839
|
+
}>;
|
|
840
|
+
//#endregion
|
|
841
|
+
//#region src/actions/keys.d.ts
|
|
842
|
+
/** Method-level metadata key — written by `@DbAction(name, opts)`. */
|
|
843
|
+
declare const MOOST_DB_ACTION = "atscript_db_action";
|
|
844
|
+
/** Class-level metadata key — written by `@DbActions` and the level-pinned shortcuts. Stored as an array; decorators accumulate. */
|
|
845
|
+
declare const MOOST_DB_ACTIONS = "atscript_db_actions";
|
|
846
|
+
/** Param-level metadata key — written by `@DbActionID()` / `@DbActionIDs()`. Drives level inference. */
|
|
847
|
+
declare const MOOST_DB_ACTION_PARAM = "atscript_db_action_param";
|
|
848
|
+
/** Param-level marker keys — written by `@DbActionRow()` / `@DbActionRows()`. */
|
|
849
|
+
declare const MOOST_DB_ACTION_ROW = "atscript_db_action_row";
|
|
850
|
+
declare const MOOST_DB_ACTION_ROWS = "atscript_db_action_rows";
|
|
851
|
+
/**
|
|
852
|
+
* Param-level metadata key — written by `@InputForm(FormType)`. Carries the
|
|
853
|
+
* compiled `.as` class plus its `.name` so {@link discoverActions} can both
|
|
854
|
+
* emit `inputForm` on `/meta` and register the type in the controller's form
|
|
855
|
+
* registry for `GET /meta/form/:name`.
|
|
856
|
+
*/
|
|
857
|
+
declare const MOOST_DB_ACTION_INPUT_FORM = "atscript_db_action_input_form";
|
|
858
|
+
/**
|
|
859
|
+
* Generic param-level metadata key — written by `@InputForm(FormType)`
|
|
860
|
+
* alongside {@link MOOST_DB_ACTION_INPUT_FORM}. Holds just the type ref so a
|
|
861
|
+
* generic atscript-aware Moost pipe (installed globally via
|
|
862
|
+
* `app.applyGlobalPipes(...)` or scoped via `@Pipe(...)`) can validate the
|
|
863
|
+
* resolved value without knowing about the moost-db-specific key.
|
|
864
|
+
*/
|
|
865
|
+
declare const MOOST_ATSCRIPT_TYPE = "atscript_type";
|
|
866
|
+
type TDbActionRowMarker = true;
|
|
867
|
+
/** Stamped by `@InputForm(FormType)` — the compiled `.as` class + the wire name (`FormType.name`). */
|
|
868
|
+
interface TDbActionInputFormMeta {
|
|
869
|
+
type: TAtscriptAnnotatedType;
|
|
870
|
+
name: string;
|
|
871
|
+
}
|
|
872
|
+
/** Method-level action metadata written by `@DbAction(name, opts)`. */
|
|
873
|
+
interface TDbActionMeta {
|
|
874
|
+
name: string;
|
|
875
|
+
opts: DbActionOpts;
|
|
876
|
+
}
|
|
877
|
+
/** Class-level entry — a `TDbActionsEntry` plus its dictionary key. */
|
|
878
|
+
interface TDbClassActionMeta {
|
|
879
|
+
name: string;
|
|
880
|
+
entry: TDbActionsEntry;
|
|
881
|
+
}
|
|
882
|
+
/** Param marker kind — informs level inference and ID-resolution shape. */
|
|
883
|
+
type TDbActionParamKind = "id" | "ids";
|
|
884
|
+
declare module "moost" {
|
|
885
|
+
interface TMoostMetadata {
|
|
886
|
+
[MOOST_DB_ACTION]?: TDbActionMeta;
|
|
887
|
+
[MOOST_DB_ACTIONS]?: TDbClassActionMeta[];
|
|
888
|
+
[MOOST_DB_ACTION_PARAM]?: TDbActionParamKind;
|
|
889
|
+
[MOOST_DB_ACTION_ROW]?: TDbActionRowMarker;
|
|
890
|
+
[MOOST_DB_ACTION_ROWS]?: TDbActionRowMarker;
|
|
891
|
+
}
|
|
892
|
+
interface TMoostParamsMetadata {
|
|
893
|
+
[MOOST_DB_ACTION_PARAM]?: TDbActionParamKind;
|
|
894
|
+
[MOOST_DB_ACTION_ROW]?: TDbActionRowMarker;
|
|
895
|
+
[MOOST_DB_ACTION_ROWS]?: TDbActionRowMarker;
|
|
896
|
+
[MOOST_DB_ACTION_INPUT_FORM]?: TDbActionInputFormMeta;
|
|
897
|
+
[MOOST_ATSCRIPT_TYPE]?: TAtscriptAnnotatedType;
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
//#endregion
|
|
755
901
|
//#region src/actions/action-disabled-error.d.ts
|
|
756
902
|
/**
|
|
757
903
|
* Wire-body shape for server-side gate rejections. The `name` discriminator
|
|
@@ -812,4 +958,4 @@ declare const QUERY_CONTROLS: readonly string[];
|
|
|
812
958
|
declare const PAGES_CONTROLS: readonly string[];
|
|
813
959
|
declare const ONE_CONTROLS: readonly string[];
|
|
814
960
|
//#endregion
|
|
815
|
-
export { ActionDisabledError, type ActionDisabledErrorBody, AsDbController, AsDbReadableController, AsJsonValueHelpController, AsReadableController, AsValueHelpController, DbAction, DbActionDefault, DbActionID, DbActionIDs, type DbActionOpts, DbActionRow, DbActionRows, DbActions, DbRowActions, DbRowsActions, DbTableActions, type IdValidationSource, ONE_CONTROLS, PAGES_CONTROLS, QUERY_CONTROLS, READABLE_DEF, ReadableController, ReadableGates, TABLE_DEF, type TCrudOp, type TCrudPermissions, type TDbActionInfo, type TDbActionIntent, type TDbActionLevel, type TDbActionProcessor, type TDbActionsEntry, type TDbActionsEntryUnpinned, TableController, UseValidationErrorTransform, ValueHelpQuery, ViewController, discoverActions, perRow, useDbActionId, useDbActionIds, useDbActionRow, useDbActionRows, validationErrorTransform };
|
|
961
|
+
export { ActionDisabledError, type ActionDisabledErrorBody, AsDbController, AsDbReadableController, AsJsonValueHelpController, AsReadableController, AsValueHelpController, DbAction, DbActionDefault, type DbActionEnvelope, DbActionID, DbActionIDs, type DbActionOpts, DbActionRow, DbActionRows, DbActions, DbRowActions, DbRowsActions, DbTableActions, type IdValidationSource, InputForm, MOOST_ATSCRIPT_TYPE, MOOST_DB_ACTION_INPUT_FORM, ONE_CONTROLS, PAGES_CONTROLS, QUERY_CONTROLS, READABLE_DEF, ReadableController, ReadableGates, TABLE_DEF, type TCrudOp, type TCrudPermissions, type TDbActionInfo, type TDbActionInputFormMeta, type TDbActionIntent, type TDbActionLevel, type TDbActionProcessor, type TDbActionsEntry, type TDbActionsEntryUnpinned, TableController, UseValidationErrorTransform, ValueHelpQuery, ViewController, dbActionBodySlot, dbActionInputSlot, discoverActions, getControllerFormType, perRow, useDbActionId, useDbActionIds, useDbActionInput, useDbActionRow, useDbActionRows, validationErrorTransform };
|
package/dist/index.mjs
CHANGED
|
@@ -212,6 +212,21 @@ const MOOST_DB_ACTION_PARAM = "atscript_db_action_param";
|
|
|
212
212
|
const MOOST_DB_ACTION_ROW = "atscript_db_action_row";
|
|
213
213
|
const MOOST_DB_ACTION_ROWS = "atscript_db_action_rows";
|
|
214
214
|
/**
|
|
215
|
+
* Param-level metadata key — written by `@InputForm(FormType)`. Carries the
|
|
216
|
+
* compiled `.as` class plus its `.name` so {@link discoverActions} can both
|
|
217
|
+
* emit `inputForm` on `/meta` and register the type in the controller's form
|
|
218
|
+
* registry for `GET /meta/form/:name`.
|
|
219
|
+
*/
|
|
220
|
+
const MOOST_DB_ACTION_INPUT_FORM = "atscript_db_action_input_form";
|
|
221
|
+
/**
|
|
222
|
+
* Generic param-level metadata key — written by `@InputForm(FormType)`
|
|
223
|
+
* alongside {@link MOOST_DB_ACTION_INPUT_FORM}. Holds just the type ref so a
|
|
224
|
+
* generic atscript-aware Moost pipe (installed globally via
|
|
225
|
+
* `app.applyGlobalPipes(...)` or scoped via `@Pipe(...)`) can validate the
|
|
226
|
+
* resolved value without knowing about the moost-db-specific key.
|
|
227
|
+
*/
|
|
228
|
+
const MOOST_ATSCRIPT_TYPE = "atscript_type";
|
|
229
|
+
/**
|
|
215
230
|
* Shared method-decorator update used by `@DbAction` and `@DbActionDefault`:
|
|
216
231
|
* read the existing `MOOST_DB_ACTION` slot, merge the patch (later-applied
|
|
217
232
|
* fields win), and write it back. `name` is empty until `@DbAction` provides
|
|
@@ -234,6 +249,8 @@ function scanParamLevel(params) {
|
|
|
234
249
|
let multi = false;
|
|
235
250
|
let hasRowParam = false;
|
|
236
251
|
let hasBody = false;
|
|
252
|
+
let inputForm;
|
|
253
|
+
let hasDuplicateInputForm = false;
|
|
237
254
|
for (const p of params) {
|
|
238
255
|
const kind = p[MOOST_DB_ACTION_PARAM];
|
|
239
256
|
if (kind === "id") single = true;
|
|
@@ -247,13 +264,18 @@ function scanParamLevel(params) {
|
|
|
247
264
|
hasRowParam = true;
|
|
248
265
|
}
|
|
249
266
|
if (p.paramSource === "BODY") hasBody = true;
|
|
267
|
+
const form = p[MOOST_DB_ACTION_INPUT_FORM];
|
|
268
|
+
if (form) if (inputForm) hasDuplicateInputForm = true;
|
|
269
|
+
else inputForm = form;
|
|
250
270
|
}
|
|
251
271
|
return {
|
|
252
272
|
level: single && multi ? "table" : single ? "row" : multi ? "rows" : "table",
|
|
253
273
|
single,
|
|
254
274
|
multi,
|
|
255
275
|
hasRowParam,
|
|
256
|
-
hasBody
|
|
276
|
+
hasBody,
|
|
277
|
+
inputForm,
|
|
278
|
+
hasDuplicateInputForm
|
|
257
279
|
};
|
|
258
280
|
}
|
|
259
281
|
//#endregion
|
|
@@ -270,6 +292,34 @@ const OPTIONAL_FIELDS = [
|
|
|
270
292
|
];
|
|
271
293
|
const actionsCache = /* @__PURE__ */ new WeakMap();
|
|
272
294
|
const rowLevelActionsCache = /* @__PURE__ */ new WeakMap();
|
|
295
|
+
/**
|
|
296
|
+
* Per-controller registry of form names → compiled `.as` classes, populated
|
|
297
|
+
* during {@link discoverActions} when a method param carries
|
|
298
|
+
* {@link MOOST_DB_ACTION_INPUT_FORM}. Backs `GET /meta/form/:name`.
|
|
299
|
+
*
|
|
300
|
+
* Same name + same type ref across multiple actions is fine (forms can be
|
|
301
|
+
* reused). Same name + *different* type refs is an ambiguity — discovery
|
|
302
|
+
* warns and drops the second action.
|
|
303
|
+
*/
|
|
304
|
+
const formRegistry = /* @__PURE__ */ new WeakMap();
|
|
305
|
+
/** Lookup helper for `AsReadableController.metaForm()`. */
|
|
306
|
+
function getControllerFormType(ctor, name) {
|
|
307
|
+
return formRegistry.get(ctor)?.get(name);
|
|
308
|
+
}
|
|
309
|
+
function registerFormType(ctor, meta, actionName, logger) {
|
|
310
|
+
let map = formRegistry.get(ctor);
|
|
311
|
+
if (!map) {
|
|
312
|
+
map = /* @__PURE__ */ new Map();
|
|
313
|
+
formRegistry.set(ctor, map);
|
|
314
|
+
}
|
|
315
|
+
const existing = map.get(meta.name);
|
|
316
|
+
if (existing && existing !== meta.type) {
|
|
317
|
+
logger.warn(`${WARN_PREFIX} action "${actionName}" — form name "${meta.name}" already registered on this controller with a different type. Reusing the same FormType across actions is fine; clashing names are not — dropping`);
|
|
318
|
+
return false;
|
|
319
|
+
}
|
|
320
|
+
if (!existing) map.set(meta.name, meta.type);
|
|
321
|
+
return true;
|
|
322
|
+
}
|
|
273
323
|
/** Discover actions on a controller, memoized per ctor. `info`-only callers map `e => e.info`. */
|
|
274
324
|
function discoverActions(controllerCtor, app, logger) {
|
|
275
325
|
const cached = actionsCache.get(controllerCtor);
|
|
@@ -355,6 +405,10 @@ function collectMethodActions(ctor, overview, logger, out, seen) {
|
|
|
355
405
|
processor: "backend",
|
|
356
406
|
value: path
|
|
357
407
|
};
|
|
408
|
+
if (levelInfer.inputForm) {
|
|
409
|
+
if (!registerFormType(ctor, levelInfer.inputForm, action.name, logger)) continue;
|
|
410
|
+
info.inputForm = levelInfer.inputForm.name;
|
|
411
|
+
}
|
|
358
412
|
emitInfo(info, action.opts);
|
|
359
413
|
seen.add(action.name);
|
|
360
414
|
out.push({
|
|
@@ -369,10 +423,12 @@ function inferMethodLevel(params, actionName, logger) {
|
|
|
369
423
|
logger.warn(`${WARN_PREFIX} action "${actionName}" mixes single-cardinality and multi-cardinality decorators (@DbActionID / @DbActionRow vs @DbActionIDs / @DbActionRows) — dropping`);
|
|
370
424
|
return null;
|
|
371
425
|
}
|
|
426
|
+
if (scan.hasDuplicateInputForm) logger.warn(`${WARN_PREFIX} action "${actionName}" has more than one @InputForm() param — only the first is honored. Compose multiple inputs into a single form interface.`);
|
|
372
427
|
return {
|
|
373
428
|
level: scan.level,
|
|
374
429
|
bodyConflict: scan.hasBody && scan.level !== "table",
|
|
375
|
-
hasRowParam: scan.hasRowParam
|
|
430
|
+
hasRowParam: scan.hasRowParam,
|
|
431
|
+
inputForm: scan.inputForm
|
|
376
432
|
};
|
|
377
433
|
}
|
|
378
434
|
function collectClassActions(ctor, logger, out, seen) {
|
|
@@ -483,6 +539,13 @@ function __decorate(decorators, target, key, desc) {
|
|
|
483
539
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
484
540
|
}
|
|
485
541
|
//#endregion
|
|
542
|
+
//#region \0@oxc-project+runtime@0.120.0/helpers/decorateParam.js
|
|
543
|
+
function __decorateParam(paramIndex, decorator) {
|
|
544
|
+
return function(target, key) {
|
|
545
|
+
decorator(target, key, paramIndex);
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
//#endregion
|
|
486
549
|
//#region src/as-readable.controller.ts
|
|
487
550
|
var _ref$4;
|
|
488
551
|
let AsReadableController = class AsReadableController {
|
|
@@ -498,6 +561,8 @@ let AsReadableController = class AsReadableController {
|
|
|
498
561
|
_serializedType;
|
|
499
562
|
/** Cached full meta response (computed lazily on first meta() call). */
|
|
500
563
|
_metaResponse;
|
|
564
|
+
/** Cached serialized form schemas keyed by `FormType.name` — populated lazily by {@link metaForm}. */
|
|
565
|
+
_formSchemas = /* @__PURE__ */ new Map();
|
|
501
566
|
constructor(boundType, controllerName, app, kindTag = "readable") {
|
|
502
567
|
this.boundType = boundType;
|
|
503
568
|
this.controllerName = controllerName;
|
|
@@ -625,6 +690,42 @@ let AsReadableController = class AsReadableController {
|
|
|
625
690
|
const idx = url.indexOf("?");
|
|
626
691
|
return parseUrl(idx >= 0 ? url.slice(idx + 1) : "");
|
|
627
692
|
}
|
|
693
|
+
/**
|
|
694
|
+
* Parse a URL keeping only `$*` control keywords; report whether any
|
|
695
|
+
* non-control parts were present. Used by `/one` routes where the
|
|
696
|
+
* uniquery lexer cannot tokenise PK values containing `-` and other
|
|
697
|
+
* reserved chars, so non-control parts must be stripped before lexing.
|
|
698
|
+
* `/one/:id` rejects stray filter params with 400 via `hasNonControl`;
|
|
699
|
+
* `/one` (composite) ignores it because the composite-key params have
|
|
700
|
+
* already been extracted via `@Query()`.
|
|
701
|
+
*/
|
|
702
|
+
parseControlsOnlyFromUrl(url) {
|
|
703
|
+
const idx = url.indexOf("?");
|
|
704
|
+
const qs = idx >= 0 ? url.slice(idx + 1) : "";
|
|
705
|
+
if (!qs) return {
|
|
706
|
+
parsed: parseUrl(""),
|
|
707
|
+
hasNonControl: false
|
|
708
|
+
};
|
|
709
|
+
const kept = [];
|
|
710
|
+
let hasNonControl = false;
|
|
711
|
+
for (const part of qs.split("&")) {
|
|
712
|
+
if (!part) continue;
|
|
713
|
+
const eq = part.indexOf("=");
|
|
714
|
+
const rawKey = eq === -1 ? part : part.slice(0, eq);
|
|
715
|
+
let key;
|
|
716
|
+
try {
|
|
717
|
+
key = decodeURIComponent(rawKey);
|
|
718
|
+
} catch {
|
|
719
|
+
key = rawKey;
|
|
720
|
+
}
|
|
721
|
+
if (key.startsWith("$")) kept.push(part);
|
|
722
|
+
else hasNonControl = true;
|
|
723
|
+
}
|
|
724
|
+
return {
|
|
725
|
+
parsed: parseUrl(kept.join("&")),
|
|
726
|
+
hasNonControl
|
|
727
|
+
};
|
|
728
|
+
}
|
|
628
729
|
async returnOne(result) {
|
|
629
730
|
const item = await result;
|
|
630
731
|
if (!item) return new HttpError(404);
|
|
@@ -640,6 +741,25 @@ let AsReadableController = class AsReadableController {
|
|
|
640
741
|
return this.applyMetaOverlay(this._metaResponse);
|
|
641
742
|
}
|
|
642
743
|
/**
|
|
744
|
+
* **GET /meta/form/:name** — returns the serialized schema of a form
|
|
745
|
+
* referenced by an action's `inputForm` field. The form name is the
|
|
746
|
+
* compiled `.as` class's `.name`, registered when an action's parameter is
|
|
747
|
+
* decorated with `@InputForm(FormType)`. Schemas are serialized once and
|
|
748
|
+
* cached per controller; the response uses the same annotation-allowlist
|
|
749
|
+
* policy as {@link getSerializeOptions}.
|
|
750
|
+
*/
|
|
751
|
+
async metaForm(name) {
|
|
752
|
+
discoverActions(this.constructor, this.app, this.logger);
|
|
753
|
+
const formType = getControllerFormType(this.constructor, name);
|
|
754
|
+
if (!formType) throw new HttpError(404, `Unknown form "${name}"`);
|
|
755
|
+
let cached = this._formSchemas.get(name);
|
|
756
|
+
if (!cached) {
|
|
757
|
+
cached = serializeAnnotatedType(formType, this.getSerializeOptions());
|
|
758
|
+
this._formSchemas.set(name, cached);
|
|
759
|
+
}
|
|
760
|
+
return cached;
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
643
763
|
* Builds the `/meta` payload. Override in subclasses to populate source-specific
|
|
644
764
|
* fields. Subclasses that fully replace the envelope must call
|
|
645
765
|
* {@link buildActions} and {@link buildCrud} directly so `@DbAction*`
|
|
@@ -693,6 +813,13 @@ __decorate([
|
|
|
693
813
|
__decorateMetadata("design:paramtypes", []),
|
|
694
814
|
__decorateMetadata("design:returntype", Promise)
|
|
695
815
|
], AsReadableController.prototype, "meta", null);
|
|
816
|
+
__decorate([
|
|
817
|
+
Get("meta/form/:name"),
|
|
818
|
+
__decorateParam(0, Param("name")),
|
|
819
|
+
__decorateMetadata("design:type", Function),
|
|
820
|
+
__decorateMetadata("design:paramtypes", [String]),
|
|
821
|
+
__decorateMetadata("design:returntype", Promise)
|
|
822
|
+
], AsReadableController.prototype, "metaForm", null);
|
|
696
823
|
AsReadableController = __decorate([UseValidationErrorTransform(), __decorateMetadata("design:paramtypes", [
|
|
697
824
|
Object,
|
|
698
825
|
String,
|
|
@@ -855,13 +982,6 @@ const QUERY_CONTROLS = [
|
|
|
855
982
|
const PAGES_CONTROLS = ["filter", ...dtoControls(PagesControlsDto)];
|
|
856
983
|
const ONE_CONTROLS = dtoControls(GetOneControlsDto);
|
|
857
984
|
//#endregion
|
|
858
|
-
//#region \0@oxc-project+runtime@0.120.0/helpers/decorateParam.js
|
|
859
|
-
function __decorateParam(paramIndex, decorator) {
|
|
860
|
-
return function(target, key) {
|
|
861
|
-
decorator(target, key, paramIndex);
|
|
862
|
-
};
|
|
863
|
-
}
|
|
864
|
-
//#endregion
|
|
865
985
|
//#region src/as-db-readable.controller.ts
|
|
866
986
|
var _ref$3, _ref2$2;
|
|
867
987
|
let AsDbReadableController = class AsDbReadableController extends AsReadableController {
|
|
@@ -1213,9 +1333,9 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
|
|
|
1213
1333
|
* **GET /one/:id** — retrieves a single record by ID or unique property.
|
|
1214
1334
|
*/
|
|
1215
1335
|
async getOne(id, url) {
|
|
1216
|
-
const parsed = this.
|
|
1336
|
+
const { parsed, hasNonControl } = this.parseControlsOnlyFromUrl(url);
|
|
1337
|
+
if (hasNonControl) return new HttpError(400, "Filtering is not allowed for \"one\" endpoint");
|
|
1217
1338
|
this._coerceActionsControl(parsed.controls);
|
|
1218
|
-
if (Object.keys(parsed.filter).length > 0) return new HttpError(400, "Filtering is not allowed for \"one\" endpoint");
|
|
1219
1339
|
const error = this.validateParsed(parsed, "getOne");
|
|
1220
1340
|
if (error) return error;
|
|
1221
1341
|
const rawSelect = await this.transformProjection(parsed.controls.$select);
|
|
@@ -1230,7 +1350,7 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
|
|
|
1230
1350
|
async getOneComposite(query, url) {
|
|
1231
1351
|
const idObj = this.extractIdShape(query);
|
|
1232
1352
|
if (idObj instanceof HttpError) return idObj;
|
|
1233
|
-
const parsed = this.
|
|
1353
|
+
const { parsed } = this.parseControlsOnlyFromUrl(url);
|
|
1234
1354
|
this._coerceActionsControl(parsed.controls);
|
|
1235
1355
|
const rawSelect = await this.transformProjection(parsed.controls.$select);
|
|
1236
1356
|
const select = this.widenPreferredIdProjection(rawSelect);
|
|
@@ -1846,6 +1966,29 @@ function readCurrentActionMeta(ctx) {
|
|
|
1846
1966
|
return getMoostMate().read(ctrl.constructor, methodName)?.[MOOST_DB_ACTION];
|
|
1847
1967
|
}
|
|
1848
1968
|
//#endregion
|
|
1969
|
+
//#region src/actions/input-form-cache.ts
|
|
1970
|
+
/**
|
|
1971
|
+
* Cached parse of the action request body. Centralises the shape check so
|
|
1972
|
+
* every per-param resolver (`@DbActionID*`, `@DbActionRow*`, `@InputForm`)
|
|
1973
|
+
* reads through the same gate. An array or scalar root is rejected with the
|
|
1974
|
+
* same `ValidatorError` envelope as today's strict-shape ID failures.
|
|
1975
|
+
*/
|
|
1976
|
+
const dbActionBodySlot = cached(async (ctx) => {
|
|
1977
|
+
const raw = await useBody(ctx).parseBody();
|
|
1978
|
+
if (raw == null) return {};
|
|
1979
|
+
if (typeof raw !== "object" || Array.isArray(raw)) throw new ValidatorError([{
|
|
1980
|
+
path: "",
|
|
1981
|
+
message: "Action body must be an object of shape { ids?, input? }"
|
|
1982
|
+
}]);
|
|
1983
|
+
return raw;
|
|
1984
|
+
});
|
|
1985
|
+
/** Cached `body.input` slot — consumed by `@InputForm()` and `useDbActionInput()`. */
|
|
1986
|
+
const dbActionInputSlot = cached(async (ctx) => {
|
|
1987
|
+
return (await ctx.get(dbActionBodySlot)).input;
|
|
1988
|
+
});
|
|
1989
|
+
/** Composable for in-handler reads of the form input. */
|
|
1990
|
+
const useDbActionInput = defineWook((ctx) => ({ load: () => ctx.get(dbActionInputSlot) }));
|
|
1991
|
+
//#endregion
|
|
1849
1992
|
//#region src/actions/id-validation.ts
|
|
1850
1993
|
const SOURCE_CACHE = /* @__PURE__ */ new WeakMap();
|
|
1851
1994
|
function getSourceCache(source) {
|
|
@@ -1963,9 +2106,9 @@ function noTableError(ctx) {
|
|
|
1963
2106
|
async function resolveValidatedId(ctx, validate) {
|
|
1964
2107
|
const table = getActionTable(ctx);
|
|
1965
2108
|
if (!isIdValidationSource(table)) throw noTableError(ctx);
|
|
1966
|
-
const
|
|
1967
|
-
validate(
|
|
1968
|
-
return
|
|
2109
|
+
const env = await ctx.get(dbActionBodySlot);
|
|
2110
|
+
validate(env.ids, table);
|
|
2111
|
+
return env.ids;
|
|
1969
2112
|
}
|
|
1970
2113
|
const dbActionIdSlot = cached((ctx) => resolveValidatedId(ctx, validateSingleId));
|
|
1971
2114
|
const dbActionIdsSlot = cached(async (ctx) => {
|
|
@@ -2324,6 +2467,43 @@ function classLevelActions(dict, forcedLevel) {
|
|
|
2324
2467
|
});
|
|
2325
2468
|
}
|
|
2326
2469
|
//#endregion
|
|
2470
|
+
//#region src/actions/db-action-input-form.decorator.ts
|
|
2471
|
+
/**
|
|
2472
|
+
* Parameter decorator that injects the `input` field of the action request
|
|
2473
|
+
* envelope (`{ ids?, input? }`) into the handler.
|
|
2474
|
+
*
|
|
2475
|
+
* Pairs the resolved value with two pieces of param-level metadata:
|
|
2476
|
+
*
|
|
2477
|
+
* 1. {@link MOOST_DB_ACTION_INPUT_FORM} — the compiled `.as` class plus its
|
|
2478
|
+
* name, consumed by {@link discoverActions} to:
|
|
2479
|
+
* - emit `inputForm: FormType.name` on the action's `/meta` entry, and
|
|
2480
|
+
* - register the type in the controller's form registry so
|
|
2481
|
+
* `GET /meta/form/:name` can serve the serialized schema.
|
|
2482
|
+
* 2. {@link MOOST_ATSCRIPT_TYPE} — just the type ref, providing a generic
|
|
2483
|
+
* hook any atscript-aware Moost pipe can read without knowing about the
|
|
2484
|
+
* moost-db-specific key.
|
|
2485
|
+
*
|
|
2486
|
+
* Validation is intentionally *not* performed here. To validate `input`
|
|
2487
|
+
* against `FormType`, install an atscript validator pipe globally
|
|
2488
|
+
* (`app.applyGlobalPipes(...)`) or scope it via `@Pipe(...)`. The pipe reads
|
|
2489
|
+
* `MOOST_ATSCRIPT_TYPE` off the param and runs `FormType.validator()`.
|
|
2490
|
+
*
|
|
2491
|
+
* Only one `@InputForm()` per action is supported. To collect multiple
|
|
2492
|
+
* structured inputs, compose them into a single `.as` interface and pass an
|
|
2493
|
+
* array form on the field whose user-facing intent is "list of items".
|
|
2494
|
+
*
|
|
2495
|
+
* @param formType A compiled `.as` interface class (carries `.validator()`,
|
|
2496
|
+
* `.metadata`, etc.).
|
|
2497
|
+
*/
|
|
2498
|
+
function InputForm(formType) {
|
|
2499
|
+
const mate = getMoostMate();
|
|
2500
|
+
const meta = {
|
|
2501
|
+
type: formType,
|
|
2502
|
+
name: formType.name
|
|
2503
|
+
};
|
|
2504
|
+
return ApplyDecorators(mate.decorate(MOOST_DB_ACTION_INPUT_FORM, meta), mate.decorate(MOOST_ATSCRIPT_TYPE, formType), Resolve(async () => current().get(dbActionInputSlot), "dbActionInputForm"));
|
|
2505
|
+
}
|
|
2506
|
+
//#endregion
|
|
2327
2507
|
//#region src/actions/per-row.ts
|
|
2328
2508
|
/**
|
|
2329
2509
|
* Lift a per-row predicate into the batch shape required by
|
|
@@ -2339,4 +2519,4 @@ function classLevelActions(dict, forcedLevel) {
|
|
|
2339
2519
|
*/
|
|
2340
2520
|
const perRow = (fn) => (rows) => rows.map(fn);
|
|
2341
2521
|
//#endregion
|
|
2342
|
-
export { ActionDisabledError, AsDbController, AsDbReadableController, AsJsonValueHelpController, AsReadableController, AsValueHelpController, DbAction, DbActionDefault, DbActionID, DbActionIDs, DbActionRow, DbActionRows, DbActions, DbRowActions, DbRowsActions, DbTableActions, ONE_CONTROLS, PAGES_CONTROLS, QUERY_CONTROLS, READABLE_DEF, ReadableController, TABLE_DEF, TableController, UseValidationErrorTransform, ViewController, discoverActions, perRow, useDbActionId, useDbActionIds, useDbActionRow, useDbActionRows, validationErrorTransform };
|
|
2522
|
+
export { ActionDisabledError, AsDbController, AsDbReadableController, AsJsonValueHelpController, AsReadableController, AsValueHelpController, DbAction, DbActionDefault, DbActionID, DbActionIDs, DbActionRow, DbActionRows, DbActions, DbRowActions, DbRowsActions, DbTableActions, InputForm, MOOST_ATSCRIPT_TYPE, MOOST_DB_ACTION_INPUT_FORM, ONE_CONTROLS, PAGES_CONTROLS, QUERY_CONTROLS, READABLE_DEF, ReadableController, TABLE_DEF, TableController, UseValidationErrorTransform, ViewController, dbActionBodySlot, dbActionInputSlot, discoverActions, getControllerFormType, perRow, useDbActionId, useDbActionIds, useDbActionInput, useDbActionRow, useDbActionRows, validationErrorTransform };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atscript/moost-db",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.63",
|
|
4
4
|
"description": "Generic database controller for Moost with Atscript.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"annotations",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"@wooksjs/event-core": "^0.7.10",
|
|
59
59
|
"@wooksjs/http-body": "^0.7.10",
|
|
60
60
|
"moost": "^0.6.8",
|
|
61
|
-
"@atscript/db": "^0.1.
|
|
61
|
+
"@atscript/db": "^0.1.63"
|
|
62
62
|
},
|
|
63
63
|
"scripts": {
|
|
64
64
|
"postinstall": "asc -f dts",
|