@atscript/moost-db 0.1.60 → 0.1.62
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 +190 -39
- package/dist/index.d.cts +141 -12
- package/dist/index.d.mts +141 -12
- package/dist/index.mjs +184 -40
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -199,9 +199,6 @@ function isAsValueHelpControllerSubclass(ctor) {
|
|
|
199
199
|
if (!asValueHelpCtor) return false;
|
|
200
200
|
return asValueHelpCtor.prototype.isPrototypeOf(ctor.prototype);
|
|
201
201
|
}
|
|
202
|
-
function isAsDbReadableControllerInstance(value) {
|
|
203
|
-
return !!asDbReadableCtor && value instanceof asDbReadableCtor;
|
|
204
|
-
}
|
|
205
202
|
//#endregion
|
|
206
203
|
//#region src/actions/keys.ts
|
|
207
204
|
/** Log-message prefix for warnings emitted from the actions subsystem. */
|
|
@@ -216,6 +213,21 @@ const MOOST_DB_ACTION_PARAM = "atscript_db_action_param";
|
|
|
216
213
|
const MOOST_DB_ACTION_ROW = "atscript_db_action_row";
|
|
217
214
|
const MOOST_DB_ACTION_ROWS = "atscript_db_action_rows";
|
|
218
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
|
+
/**
|
|
219
231
|
* Shared method-decorator update used by `@DbAction` and `@DbActionDefault`:
|
|
220
232
|
* read the existing `MOOST_DB_ACTION` slot, merge the patch (later-applied
|
|
221
233
|
* fields win), and write it back. `name` is empty until `@DbAction` provides
|
|
@@ -238,6 +250,8 @@ function scanParamLevel(params) {
|
|
|
238
250
|
let multi = false;
|
|
239
251
|
let hasRowParam = false;
|
|
240
252
|
let hasBody = false;
|
|
253
|
+
let inputForm;
|
|
254
|
+
let hasDuplicateInputForm = false;
|
|
241
255
|
for (const p of params) {
|
|
242
256
|
const kind = p[MOOST_DB_ACTION_PARAM];
|
|
243
257
|
if (kind === "id") single = true;
|
|
@@ -251,13 +265,18 @@ function scanParamLevel(params) {
|
|
|
251
265
|
hasRowParam = true;
|
|
252
266
|
}
|
|
253
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;
|
|
254
271
|
}
|
|
255
272
|
return {
|
|
256
273
|
level: single && multi ? "table" : single ? "row" : multi ? "rows" : "table",
|
|
257
274
|
single,
|
|
258
275
|
multi,
|
|
259
276
|
hasRowParam,
|
|
260
|
-
hasBody
|
|
277
|
+
hasBody,
|
|
278
|
+
inputForm,
|
|
279
|
+
hasDuplicateInputForm
|
|
261
280
|
};
|
|
262
281
|
}
|
|
263
282
|
//#endregion
|
|
@@ -274,6 +293,34 @@ const OPTIONAL_FIELDS = [
|
|
|
274
293
|
];
|
|
275
294
|
const actionsCache = /* @__PURE__ */ new WeakMap();
|
|
276
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
|
+
}
|
|
277
324
|
/** Discover actions on a controller, memoized per ctor. `info`-only callers map `e => e.info`. */
|
|
278
325
|
function discoverActions(controllerCtor, app, logger) {
|
|
279
326
|
const cached = actionsCache.get(controllerCtor);
|
|
@@ -359,6 +406,10 @@ function collectMethodActions(ctor, overview, logger, out, seen) {
|
|
|
359
406
|
processor: "backend",
|
|
360
407
|
value: path
|
|
361
408
|
};
|
|
409
|
+
if (levelInfer.inputForm) {
|
|
410
|
+
if (!registerFormType(ctor, levelInfer.inputForm, action.name, logger)) continue;
|
|
411
|
+
info.inputForm = levelInfer.inputForm.name;
|
|
412
|
+
}
|
|
362
413
|
emitInfo(info, action.opts);
|
|
363
414
|
seen.add(action.name);
|
|
364
415
|
out.push({
|
|
@@ -373,10 +424,12 @@ function inferMethodLevel(params, actionName, logger) {
|
|
|
373
424
|
logger.warn(`${WARN_PREFIX} action "${actionName}" mixes single-cardinality and multi-cardinality decorators (@DbActionID / @DbActionRow vs @DbActionIDs / @DbActionRows) — dropping`);
|
|
374
425
|
return null;
|
|
375
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.`);
|
|
376
428
|
return {
|
|
377
429
|
level: scan.level,
|
|
378
430
|
bodyConflict: scan.hasBody && scan.level !== "table",
|
|
379
|
-
hasRowParam: scan.hasRowParam
|
|
431
|
+
hasRowParam: scan.hasRowParam,
|
|
432
|
+
inputForm: scan.inputForm
|
|
380
433
|
};
|
|
381
434
|
}
|
|
382
435
|
function collectClassActions(ctor, logger, out, seen) {
|
|
@@ -487,6 +540,13 @@ function __decorate(decorators, target, key, desc) {
|
|
|
487
540
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
488
541
|
}
|
|
489
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
|
|
490
550
|
//#region src/as-readable.controller.ts
|
|
491
551
|
var _ref$4;
|
|
492
552
|
let AsReadableController = class AsReadableController {
|
|
@@ -502,6 +562,8 @@ let AsReadableController = class AsReadableController {
|
|
|
502
562
|
_serializedType;
|
|
503
563
|
/** Cached full meta response (computed lazily on first meta() call). */
|
|
504
564
|
_metaResponse;
|
|
565
|
+
/** Cached serialized form schemas keyed by `FormType.name` — populated lazily by {@link metaForm}. */
|
|
566
|
+
_formSchemas = /* @__PURE__ */ new Map();
|
|
505
567
|
constructor(boundType, controllerName, app, kindTag = "readable") {
|
|
506
568
|
this.boundType = boundType;
|
|
507
569
|
this.controllerName = controllerName;
|
|
@@ -644,6 +706,25 @@ let AsReadableController = class AsReadableController {
|
|
|
644
706
|
return this.applyMetaOverlay(this._metaResponse);
|
|
645
707
|
}
|
|
646
708
|
/**
|
|
709
|
+
* **GET /meta/form/:name** — returns the serialized schema of a form
|
|
710
|
+
* referenced by an action's `inputForm` field. The form name is the
|
|
711
|
+
* compiled `.as` class's `.name`, registered when an action's parameter is
|
|
712
|
+
* decorated with `@InputForm(FormType)`. Schemas are serialized once and
|
|
713
|
+
* cached per controller; the response uses the same annotation-allowlist
|
|
714
|
+
* policy as {@link getSerializeOptions}.
|
|
715
|
+
*/
|
|
716
|
+
async metaForm(name) {
|
|
717
|
+
discoverActions(this.constructor, this.app, this.logger);
|
|
718
|
+
const formType = getControllerFormType(this.constructor, name);
|
|
719
|
+
if (!formType) throw new _moostjs_event_http.HttpError(404, `Unknown form "${name}"`);
|
|
720
|
+
let cached = this._formSchemas.get(name);
|
|
721
|
+
if (!cached) {
|
|
722
|
+
cached = (0, _atscript_typescript_utils.serializeAnnotatedType)(formType, this.getSerializeOptions());
|
|
723
|
+
this._formSchemas.set(name, cached);
|
|
724
|
+
}
|
|
725
|
+
return cached;
|
|
726
|
+
}
|
|
727
|
+
/**
|
|
647
728
|
* Builds the `/meta` payload. Override in subclasses to populate source-specific
|
|
648
729
|
* fields. Subclasses that fully replace the envelope must call
|
|
649
730
|
* {@link buildActions} and {@link buildCrud} directly so `@DbAction*`
|
|
@@ -697,6 +778,13 @@ __decorate([
|
|
|
697
778
|
__decorateMetadata("design:paramtypes", []),
|
|
698
779
|
__decorateMetadata("design:returntype", Promise)
|
|
699
780
|
], AsReadableController.prototype, "meta", null);
|
|
781
|
+
__decorate([
|
|
782
|
+
(0, _moostjs_event_http.Get)("meta/form/:name"),
|
|
783
|
+
__decorateParam(0, (0, moost.Param)("name")),
|
|
784
|
+
__decorateMetadata("design:type", Function),
|
|
785
|
+
__decorateMetadata("design:paramtypes", [String]),
|
|
786
|
+
__decorateMetadata("design:returntype", Promise)
|
|
787
|
+
], AsReadableController.prototype, "metaForm", null);
|
|
700
788
|
AsReadableController = __decorate([UseValidationErrorTransform(), __decorateMetadata("design:paramtypes", [
|
|
701
789
|
Object,
|
|
702
790
|
String,
|
|
@@ -859,13 +947,6 @@ const QUERY_CONTROLS = [
|
|
|
859
947
|
const PAGES_CONTROLS = ["filter", ...dtoControls(PagesControlsDto)];
|
|
860
948
|
const ONE_CONTROLS = dtoControls(GetOneControlsDto);
|
|
861
949
|
//#endregion
|
|
862
|
-
//#region \0@oxc-project+runtime@0.120.0/helpers/decorateParam.js
|
|
863
|
-
function __decorateParam(paramIndex, decorator) {
|
|
864
|
-
return function(target, key) {
|
|
865
|
-
decorator(target, key, paramIndex);
|
|
866
|
-
};
|
|
867
|
-
}
|
|
868
|
-
//#endregion
|
|
869
950
|
//#region src/as-db-readable.controller.ts
|
|
870
951
|
var _ref$3, _ref2$2;
|
|
871
952
|
let AsDbReadableController = class AsDbReadableController extends AsReadableController {
|
|
@@ -873,14 +954,12 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
|
|
|
873
954
|
readable;
|
|
874
955
|
_gates;
|
|
875
956
|
_preferredIdSet;
|
|
876
|
-
_compositeIdShapes;
|
|
877
957
|
_overlayIsNoOp;
|
|
878
958
|
constructor(readable, app) {
|
|
879
959
|
super(readable.type, readable.tableName, app, readable.isView ? "view" : "table");
|
|
880
960
|
this.readable = readable;
|
|
881
961
|
this._gates = this._buildGates();
|
|
882
962
|
this._preferredIdSet = new Set(readable.preferredId ?? []);
|
|
883
|
-
this._compositeIdShapes = (readable.identifications ?? []).filter((id) => id.fields.length >= 2);
|
|
884
963
|
const defaultOverlay = AsReadableController.prototype.applyMetaOverlay;
|
|
885
964
|
this._overlayIsNoOp = this.applyMetaOverlay === defaultOverlay;
|
|
886
965
|
}
|
|
@@ -1104,12 +1183,9 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
|
|
|
1104
1183
|
});
|
|
1105
1184
|
return result;
|
|
1106
1185
|
}
|
|
1107
|
-
/**
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
*/
|
|
1111
|
-
extractCompositeId(query) {
|
|
1112
|
-
for (const id of this._compositeIdShapes) {
|
|
1186
|
+
/** Pick the first identification (PK or unique index) whose fields are all present in the query. */
|
|
1187
|
+
extractIdShape(query) {
|
|
1188
|
+
for (const id of this.readable.identifications) {
|
|
1113
1189
|
const idObj = {};
|
|
1114
1190
|
let allPresent = true;
|
|
1115
1191
|
for (const field of id.fields) {
|
|
@@ -1121,7 +1197,7 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
|
|
|
1121
1197
|
}
|
|
1122
1198
|
if (allPresent) return idObj;
|
|
1123
1199
|
}
|
|
1124
|
-
return new _moostjs_event_http.HttpError(400, "Query params do not match any
|
|
1200
|
+
return new _moostjs_event_http.HttpError(400, "Query params do not match any primary key or unique index");
|
|
1125
1201
|
}
|
|
1126
1202
|
/**
|
|
1127
1203
|
* **GET /query** — returns an array of records or a count.
|
|
@@ -1237,7 +1313,7 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
|
|
|
1237
1313
|
* (composite primary key or compound unique index).
|
|
1238
1314
|
*/
|
|
1239
1315
|
async getOneComposite(query, url) {
|
|
1240
|
-
const idObj = this.
|
|
1316
|
+
const idObj = this.extractIdShape(query);
|
|
1241
1317
|
if (idObj instanceof _moostjs_event_http.HttpError) return idObj;
|
|
1242
1318
|
const parsed = this.parseQueryString(url);
|
|
1243
1319
|
this._coerceActionsControl(parsed.controls);
|
|
@@ -1437,7 +1513,7 @@ let AsDbController = class AsDbController extends AsDbReadableController {
|
|
|
1437
1513
|
* (composite primary key or compound unique index).
|
|
1438
1514
|
*/
|
|
1439
1515
|
async removeComposite(query) {
|
|
1440
|
-
const idObj = this.
|
|
1516
|
+
const idObj = this.extractIdShape(query);
|
|
1441
1517
|
if (idObj instanceof _moostjs_event_http.HttpError) return idObj;
|
|
1442
1518
|
const resolvedId = await this.onRemove(idObj);
|
|
1443
1519
|
if (resolvedId === void 0) return new _moostjs_event_http.HttpError(500, "Not deleted");
|
|
@@ -1855,12 +1931,35 @@ function readCurrentActionMeta(ctx) {
|
|
|
1855
1931
|
return (0, moost.getMoostMate)().read(ctrl.constructor, methodName)?.[MOOST_DB_ACTION];
|
|
1856
1932
|
}
|
|
1857
1933
|
//#endregion
|
|
1934
|
+
//#region src/actions/input-form-cache.ts
|
|
1935
|
+
/**
|
|
1936
|
+
* Cached parse of the action request body. Centralises the shape check so
|
|
1937
|
+
* every per-param resolver (`@DbActionID*`, `@DbActionRow*`, `@InputForm`)
|
|
1938
|
+
* reads through the same gate. An array or scalar root is rejected with the
|
|
1939
|
+
* same `ValidatorError` envelope as today's strict-shape ID failures.
|
|
1940
|
+
*/
|
|
1941
|
+
const dbActionBodySlot = (0, _wooksjs_event_core.cached)(async (ctx) => {
|
|
1942
|
+
const raw = await (0, _wooksjs_http_body.useBody)(ctx).parseBody();
|
|
1943
|
+
if (raw == null) return {};
|
|
1944
|
+
if (typeof raw !== "object" || Array.isArray(raw)) throw new _atscript_typescript_utils.ValidatorError([{
|
|
1945
|
+
path: "",
|
|
1946
|
+
message: "Action body must be an object of shape { ids?, input? }"
|
|
1947
|
+
}]);
|
|
1948
|
+
return raw;
|
|
1949
|
+
});
|
|
1950
|
+
/** Cached `body.input` slot — consumed by `@InputForm()` and `useDbActionInput()`. */
|
|
1951
|
+
const dbActionInputSlot = (0, _wooksjs_event_core.cached)(async (ctx) => {
|
|
1952
|
+
return (await ctx.get(dbActionBodySlot)).input;
|
|
1953
|
+
});
|
|
1954
|
+
/** Composable for in-handler reads of the form input. */
|
|
1955
|
+
const useDbActionInput = (0, _wooksjs_event_core.defineWook)((ctx) => ({ load: () => ctx.get(dbActionInputSlot) }));
|
|
1956
|
+
//#endregion
|
|
1858
1957
|
//#region src/actions/id-validation.ts
|
|
1859
1958
|
const SOURCE_CACHE = /* @__PURE__ */ new WeakMap();
|
|
1860
1959
|
function getSourceCache(source) {
|
|
1861
1960
|
let cache = SOURCE_CACHE.get(source);
|
|
1862
1961
|
if (cache) return cache;
|
|
1863
|
-
const identifications = source.
|
|
1962
|
+
const identifications = source.identifications;
|
|
1864
1963
|
const byKeySig = /* @__PURE__ */ new Map();
|
|
1865
1964
|
for (const ident of identifications) byKeySig.set(fieldsSig(ident.fields), ident);
|
|
1866
1965
|
const fieldByName = /* @__PURE__ */ new Map();
|
|
@@ -1879,7 +1978,7 @@ function fieldsSig(fields) {
|
|
|
1879
1978
|
function isIdValidationSource(value) {
|
|
1880
1979
|
if (!value || typeof value !== "object") return false;
|
|
1881
1980
|
const v = value;
|
|
1882
|
-
return
|
|
1981
|
+
return Array.isArray(v.identifications) && Array.isArray(v.fieldDescriptors);
|
|
1883
1982
|
}
|
|
1884
1983
|
function validateSingleId(body, source, path = "") {
|
|
1885
1984
|
const errors = collectIdErrors(body, source, path);
|
|
@@ -1947,22 +2046,34 @@ function isPlainObject(value) {
|
|
|
1947
2046
|
//#endregion
|
|
1948
2047
|
//#region src/actions/id-cache.ts
|
|
1949
2048
|
const boundTableKey = (0, _wooksjs_event_core.key)("atscript_db_action_bound_table");
|
|
1950
|
-
function
|
|
1951
|
-
const fromSlot = ctx.has(boundTableKey) ? ctx.get(boundTableKey) : void 0;
|
|
1952
|
-
if (fromSlot) return fromSlot;
|
|
2049
|
+
function controllerTable(ctx) {
|
|
1953
2050
|
const ctrl = (0, moost.useControllerContext)(ctx).getController();
|
|
1954
2051
|
return ctrl?.readable ?? ctrl?.table ?? null;
|
|
1955
2052
|
}
|
|
2053
|
+
function getActionTable(ctx) {
|
|
2054
|
+
return (ctx.has(boundTableKey) ? ctx.get(boundTableKey) : void 0) ?? controllerTable(ctx);
|
|
2055
|
+
}
|
|
2056
|
+
const warnedTags = /* @__PURE__ */ new Set();
|
|
1956
2057
|
function noTableError(ctx) {
|
|
1957
2058
|
const actionName = readCurrentActionMeta(ctx)?.name;
|
|
1958
|
-
|
|
2059
|
+
const tag = actionName ? `"${actionName}"` : "<unknown>";
|
|
2060
|
+
if (!warnedTags.has(tag)) {
|
|
2061
|
+
warnedTags.add(tag);
|
|
2062
|
+
console.warn(`${WARN_PREFIX} ${tag}: 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.`);
|
|
2063
|
+
}
|
|
2064
|
+
return new _moostjs_event_http.HttpError(500, {
|
|
2065
|
+
statusCode: 500,
|
|
2066
|
+
error: "Internal Server Error",
|
|
2067
|
+
message: "Internal server error",
|
|
2068
|
+
code: "ACTION_TABLE_NOT_BOUND"
|
|
2069
|
+
});
|
|
1959
2070
|
}
|
|
1960
2071
|
async function resolveValidatedId(ctx, validate) {
|
|
1961
2072
|
const table = getActionTable(ctx);
|
|
1962
2073
|
if (!isIdValidationSource(table)) throw noTableError(ctx);
|
|
1963
|
-
const
|
|
1964
|
-
validate(
|
|
1965
|
-
return
|
|
2074
|
+
const env = await ctx.get(dbActionBodySlot);
|
|
2075
|
+
validate(env.ids, table);
|
|
2076
|
+
return env.ids;
|
|
1966
2077
|
}
|
|
1967
2078
|
const dbActionIdSlot = (0, _wooksjs_event_core.cached)((ctx) => resolveValidatedId(ctx, validateSingleId));
|
|
1968
2079
|
const dbActionIdsSlot = (0, _wooksjs_event_core.cached)(async (ctx) => {
|
|
@@ -2062,15 +2173,11 @@ const useDbActionRows = (0, _wooksjs_event_core.defineWook)((ctx) => ({ load: ()
|
|
|
2062
2173
|
//#endregion
|
|
2063
2174
|
//#region src/actions/gate-interceptor.ts
|
|
2064
2175
|
const GATE_PRIORITY = moost.TInterceptorPriority.AFTER_GUARD;
|
|
2065
|
-
function injectBoundTable(
|
|
2176
|
+
function injectBoundTable(fallback) {
|
|
2066
2177
|
const ctx = (0, _wooksjs_event_core.current)();
|
|
2067
2178
|
if (ctx.has(boundTableKey)) return;
|
|
2068
|
-
const
|
|
2069
|
-
if (
|
|
2070
|
-
ctx.set(boundTableKey, controller.readable);
|
|
2071
|
-
return;
|
|
2072
|
-
}
|
|
2073
|
-
if (table != null) ctx.set(boundTableKey, table);
|
|
2179
|
+
const t = controllerTable(ctx) ?? fallback;
|
|
2180
|
+
if (t != null) ctx.set(boundTableKey, t);
|
|
2074
2181
|
}
|
|
2075
2182
|
function buildGateInterceptor(opts) {
|
|
2076
2183
|
const { action, level, disabled, onDisabledRows, table } = opts;
|
|
@@ -2325,6 +2432,43 @@ function classLevelActions(dict, forcedLevel) {
|
|
|
2325
2432
|
});
|
|
2326
2433
|
}
|
|
2327
2434
|
//#endregion
|
|
2435
|
+
//#region src/actions/db-action-input-form.decorator.ts
|
|
2436
|
+
/**
|
|
2437
|
+
* Parameter decorator that injects the `input` field of the action request
|
|
2438
|
+
* envelope (`{ ids?, input? }`) into the handler.
|
|
2439
|
+
*
|
|
2440
|
+
* Pairs the resolved value with two pieces of param-level metadata:
|
|
2441
|
+
*
|
|
2442
|
+
* 1. {@link MOOST_DB_ACTION_INPUT_FORM} — the compiled `.as` class plus its
|
|
2443
|
+
* name, consumed by {@link discoverActions} to:
|
|
2444
|
+
* - emit `inputForm: FormType.name` on the action's `/meta` entry, and
|
|
2445
|
+
* - register the type in the controller's form registry so
|
|
2446
|
+
* `GET /meta/form/:name` can serve the serialized schema.
|
|
2447
|
+
* 2. {@link MOOST_ATSCRIPT_TYPE} — just the type ref, providing a generic
|
|
2448
|
+
* hook any atscript-aware Moost pipe can read without knowing about the
|
|
2449
|
+
* moost-db-specific key.
|
|
2450
|
+
*
|
|
2451
|
+
* Validation is intentionally *not* performed here. To validate `input`
|
|
2452
|
+
* against `FormType`, install an atscript validator pipe globally
|
|
2453
|
+
* (`app.applyGlobalPipes(...)`) or scope it via `@Pipe(...)`. The pipe reads
|
|
2454
|
+
* `MOOST_ATSCRIPT_TYPE` off the param and runs `FormType.validator()`.
|
|
2455
|
+
*
|
|
2456
|
+
* Only one `@InputForm()` per action is supported. To collect multiple
|
|
2457
|
+
* structured inputs, compose them into a single `.as` interface and pass an
|
|
2458
|
+
* array form on the field whose user-facing intent is "list of items".
|
|
2459
|
+
*
|
|
2460
|
+
* @param formType A compiled `.as` interface class (carries `.validator()`,
|
|
2461
|
+
* `.metadata`, etc.).
|
|
2462
|
+
*/
|
|
2463
|
+
function InputForm(formType) {
|
|
2464
|
+
const mate = (0, moost.getMoostMate)();
|
|
2465
|
+
const meta = {
|
|
2466
|
+
type: formType,
|
|
2467
|
+
name: formType.name
|
|
2468
|
+
};
|
|
2469
|
+
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"));
|
|
2470
|
+
}
|
|
2471
|
+
//#endregion
|
|
2328
2472
|
//#region src/actions/per-row.ts
|
|
2329
2473
|
/**
|
|
2330
2474
|
* Lift a per-row predicate into the batch shape required by
|
|
@@ -2381,6 +2525,9 @@ exports.DbActions = DbActions;
|
|
|
2381
2525
|
exports.DbRowActions = DbRowActions;
|
|
2382
2526
|
exports.DbRowsActions = DbRowsActions;
|
|
2383
2527
|
exports.DbTableActions = DbTableActions;
|
|
2528
|
+
exports.InputForm = InputForm;
|
|
2529
|
+
exports.MOOST_ATSCRIPT_TYPE = MOOST_ATSCRIPT_TYPE;
|
|
2530
|
+
exports.MOOST_DB_ACTION_INPUT_FORM = MOOST_DB_ACTION_INPUT_FORM;
|
|
2384
2531
|
exports.ONE_CONTROLS = ONE_CONTROLS;
|
|
2385
2532
|
exports.PAGES_CONTROLS = PAGES_CONTROLS;
|
|
2386
2533
|
exports.QUERY_CONTROLS = QUERY_CONTROLS;
|
|
@@ -2390,10 +2537,14 @@ exports.TABLE_DEF = TABLE_DEF;
|
|
|
2390
2537
|
exports.TableController = TableController;
|
|
2391
2538
|
exports.UseValidationErrorTransform = UseValidationErrorTransform;
|
|
2392
2539
|
exports.ViewController = ViewController;
|
|
2540
|
+
exports.dbActionBodySlot = dbActionBodySlot;
|
|
2541
|
+
exports.dbActionInputSlot = dbActionInputSlot;
|
|
2393
2542
|
exports.discoverActions = discoverActions;
|
|
2543
|
+
exports.getControllerFormType = getControllerFormType;
|
|
2394
2544
|
exports.perRow = perRow;
|
|
2395
2545
|
exports.useDbActionId = useDbActionId;
|
|
2396
2546
|
exports.useDbActionIds = useDbActionIds;
|
|
2547
|
+
exports.useDbActionInput = useDbActionInput;
|
|
2397
2548
|
exports.useDbActionRow = useDbActionRow;
|
|
2398
2549
|
exports.useDbActionRows = useDbActionRows;
|
|
2399
2550
|
exports.validationErrorTransform = validationErrorTransform;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
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 { TAtscriptAnnotatedType, TAtscriptDataType, TSerializeOptions, TSerializedAnnotatedType, Validator } from "@atscript/typescript/utils";
|
|
4
3
|
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
4
|
import { HttpError } from "@moostjs/event-http";
|
|
6
5
|
import * as moost from "moost";
|
|
@@ -61,13 +60,15 @@ declare abstract class AsReadableController<T extends TAtscriptAnnotatedType = T
|
|
|
61
60
|
private _serializedType?;
|
|
62
61
|
/** Cached full meta response (computed lazily on first meta() call). */
|
|
63
62
|
private _metaResponse?;
|
|
63
|
+
/** Cached serialized form schemas keyed by `FormType.name` — populated lazily by {@link metaForm}. */
|
|
64
|
+
private _formSchemas;
|
|
64
65
|
constructor(boundType: T, controllerName: string, app: Moost, kindTag?: string);
|
|
65
66
|
/** Subclass contract: return `true` if `path` addresses a valid field on the bound source. */
|
|
66
67
|
protected abstract hasField(path: string): boolean;
|
|
67
68
|
/** Sets @db.http.path on the type metadata from the controller's computed prefix. */
|
|
68
69
|
private _resolveHttpPath;
|
|
69
70
|
/** Lazily serializes the bound type (after all controllers have set @db.http.path). */
|
|
70
|
-
protected getSerializedType():
|
|
71
|
+
protected getSerializedType(): TSerializedAnnotatedType;
|
|
71
72
|
/**
|
|
72
73
|
* One-time initialization hook. Override to seed data, register watchers, etc.
|
|
73
74
|
*/
|
|
@@ -112,6 +113,15 @@ declare abstract class AsReadableController<T extends TAtscriptAnnotatedType = T
|
|
|
112
113
|
* subclasses can prune the response by principal.
|
|
113
114
|
*/
|
|
114
115
|
meta(): Promise<TMetaResponse>;
|
|
116
|
+
/**
|
|
117
|
+
* **GET /meta/form/:name** — returns the serialized schema of a form
|
|
118
|
+
* referenced by an action's `inputForm` field. The form name is the
|
|
119
|
+
* compiled `.as` class's `.name`, registered when an action's parameter is
|
|
120
|
+
* decorated with `@InputForm(FormType)`. Schemas are serialized once and
|
|
121
|
+
* cached per controller; the response uses the same annotation-allowlist
|
|
122
|
+
* policy as {@link getSerializeOptions}.
|
|
123
|
+
*/
|
|
124
|
+
metaForm(name: string): Promise<TSerializedAnnotatedType>;
|
|
115
125
|
/**
|
|
116
126
|
* Builds the `/meta` payload. Override in subclasses to populate source-specific
|
|
117
127
|
* fields. Subclasses that fully replace the envelope must call
|
|
@@ -155,7 +165,6 @@ declare class AsDbReadableController<T extends TAtscriptAnnotatedType = TAtscrip
|
|
|
155
165
|
protected readable: AtscriptDbReadable<T>;
|
|
156
166
|
private readonly _gates;
|
|
157
167
|
private readonly _preferredIdSet;
|
|
158
|
-
private readonly _compositeIdShapes;
|
|
159
168
|
private readonly _overlayIsNoOp;
|
|
160
169
|
constructor(readable: AtscriptDbReadable<T>, app: Moost);
|
|
161
170
|
private _buildGates;
|
|
@@ -200,11 +209,8 @@ declare class AsDbReadableController<T extends TAtscriptAnnotatedType = TAtscrip
|
|
|
200
209
|
* family (count vs no-count).
|
|
201
210
|
*/
|
|
202
211
|
private _runReadWithActions;
|
|
203
|
-
/**
|
|
204
|
-
|
|
205
|
-
* Tries composite primary key first, then compound unique indexes.
|
|
206
|
-
*/
|
|
207
|
-
protected extractCompositeId(query: Record<string, string>): Record<string, unknown> | HttpError;
|
|
212
|
+
/** Pick the first identification (PK or unique index) whose fields are all present in the query. */
|
|
213
|
+
protected extractIdShape(query: Record<string, string>): Record<string, unknown> | HttpError;
|
|
208
214
|
/**
|
|
209
215
|
* **GET /query** — returns an array of records or a count.
|
|
210
216
|
*/
|
|
@@ -720,6 +726,38 @@ declare function DbRowActions<TRow = unknown, const D extends Record<string, unk
|
|
|
720
726
|
/** Sugar for `@DbActions` with `level: 'rows'` injected into each entry. */
|
|
721
727
|
declare function DbRowsActions<TRow = unknown, const D extends Record<string, unknown> = {}>(dict: D & ValidatedUnpinnedDict<TRow, D>): ClassDecorator;
|
|
722
728
|
//#endregion
|
|
729
|
+
//#region src/actions/db-action-input-form.decorator.d.ts
|
|
730
|
+
/**
|
|
731
|
+
* Parameter decorator that injects the `input` field of the action request
|
|
732
|
+
* envelope (`{ ids?, input? }`) into the handler.
|
|
733
|
+
*
|
|
734
|
+
* Pairs the resolved value with two pieces of param-level metadata:
|
|
735
|
+
*
|
|
736
|
+
* 1. {@link MOOST_DB_ACTION_INPUT_FORM} — the compiled `.as` class plus its
|
|
737
|
+
* name, consumed by {@link discoverActions} to:
|
|
738
|
+
* - emit `inputForm: FormType.name` on the action's `/meta` entry, and
|
|
739
|
+
* - register the type in the controller's form registry so
|
|
740
|
+
* `GET /meta/form/:name` can serve the serialized schema.
|
|
741
|
+
* 2. {@link MOOST_ATSCRIPT_TYPE} — just the type ref, providing a generic
|
|
742
|
+
* hook any atscript-aware Moost pipe can read without knowing about the
|
|
743
|
+
* moost-db-specific key.
|
|
744
|
+
*
|
|
745
|
+
* Validation is intentionally *not* performed here. To validate `input`
|
|
746
|
+
* against `FormType`, install an atscript validator pipe globally
|
|
747
|
+
* (`app.applyGlobalPipes(...)`) or scope it via `@Pipe(...)`. The pipe reads
|
|
748
|
+
* `MOOST_ATSCRIPT_TYPE` off the param and runs `FormType.validator()`.
|
|
749
|
+
*
|
|
750
|
+
* Only one `@InputForm()` per action is supported. To collect multiple
|
|
751
|
+
* structured inputs, compose them into a single `.as` interface and pass an
|
|
752
|
+
* array form on the field whose user-facing intent is "list of items".
|
|
753
|
+
*
|
|
754
|
+
* @param formType A compiled `.as` interface class (carries `.validator()`,
|
|
755
|
+
* `.metadata`, etc.).
|
|
756
|
+
*/
|
|
757
|
+
declare function InputForm<T extends TAtscriptAnnotatedType & {
|
|
758
|
+
readonly name: string;
|
|
759
|
+
}>(formType: T): ParameterDecorator;
|
|
760
|
+
//#endregion
|
|
723
761
|
//#region src/actions/discover.d.ts
|
|
724
762
|
/**
|
|
725
763
|
* Pairs the wire-shaped `info` with the original decorator opts / dict entry,
|
|
@@ -730,13 +768,16 @@ interface TDbActionEnvelope {
|
|
|
730
768
|
info: TDbActionInfo$1;
|
|
731
769
|
raw: DbActionOpts | TDbActionsEntry;
|
|
732
770
|
}
|
|
771
|
+
/** Lookup helper for `AsReadableController.metaForm()`. */
|
|
772
|
+
declare function getControllerFormType(ctor: Function, name: string): TAtscriptAnnotatedType | undefined;
|
|
733
773
|
/** Discover actions on a controller, memoized per ctor. `info`-only callers map `e => e.info`. */
|
|
734
774
|
declare function discoverActions(controllerCtor: Function, app: Moost, logger: TConsoleBase): TDbActionEnvelope[];
|
|
735
775
|
//#endregion
|
|
736
776
|
//#region src/actions/id-validation.d.ts
|
|
777
|
+
/** Duck-typed shape; matches `AtscriptDbReadable`'s public surface. */
|
|
737
778
|
interface IdValidationSource {
|
|
738
|
-
|
|
739
|
-
fieldDescriptors: readonly TDbFieldMeta[];
|
|
779
|
+
readonly identifications: readonly TIdentification[];
|
|
780
|
+
readonly fieldDescriptors: readonly TDbFieldMeta[];
|
|
740
781
|
}
|
|
741
782
|
//#endregion
|
|
742
783
|
//#region src/actions/id-cache.d.ts
|
|
@@ -755,6 +796,94 @@ declare const useDbActionRows: _wooksjs_event_core0.WookComposable<{
|
|
|
755
796
|
load: () => Promise<(Record<string, unknown> | undefined)[]>;
|
|
756
797
|
}>;
|
|
757
798
|
//#endregion
|
|
799
|
+
//#region src/actions/input-form-cache.d.ts
|
|
800
|
+
/**
|
|
801
|
+
* Wire-shape of an action request body. Both fields are optional:
|
|
802
|
+
*
|
|
803
|
+
* - `ids` — what previously sat at the body root: a single identifier object
|
|
804
|
+
* (`'row'`-level), an array of identifier objects (`'rows'`-level), or
|
|
805
|
+
* absent (`'table'`-level).
|
|
806
|
+
* - `input` — present only when the action declares an `@InputForm()`
|
|
807
|
+
* parameter; carries the form payload the user filled out.
|
|
808
|
+
*/
|
|
809
|
+
interface DbActionEnvelope {
|
|
810
|
+
ids?: unknown;
|
|
811
|
+
input?: unknown;
|
|
812
|
+
}
|
|
813
|
+
/**
|
|
814
|
+
* Cached parse of the action request body. Centralises the shape check so
|
|
815
|
+
* every per-param resolver (`@DbActionID*`, `@DbActionRow*`, `@InputForm`)
|
|
816
|
+
* reads through the same gate. An array or scalar root is rejected with the
|
|
817
|
+
* same `ValidatorError` envelope as today's strict-shape ID failures.
|
|
818
|
+
*/
|
|
819
|
+
declare const dbActionBodySlot: moost.Cached<Promise<DbActionEnvelope>>;
|
|
820
|
+
/** Cached `body.input` slot — consumed by `@InputForm()` and `useDbActionInput()`. */
|
|
821
|
+
declare const dbActionInputSlot: moost.Cached<Promise<unknown>>;
|
|
822
|
+
/** Composable for in-handler reads of the form input. */
|
|
823
|
+
declare const useDbActionInput: _wooksjs_event_core0.WookComposable<{
|
|
824
|
+
load: () => Promise<unknown>;
|
|
825
|
+
}>;
|
|
826
|
+
//#endregion
|
|
827
|
+
//#region src/actions/keys.d.ts
|
|
828
|
+
/** Method-level metadata key — written by `@DbAction(name, opts)`. */
|
|
829
|
+
declare const MOOST_DB_ACTION = "atscript_db_action";
|
|
830
|
+
/** Class-level metadata key — written by `@DbActions` and the level-pinned shortcuts. Stored as an array; decorators accumulate. */
|
|
831
|
+
declare const MOOST_DB_ACTIONS = "atscript_db_actions";
|
|
832
|
+
/** Param-level metadata key — written by `@DbActionID()` / `@DbActionIDs()`. Drives level inference. */
|
|
833
|
+
declare const MOOST_DB_ACTION_PARAM = "atscript_db_action_param";
|
|
834
|
+
/** Param-level marker keys — written by `@DbActionRow()` / `@DbActionRows()`. */
|
|
835
|
+
declare const MOOST_DB_ACTION_ROW = "atscript_db_action_row";
|
|
836
|
+
declare const MOOST_DB_ACTION_ROWS = "atscript_db_action_rows";
|
|
837
|
+
/**
|
|
838
|
+
* Param-level metadata key — written by `@InputForm(FormType)`. Carries the
|
|
839
|
+
* compiled `.as` class plus its `.name` so {@link discoverActions} can both
|
|
840
|
+
* emit `inputForm` on `/meta` and register the type in the controller's form
|
|
841
|
+
* registry for `GET /meta/form/:name`.
|
|
842
|
+
*/
|
|
843
|
+
declare const MOOST_DB_ACTION_INPUT_FORM = "atscript_db_action_input_form";
|
|
844
|
+
/**
|
|
845
|
+
* Generic param-level metadata key — written by `@InputForm(FormType)`
|
|
846
|
+
* alongside {@link MOOST_DB_ACTION_INPUT_FORM}. Holds just the type ref so a
|
|
847
|
+
* generic atscript-aware Moost pipe (installed globally via
|
|
848
|
+
* `app.applyGlobalPipes(...)` or scoped via `@Pipe(...)`) can validate the
|
|
849
|
+
* resolved value without knowing about the moost-db-specific key.
|
|
850
|
+
*/
|
|
851
|
+
declare const MOOST_ATSCRIPT_TYPE = "atscript_type";
|
|
852
|
+
type TDbActionRowMarker = true;
|
|
853
|
+
/** Stamped by `@InputForm(FormType)` — the compiled `.as` class + the wire name (`FormType.name`). */
|
|
854
|
+
interface TDbActionInputFormMeta {
|
|
855
|
+
type: TAtscriptAnnotatedType;
|
|
856
|
+
name: string;
|
|
857
|
+
}
|
|
858
|
+
/** Method-level action metadata written by `@DbAction(name, opts)`. */
|
|
859
|
+
interface TDbActionMeta {
|
|
860
|
+
name: string;
|
|
861
|
+
opts: DbActionOpts;
|
|
862
|
+
}
|
|
863
|
+
/** Class-level entry — a `TDbActionsEntry` plus its dictionary key. */
|
|
864
|
+
interface TDbClassActionMeta {
|
|
865
|
+
name: string;
|
|
866
|
+
entry: TDbActionsEntry;
|
|
867
|
+
}
|
|
868
|
+
/** Param marker kind — informs level inference and ID-resolution shape. */
|
|
869
|
+
type TDbActionParamKind = "id" | "ids";
|
|
870
|
+
declare module "moost" {
|
|
871
|
+
interface TMoostMetadata {
|
|
872
|
+
[MOOST_DB_ACTION]?: TDbActionMeta;
|
|
873
|
+
[MOOST_DB_ACTIONS]?: TDbClassActionMeta[];
|
|
874
|
+
[MOOST_DB_ACTION_PARAM]?: TDbActionParamKind;
|
|
875
|
+
[MOOST_DB_ACTION_ROW]?: TDbActionRowMarker;
|
|
876
|
+
[MOOST_DB_ACTION_ROWS]?: TDbActionRowMarker;
|
|
877
|
+
}
|
|
878
|
+
interface TMoostParamsMetadata {
|
|
879
|
+
[MOOST_DB_ACTION_PARAM]?: TDbActionParamKind;
|
|
880
|
+
[MOOST_DB_ACTION_ROW]?: TDbActionRowMarker;
|
|
881
|
+
[MOOST_DB_ACTION_ROWS]?: TDbActionRowMarker;
|
|
882
|
+
[MOOST_DB_ACTION_INPUT_FORM]?: TDbActionInputFormMeta;
|
|
883
|
+
[MOOST_ATSCRIPT_TYPE]?: TAtscriptAnnotatedType;
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
//#endregion
|
|
758
887
|
//#region src/actions/action-disabled-error.d.ts
|
|
759
888
|
/**
|
|
760
889
|
* Wire-body shape for server-side gate rejections. The `name` discriminator
|
|
@@ -815,4 +944,4 @@ declare const QUERY_CONTROLS: readonly string[];
|
|
|
815
944
|
declare const PAGES_CONTROLS: readonly string[];
|
|
816
945
|
declare const ONE_CONTROLS: readonly string[];
|
|
817
946
|
//#endregion
|
|
818
|
-
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 };
|
|
947
|
+
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,5 +1,4 @@
|
|
|
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";
|
|
@@ -61,13 +60,15 @@ declare abstract class AsReadableController<T extends TAtscriptAnnotatedType = T
|
|
|
61
60
|
private _serializedType?;
|
|
62
61
|
/** Cached full meta response (computed lazily on first meta() call). */
|
|
63
62
|
private _metaResponse?;
|
|
63
|
+
/** Cached serialized form schemas keyed by `FormType.name` — populated lazily by {@link metaForm}. */
|
|
64
|
+
private _formSchemas;
|
|
64
65
|
constructor(boundType: T, controllerName: string, app: Moost, kindTag?: string);
|
|
65
66
|
/** Subclass contract: return `true` if `path` addresses a valid field on the bound source. */
|
|
66
67
|
protected abstract hasField(path: string): boolean;
|
|
67
68
|
/** Sets @db.http.path on the type metadata from the controller's computed prefix. */
|
|
68
69
|
private _resolveHttpPath;
|
|
69
70
|
/** Lazily serializes the bound type (after all controllers have set @db.http.path). */
|
|
70
|
-
protected getSerializedType():
|
|
71
|
+
protected getSerializedType(): TSerializedAnnotatedType;
|
|
71
72
|
/**
|
|
72
73
|
* One-time initialization hook. Override to seed data, register watchers, etc.
|
|
73
74
|
*/
|
|
@@ -112,6 +113,15 @@ declare abstract class AsReadableController<T extends TAtscriptAnnotatedType = T
|
|
|
112
113
|
* subclasses can prune the response by principal.
|
|
113
114
|
*/
|
|
114
115
|
meta(): Promise<TMetaResponse>;
|
|
116
|
+
/**
|
|
117
|
+
* **GET /meta/form/:name** — returns the serialized schema of a form
|
|
118
|
+
* referenced by an action's `inputForm` field. The form name is the
|
|
119
|
+
* compiled `.as` class's `.name`, registered when an action's parameter is
|
|
120
|
+
* decorated with `@InputForm(FormType)`. Schemas are serialized once and
|
|
121
|
+
* cached per controller; the response uses the same annotation-allowlist
|
|
122
|
+
* policy as {@link getSerializeOptions}.
|
|
123
|
+
*/
|
|
124
|
+
metaForm(name: string): Promise<TSerializedAnnotatedType>;
|
|
115
125
|
/**
|
|
116
126
|
* Builds the `/meta` payload. Override in subclasses to populate source-specific
|
|
117
127
|
* fields. Subclasses that fully replace the envelope must call
|
|
@@ -155,7 +165,6 @@ declare class AsDbReadableController<T extends TAtscriptAnnotatedType = TAtscrip
|
|
|
155
165
|
protected readable: AtscriptDbReadable<T>;
|
|
156
166
|
private readonly _gates;
|
|
157
167
|
private readonly _preferredIdSet;
|
|
158
|
-
private readonly _compositeIdShapes;
|
|
159
168
|
private readonly _overlayIsNoOp;
|
|
160
169
|
constructor(readable: AtscriptDbReadable<T>, app: Moost);
|
|
161
170
|
private _buildGates;
|
|
@@ -200,11 +209,8 @@ declare class AsDbReadableController<T extends TAtscriptAnnotatedType = TAtscrip
|
|
|
200
209
|
* family (count vs no-count).
|
|
201
210
|
*/
|
|
202
211
|
private _runReadWithActions;
|
|
203
|
-
/**
|
|
204
|
-
|
|
205
|
-
* Tries composite primary key first, then compound unique indexes.
|
|
206
|
-
*/
|
|
207
|
-
protected extractCompositeId(query: Record<string, string>): Record<string, unknown> | HttpError;
|
|
212
|
+
/** Pick the first identification (PK or unique index) whose fields are all present in the query. */
|
|
213
|
+
protected extractIdShape(query: Record<string, string>): Record<string, unknown> | HttpError;
|
|
208
214
|
/**
|
|
209
215
|
* **GET /query** — returns an array of records or a count.
|
|
210
216
|
*/
|
|
@@ -720,6 +726,38 @@ declare function DbRowActions<TRow = unknown, const D extends Record<string, unk
|
|
|
720
726
|
/** Sugar for `@DbActions` with `level: 'rows'` injected into each entry. */
|
|
721
727
|
declare function DbRowsActions<TRow = unknown, const D extends Record<string, unknown> = {}>(dict: D & ValidatedUnpinnedDict<TRow, D>): ClassDecorator;
|
|
722
728
|
//#endregion
|
|
729
|
+
//#region src/actions/db-action-input-form.decorator.d.ts
|
|
730
|
+
/**
|
|
731
|
+
* Parameter decorator that injects the `input` field of the action request
|
|
732
|
+
* envelope (`{ ids?, input? }`) into the handler.
|
|
733
|
+
*
|
|
734
|
+
* Pairs the resolved value with two pieces of param-level metadata:
|
|
735
|
+
*
|
|
736
|
+
* 1. {@link MOOST_DB_ACTION_INPUT_FORM} — the compiled `.as` class plus its
|
|
737
|
+
* name, consumed by {@link discoverActions} to:
|
|
738
|
+
* - emit `inputForm: FormType.name` on the action's `/meta` entry, and
|
|
739
|
+
* - register the type in the controller's form registry so
|
|
740
|
+
* `GET /meta/form/:name` can serve the serialized schema.
|
|
741
|
+
* 2. {@link MOOST_ATSCRIPT_TYPE} — just the type ref, providing a generic
|
|
742
|
+
* hook any atscript-aware Moost pipe can read without knowing about the
|
|
743
|
+
* moost-db-specific key.
|
|
744
|
+
*
|
|
745
|
+
* Validation is intentionally *not* performed here. To validate `input`
|
|
746
|
+
* against `FormType`, install an atscript validator pipe globally
|
|
747
|
+
* (`app.applyGlobalPipes(...)`) or scope it via `@Pipe(...)`. The pipe reads
|
|
748
|
+
* `MOOST_ATSCRIPT_TYPE` off the param and runs `FormType.validator()`.
|
|
749
|
+
*
|
|
750
|
+
* Only one `@InputForm()` per action is supported. To collect multiple
|
|
751
|
+
* structured inputs, compose them into a single `.as` interface and pass an
|
|
752
|
+
* array form on the field whose user-facing intent is "list of items".
|
|
753
|
+
*
|
|
754
|
+
* @param formType A compiled `.as` interface class (carries `.validator()`,
|
|
755
|
+
* `.metadata`, etc.).
|
|
756
|
+
*/
|
|
757
|
+
declare function InputForm<T extends TAtscriptAnnotatedType & {
|
|
758
|
+
readonly name: string;
|
|
759
|
+
}>(formType: T): ParameterDecorator;
|
|
760
|
+
//#endregion
|
|
723
761
|
//#region src/actions/discover.d.ts
|
|
724
762
|
/**
|
|
725
763
|
* Pairs the wire-shaped `info` with the original decorator opts / dict entry,
|
|
@@ -730,13 +768,16 @@ interface TDbActionEnvelope {
|
|
|
730
768
|
info: TDbActionInfo$1;
|
|
731
769
|
raw: DbActionOpts | TDbActionsEntry;
|
|
732
770
|
}
|
|
771
|
+
/** Lookup helper for `AsReadableController.metaForm()`. */
|
|
772
|
+
declare function getControllerFormType(ctor: Function, name: string): TAtscriptAnnotatedType | undefined;
|
|
733
773
|
/** Discover actions on a controller, memoized per ctor. `info`-only callers map `e => e.info`. */
|
|
734
774
|
declare function discoverActions(controllerCtor: Function, app: Moost, logger: TConsoleBase): TDbActionEnvelope[];
|
|
735
775
|
//#endregion
|
|
736
776
|
//#region src/actions/id-validation.d.ts
|
|
777
|
+
/** Duck-typed shape; matches `AtscriptDbReadable`'s public surface. */
|
|
737
778
|
interface IdValidationSource {
|
|
738
|
-
|
|
739
|
-
fieldDescriptors: readonly TDbFieldMeta[];
|
|
779
|
+
readonly identifications: readonly TIdentification[];
|
|
780
|
+
readonly fieldDescriptors: readonly TDbFieldMeta[];
|
|
740
781
|
}
|
|
741
782
|
//#endregion
|
|
742
783
|
//#region src/actions/id-cache.d.ts
|
|
@@ -755,6 +796,94 @@ declare const useDbActionRows: _wooksjs_event_core0.WookComposable<{
|
|
|
755
796
|
load: () => Promise<(Record<string, unknown> | undefined)[]>;
|
|
756
797
|
}>;
|
|
757
798
|
//#endregion
|
|
799
|
+
//#region src/actions/input-form-cache.d.ts
|
|
800
|
+
/**
|
|
801
|
+
* Wire-shape of an action request body. Both fields are optional:
|
|
802
|
+
*
|
|
803
|
+
* - `ids` — what previously sat at the body root: a single identifier object
|
|
804
|
+
* (`'row'`-level), an array of identifier objects (`'rows'`-level), or
|
|
805
|
+
* absent (`'table'`-level).
|
|
806
|
+
* - `input` — present only when the action declares an `@InputForm()`
|
|
807
|
+
* parameter; carries the form payload the user filled out.
|
|
808
|
+
*/
|
|
809
|
+
interface DbActionEnvelope {
|
|
810
|
+
ids?: unknown;
|
|
811
|
+
input?: unknown;
|
|
812
|
+
}
|
|
813
|
+
/**
|
|
814
|
+
* Cached parse of the action request body. Centralises the shape check so
|
|
815
|
+
* every per-param resolver (`@DbActionID*`, `@DbActionRow*`, `@InputForm`)
|
|
816
|
+
* reads through the same gate. An array or scalar root is rejected with the
|
|
817
|
+
* same `ValidatorError` envelope as today's strict-shape ID failures.
|
|
818
|
+
*/
|
|
819
|
+
declare const dbActionBodySlot: moost.Cached<Promise<DbActionEnvelope>>;
|
|
820
|
+
/** Cached `body.input` slot — consumed by `@InputForm()` and `useDbActionInput()`. */
|
|
821
|
+
declare const dbActionInputSlot: moost.Cached<Promise<unknown>>;
|
|
822
|
+
/** Composable for in-handler reads of the form input. */
|
|
823
|
+
declare const useDbActionInput: _wooksjs_event_core0.WookComposable<{
|
|
824
|
+
load: () => Promise<unknown>;
|
|
825
|
+
}>;
|
|
826
|
+
//#endregion
|
|
827
|
+
//#region src/actions/keys.d.ts
|
|
828
|
+
/** Method-level metadata key — written by `@DbAction(name, opts)`. */
|
|
829
|
+
declare const MOOST_DB_ACTION = "atscript_db_action";
|
|
830
|
+
/** Class-level metadata key — written by `@DbActions` and the level-pinned shortcuts. Stored as an array; decorators accumulate. */
|
|
831
|
+
declare const MOOST_DB_ACTIONS = "atscript_db_actions";
|
|
832
|
+
/** Param-level metadata key — written by `@DbActionID()` / `@DbActionIDs()`. Drives level inference. */
|
|
833
|
+
declare const MOOST_DB_ACTION_PARAM = "atscript_db_action_param";
|
|
834
|
+
/** Param-level marker keys — written by `@DbActionRow()` / `@DbActionRows()`. */
|
|
835
|
+
declare const MOOST_DB_ACTION_ROW = "atscript_db_action_row";
|
|
836
|
+
declare const MOOST_DB_ACTION_ROWS = "atscript_db_action_rows";
|
|
837
|
+
/**
|
|
838
|
+
* Param-level metadata key — written by `@InputForm(FormType)`. Carries the
|
|
839
|
+
* compiled `.as` class plus its `.name` so {@link discoverActions} can both
|
|
840
|
+
* emit `inputForm` on `/meta` and register the type in the controller's form
|
|
841
|
+
* registry for `GET /meta/form/:name`.
|
|
842
|
+
*/
|
|
843
|
+
declare const MOOST_DB_ACTION_INPUT_FORM = "atscript_db_action_input_form";
|
|
844
|
+
/**
|
|
845
|
+
* Generic param-level metadata key — written by `@InputForm(FormType)`
|
|
846
|
+
* alongside {@link MOOST_DB_ACTION_INPUT_FORM}. Holds just the type ref so a
|
|
847
|
+
* generic atscript-aware Moost pipe (installed globally via
|
|
848
|
+
* `app.applyGlobalPipes(...)` or scoped via `@Pipe(...)`) can validate the
|
|
849
|
+
* resolved value without knowing about the moost-db-specific key.
|
|
850
|
+
*/
|
|
851
|
+
declare const MOOST_ATSCRIPT_TYPE = "atscript_type";
|
|
852
|
+
type TDbActionRowMarker = true;
|
|
853
|
+
/** Stamped by `@InputForm(FormType)` — the compiled `.as` class + the wire name (`FormType.name`). */
|
|
854
|
+
interface TDbActionInputFormMeta {
|
|
855
|
+
type: TAtscriptAnnotatedType;
|
|
856
|
+
name: string;
|
|
857
|
+
}
|
|
858
|
+
/** Method-level action metadata written by `@DbAction(name, opts)`. */
|
|
859
|
+
interface TDbActionMeta {
|
|
860
|
+
name: string;
|
|
861
|
+
opts: DbActionOpts;
|
|
862
|
+
}
|
|
863
|
+
/** Class-level entry — a `TDbActionsEntry` plus its dictionary key. */
|
|
864
|
+
interface TDbClassActionMeta {
|
|
865
|
+
name: string;
|
|
866
|
+
entry: TDbActionsEntry;
|
|
867
|
+
}
|
|
868
|
+
/** Param marker kind — informs level inference and ID-resolution shape. */
|
|
869
|
+
type TDbActionParamKind = "id" | "ids";
|
|
870
|
+
declare module "moost" {
|
|
871
|
+
interface TMoostMetadata {
|
|
872
|
+
[MOOST_DB_ACTION]?: TDbActionMeta;
|
|
873
|
+
[MOOST_DB_ACTIONS]?: TDbClassActionMeta[];
|
|
874
|
+
[MOOST_DB_ACTION_PARAM]?: TDbActionParamKind;
|
|
875
|
+
[MOOST_DB_ACTION_ROW]?: TDbActionRowMarker;
|
|
876
|
+
[MOOST_DB_ACTION_ROWS]?: TDbActionRowMarker;
|
|
877
|
+
}
|
|
878
|
+
interface TMoostParamsMetadata {
|
|
879
|
+
[MOOST_DB_ACTION_PARAM]?: TDbActionParamKind;
|
|
880
|
+
[MOOST_DB_ACTION_ROW]?: TDbActionRowMarker;
|
|
881
|
+
[MOOST_DB_ACTION_ROWS]?: TDbActionRowMarker;
|
|
882
|
+
[MOOST_DB_ACTION_INPUT_FORM]?: TDbActionInputFormMeta;
|
|
883
|
+
[MOOST_ATSCRIPT_TYPE]?: TAtscriptAnnotatedType;
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
//#endregion
|
|
758
887
|
//#region src/actions/action-disabled-error.d.ts
|
|
759
888
|
/**
|
|
760
889
|
* Wire-body shape for server-side gate rejections. The `name` discriminator
|
|
@@ -815,4 +944,4 @@ declare const QUERY_CONTROLS: readonly string[];
|
|
|
815
944
|
declare const PAGES_CONTROLS: readonly string[];
|
|
816
945
|
declare const ONE_CONTROLS: readonly string[];
|
|
817
946
|
//#endregion
|
|
818
|
-
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 };
|
|
947
|
+
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
|
@@ -198,9 +198,6 @@ function isAsValueHelpControllerSubclass(ctor) {
|
|
|
198
198
|
if (!asValueHelpCtor) return false;
|
|
199
199
|
return asValueHelpCtor.prototype.isPrototypeOf(ctor.prototype);
|
|
200
200
|
}
|
|
201
|
-
function isAsDbReadableControllerInstance(value) {
|
|
202
|
-
return !!asDbReadableCtor && value instanceof asDbReadableCtor;
|
|
203
|
-
}
|
|
204
201
|
//#endregion
|
|
205
202
|
//#region src/actions/keys.ts
|
|
206
203
|
/** Log-message prefix for warnings emitted from the actions subsystem. */
|
|
@@ -215,6 +212,21 @@ const MOOST_DB_ACTION_PARAM = "atscript_db_action_param";
|
|
|
215
212
|
const MOOST_DB_ACTION_ROW = "atscript_db_action_row";
|
|
216
213
|
const MOOST_DB_ACTION_ROWS = "atscript_db_action_rows";
|
|
217
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
|
+
/**
|
|
218
230
|
* Shared method-decorator update used by `@DbAction` and `@DbActionDefault`:
|
|
219
231
|
* read the existing `MOOST_DB_ACTION` slot, merge the patch (later-applied
|
|
220
232
|
* fields win), and write it back. `name` is empty until `@DbAction` provides
|
|
@@ -237,6 +249,8 @@ function scanParamLevel(params) {
|
|
|
237
249
|
let multi = false;
|
|
238
250
|
let hasRowParam = false;
|
|
239
251
|
let hasBody = false;
|
|
252
|
+
let inputForm;
|
|
253
|
+
let hasDuplicateInputForm = false;
|
|
240
254
|
for (const p of params) {
|
|
241
255
|
const kind = p[MOOST_DB_ACTION_PARAM];
|
|
242
256
|
if (kind === "id") single = true;
|
|
@@ -250,13 +264,18 @@ function scanParamLevel(params) {
|
|
|
250
264
|
hasRowParam = true;
|
|
251
265
|
}
|
|
252
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;
|
|
253
270
|
}
|
|
254
271
|
return {
|
|
255
272
|
level: single && multi ? "table" : single ? "row" : multi ? "rows" : "table",
|
|
256
273
|
single,
|
|
257
274
|
multi,
|
|
258
275
|
hasRowParam,
|
|
259
|
-
hasBody
|
|
276
|
+
hasBody,
|
|
277
|
+
inputForm,
|
|
278
|
+
hasDuplicateInputForm
|
|
260
279
|
};
|
|
261
280
|
}
|
|
262
281
|
//#endregion
|
|
@@ -273,6 +292,34 @@ const OPTIONAL_FIELDS = [
|
|
|
273
292
|
];
|
|
274
293
|
const actionsCache = /* @__PURE__ */ new WeakMap();
|
|
275
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
|
+
}
|
|
276
323
|
/** Discover actions on a controller, memoized per ctor. `info`-only callers map `e => e.info`. */
|
|
277
324
|
function discoverActions(controllerCtor, app, logger) {
|
|
278
325
|
const cached = actionsCache.get(controllerCtor);
|
|
@@ -358,6 +405,10 @@ function collectMethodActions(ctor, overview, logger, out, seen) {
|
|
|
358
405
|
processor: "backend",
|
|
359
406
|
value: path
|
|
360
407
|
};
|
|
408
|
+
if (levelInfer.inputForm) {
|
|
409
|
+
if (!registerFormType(ctor, levelInfer.inputForm, action.name, logger)) continue;
|
|
410
|
+
info.inputForm = levelInfer.inputForm.name;
|
|
411
|
+
}
|
|
361
412
|
emitInfo(info, action.opts);
|
|
362
413
|
seen.add(action.name);
|
|
363
414
|
out.push({
|
|
@@ -372,10 +423,12 @@ function inferMethodLevel(params, actionName, logger) {
|
|
|
372
423
|
logger.warn(`${WARN_PREFIX} action "${actionName}" mixes single-cardinality and multi-cardinality decorators (@DbActionID / @DbActionRow vs @DbActionIDs / @DbActionRows) — dropping`);
|
|
373
424
|
return null;
|
|
374
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.`);
|
|
375
427
|
return {
|
|
376
428
|
level: scan.level,
|
|
377
429
|
bodyConflict: scan.hasBody && scan.level !== "table",
|
|
378
|
-
hasRowParam: scan.hasRowParam
|
|
430
|
+
hasRowParam: scan.hasRowParam,
|
|
431
|
+
inputForm: scan.inputForm
|
|
379
432
|
};
|
|
380
433
|
}
|
|
381
434
|
function collectClassActions(ctor, logger, out, seen) {
|
|
@@ -486,6 +539,13 @@ function __decorate(decorators, target, key, desc) {
|
|
|
486
539
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
487
540
|
}
|
|
488
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
|
|
489
549
|
//#region src/as-readable.controller.ts
|
|
490
550
|
var _ref$4;
|
|
491
551
|
let AsReadableController = class AsReadableController {
|
|
@@ -501,6 +561,8 @@ let AsReadableController = class AsReadableController {
|
|
|
501
561
|
_serializedType;
|
|
502
562
|
/** Cached full meta response (computed lazily on first meta() call). */
|
|
503
563
|
_metaResponse;
|
|
564
|
+
/** Cached serialized form schemas keyed by `FormType.name` — populated lazily by {@link metaForm}. */
|
|
565
|
+
_formSchemas = /* @__PURE__ */ new Map();
|
|
504
566
|
constructor(boundType, controllerName, app, kindTag = "readable") {
|
|
505
567
|
this.boundType = boundType;
|
|
506
568
|
this.controllerName = controllerName;
|
|
@@ -643,6 +705,25 @@ let AsReadableController = class AsReadableController {
|
|
|
643
705
|
return this.applyMetaOverlay(this._metaResponse);
|
|
644
706
|
}
|
|
645
707
|
/**
|
|
708
|
+
* **GET /meta/form/:name** — returns the serialized schema of a form
|
|
709
|
+
* referenced by an action's `inputForm` field. The form name is the
|
|
710
|
+
* compiled `.as` class's `.name`, registered when an action's parameter is
|
|
711
|
+
* decorated with `@InputForm(FormType)`. Schemas are serialized once and
|
|
712
|
+
* cached per controller; the response uses the same annotation-allowlist
|
|
713
|
+
* policy as {@link getSerializeOptions}.
|
|
714
|
+
*/
|
|
715
|
+
async metaForm(name) {
|
|
716
|
+
discoverActions(this.constructor, this.app, this.logger);
|
|
717
|
+
const formType = getControllerFormType(this.constructor, name);
|
|
718
|
+
if (!formType) throw new HttpError(404, `Unknown form "${name}"`);
|
|
719
|
+
let cached = this._formSchemas.get(name);
|
|
720
|
+
if (!cached) {
|
|
721
|
+
cached = serializeAnnotatedType(formType, this.getSerializeOptions());
|
|
722
|
+
this._formSchemas.set(name, cached);
|
|
723
|
+
}
|
|
724
|
+
return cached;
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
646
727
|
* Builds the `/meta` payload. Override in subclasses to populate source-specific
|
|
647
728
|
* fields. Subclasses that fully replace the envelope must call
|
|
648
729
|
* {@link buildActions} and {@link buildCrud} directly so `@DbAction*`
|
|
@@ -696,6 +777,13 @@ __decorate([
|
|
|
696
777
|
__decorateMetadata("design:paramtypes", []),
|
|
697
778
|
__decorateMetadata("design:returntype", Promise)
|
|
698
779
|
], AsReadableController.prototype, "meta", null);
|
|
780
|
+
__decorate([
|
|
781
|
+
Get("meta/form/:name"),
|
|
782
|
+
__decorateParam(0, Param("name")),
|
|
783
|
+
__decorateMetadata("design:type", Function),
|
|
784
|
+
__decorateMetadata("design:paramtypes", [String]),
|
|
785
|
+
__decorateMetadata("design:returntype", Promise)
|
|
786
|
+
], AsReadableController.prototype, "metaForm", null);
|
|
699
787
|
AsReadableController = __decorate([UseValidationErrorTransform(), __decorateMetadata("design:paramtypes", [
|
|
700
788
|
Object,
|
|
701
789
|
String,
|
|
@@ -858,13 +946,6 @@ const QUERY_CONTROLS = [
|
|
|
858
946
|
const PAGES_CONTROLS = ["filter", ...dtoControls(PagesControlsDto)];
|
|
859
947
|
const ONE_CONTROLS = dtoControls(GetOneControlsDto);
|
|
860
948
|
//#endregion
|
|
861
|
-
//#region \0@oxc-project+runtime@0.120.0/helpers/decorateParam.js
|
|
862
|
-
function __decorateParam(paramIndex, decorator) {
|
|
863
|
-
return function(target, key) {
|
|
864
|
-
decorator(target, key, paramIndex);
|
|
865
|
-
};
|
|
866
|
-
}
|
|
867
|
-
//#endregion
|
|
868
949
|
//#region src/as-db-readable.controller.ts
|
|
869
950
|
var _ref$3, _ref2$2;
|
|
870
951
|
let AsDbReadableController = class AsDbReadableController extends AsReadableController {
|
|
@@ -872,14 +953,12 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
|
|
|
872
953
|
readable;
|
|
873
954
|
_gates;
|
|
874
955
|
_preferredIdSet;
|
|
875
|
-
_compositeIdShapes;
|
|
876
956
|
_overlayIsNoOp;
|
|
877
957
|
constructor(readable, app) {
|
|
878
958
|
super(readable.type, readable.tableName, app, readable.isView ? "view" : "table");
|
|
879
959
|
this.readable = readable;
|
|
880
960
|
this._gates = this._buildGates();
|
|
881
961
|
this._preferredIdSet = new Set(readable.preferredId ?? []);
|
|
882
|
-
this._compositeIdShapes = (readable.identifications ?? []).filter((id) => id.fields.length >= 2);
|
|
883
962
|
const defaultOverlay = AsReadableController.prototype.applyMetaOverlay;
|
|
884
963
|
this._overlayIsNoOp = this.applyMetaOverlay === defaultOverlay;
|
|
885
964
|
}
|
|
@@ -1103,12 +1182,9 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
|
|
|
1103
1182
|
});
|
|
1104
1183
|
return result;
|
|
1105
1184
|
}
|
|
1106
|
-
/**
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
*/
|
|
1110
|
-
extractCompositeId(query) {
|
|
1111
|
-
for (const id of this._compositeIdShapes) {
|
|
1185
|
+
/** Pick the first identification (PK or unique index) whose fields are all present in the query. */
|
|
1186
|
+
extractIdShape(query) {
|
|
1187
|
+
for (const id of this.readable.identifications) {
|
|
1112
1188
|
const idObj = {};
|
|
1113
1189
|
let allPresent = true;
|
|
1114
1190
|
for (const field of id.fields) {
|
|
@@ -1120,7 +1196,7 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
|
|
|
1120
1196
|
}
|
|
1121
1197
|
if (allPresent) return idObj;
|
|
1122
1198
|
}
|
|
1123
|
-
return new HttpError(400, "Query params do not match any
|
|
1199
|
+
return new HttpError(400, "Query params do not match any primary key or unique index");
|
|
1124
1200
|
}
|
|
1125
1201
|
/**
|
|
1126
1202
|
* **GET /query** — returns an array of records or a count.
|
|
@@ -1236,7 +1312,7 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
|
|
|
1236
1312
|
* (composite primary key or compound unique index).
|
|
1237
1313
|
*/
|
|
1238
1314
|
async getOneComposite(query, url) {
|
|
1239
|
-
const idObj = this.
|
|
1315
|
+
const idObj = this.extractIdShape(query);
|
|
1240
1316
|
if (idObj instanceof HttpError) return idObj;
|
|
1241
1317
|
const parsed = this.parseQueryString(url);
|
|
1242
1318
|
this._coerceActionsControl(parsed.controls);
|
|
@@ -1436,7 +1512,7 @@ let AsDbController = class AsDbController extends AsDbReadableController {
|
|
|
1436
1512
|
* (composite primary key or compound unique index).
|
|
1437
1513
|
*/
|
|
1438
1514
|
async removeComposite(query) {
|
|
1439
|
-
const idObj = this.
|
|
1515
|
+
const idObj = this.extractIdShape(query);
|
|
1440
1516
|
if (idObj instanceof HttpError) return idObj;
|
|
1441
1517
|
const resolvedId = await this.onRemove(idObj);
|
|
1442
1518
|
if (resolvedId === void 0) return new HttpError(500, "Not deleted");
|
|
@@ -1854,12 +1930,35 @@ function readCurrentActionMeta(ctx) {
|
|
|
1854
1930
|
return getMoostMate().read(ctrl.constructor, methodName)?.[MOOST_DB_ACTION];
|
|
1855
1931
|
}
|
|
1856
1932
|
//#endregion
|
|
1933
|
+
//#region src/actions/input-form-cache.ts
|
|
1934
|
+
/**
|
|
1935
|
+
* Cached parse of the action request body. Centralises the shape check so
|
|
1936
|
+
* every per-param resolver (`@DbActionID*`, `@DbActionRow*`, `@InputForm`)
|
|
1937
|
+
* reads through the same gate. An array or scalar root is rejected with the
|
|
1938
|
+
* same `ValidatorError` envelope as today's strict-shape ID failures.
|
|
1939
|
+
*/
|
|
1940
|
+
const dbActionBodySlot = cached(async (ctx) => {
|
|
1941
|
+
const raw = await useBody(ctx).parseBody();
|
|
1942
|
+
if (raw == null) return {};
|
|
1943
|
+
if (typeof raw !== "object" || Array.isArray(raw)) throw new ValidatorError([{
|
|
1944
|
+
path: "",
|
|
1945
|
+
message: "Action body must be an object of shape { ids?, input? }"
|
|
1946
|
+
}]);
|
|
1947
|
+
return raw;
|
|
1948
|
+
});
|
|
1949
|
+
/** Cached `body.input` slot — consumed by `@InputForm()` and `useDbActionInput()`. */
|
|
1950
|
+
const dbActionInputSlot = cached(async (ctx) => {
|
|
1951
|
+
return (await ctx.get(dbActionBodySlot)).input;
|
|
1952
|
+
});
|
|
1953
|
+
/** Composable for in-handler reads of the form input. */
|
|
1954
|
+
const useDbActionInput = defineWook((ctx) => ({ load: () => ctx.get(dbActionInputSlot) }));
|
|
1955
|
+
//#endregion
|
|
1857
1956
|
//#region src/actions/id-validation.ts
|
|
1858
1957
|
const SOURCE_CACHE = /* @__PURE__ */ new WeakMap();
|
|
1859
1958
|
function getSourceCache(source) {
|
|
1860
1959
|
let cache = SOURCE_CACHE.get(source);
|
|
1861
1960
|
if (cache) return cache;
|
|
1862
|
-
const identifications = source.
|
|
1961
|
+
const identifications = source.identifications;
|
|
1863
1962
|
const byKeySig = /* @__PURE__ */ new Map();
|
|
1864
1963
|
for (const ident of identifications) byKeySig.set(fieldsSig(ident.fields), ident);
|
|
1865
1964
|
const fieldByName = /* @__PURE__ */ new Map();
|
|
@@ -1878,7 +1977,7 @@ function fieldsSig(fields) {
|
|
|
1878
1977
|
function isIdValidationSource(value) {
|
|
1879
1978
|
if (!value || typeof value !== "object") return false;
|
|
1880
1979
|
const v = value;
|
|
1881
|
-
return
|
|
1980
|
+
return Array.isArray(v.identifications) && Array.isArray(v.fieldDescriptors);
|
|
1882
1981
|
}
|
|
1883
1982
|
function validateSingleId(body, source, path = "") {
|
|
1884
1983
|
const errors = collectIdErrors(body, source, path);
|
|
@@ -1946,22 +2045,34 @@ function isPlainObject(value) {
|
|
|
1946
2045
|
//#endregion
|
|
1947
2046
|
//#region src/actions/id-cache.ts
|
|
1948
2047
|
const boundTableKey = key("atscript_db_action_bound_table");
|
|
1949
|
-
function
|
|
1950
|
-
const fromSlot = ctx.has(boundTableKey) ? ctx.get(boundTableKey) : void 0;
|
|
1951
|
-
if (fromSlot) return fromSlot;
|
|
2048
|
+
function controllerTable(ctx) {
|
|
1952
2049
|
const ctrl = useControllerContext(ctx).getController();
|
|
1953
2050
|
return ctrl?.readable ?? ctrl?.table ?? null;
|
|
1954
2051
|
}
|
|
2052
|
+
function getActionTable(ctx) {
|
|
2053
|
+
return (ctx.has(boundTableKey) ? ctx.get(boundTableKey) : void 0) ?? controllerTable(ctx);
|
|
2054
|
+
}
|
|
2055
|
+
const warnedTags = /* @__PURE__ */ new Set();
|
|
1955
2056
|
function noTableError(ctx) {
|
|
1956
2057
|
const actionName = readCurrentActionMeta(ctx)?.name;
|
|
1957
|
-
|
|
2058
|
+
const tag = actionName ? `"${actionName}"` : "<unknown>";
|
|
2059
|
+
if (!warnedTags.has(tag)) {
|
|
2060
|
+
warnedTags.add(tag);
|
|
2061
|
+
console.warn(`${WARN_PREFIX} ${tag}: 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.`);
|
|
2062
|
+
}
|
|
2063
|
+
return new HttpError(500, {
|
|
2064
|
+
statusCode: 500,
|
|
2065
|
+
error: "Internal Server Error",
|
|
2066
|
+
message: "Internal server error",
|
|
2067
|
+
code: "ACTION_TABLE_NOT_BOUND"
|
|
2068
|
+
});
|
|
1958
2069
|
}
|
|
1959
2070
|
async function resolveValidatedId(ctx, validate) {
|
|
1960
2071
|
const table = getActionTable(ctx);
|
|
1961
2072
|
if (!isIdValidationSource(table)) throw noTableError(ctx);
|
|
1962
|
-
const
|
|
1963
|
-
validate(
|
|
1964
|
-
return
|
|
2073
|
+
const env = await ctx.get(dbActionBodySlot);
|
|
2074
|
+
validate(env.ids, table);
|
|
2075
|
+
return env.ids;
|
|
1965
2076
|
}
|
|
1966
2077
|
const dbActionIdSlot = cached((ctx) => resolveValidatedId(ctx, validateSingleId));
|
|
1967
2078
|
const dbActionIdsSlot = cached(async (ctx) => {
|
|
@@ -2061,15 +2172,11 @@ const useDbActionRows = defineWook((ctx) => ({ load: () => ctx.get(dbActionRowsS
|
|
|
2061
2172
|
//#endregion
|
|
2062
2173
|
//#region src/actions/gate-interceptor.ts
|
|
2063
2174
|
const GATE_PRIORITY = TInterceptorPriority.AFTER_GUARD;
|
|
2064
|
-
function injectBoundTable(
|
|
2175
|
+
function injectBoundTable(fallback) {
|
|
2065
2176
|
const ctx = current();
|
|
2066
2177
|
if (ctx.has(boundTableKey)) return;
|
|
2067
|
-
const
|
|
2068
|
-
if (
|
|
2069
|
-
ctx.set(boundTableKey, controller.readable);
|
|
2070
|
-
return;
|
|
2071
|
-
}
|
|
2072
|
-
if (table != null) ctx.set(boundTableKey, table);
|
|
2178
|
+
const t = controllerTable(ctx) ?? fallback;
|
|
2179
|
+
if (t != null) ctx.set(boundTableKey, t);
|
|
2073
2180
|
}
|
|
2074
2181
|
function buildGateInterceptor(opts) {
|
|
2075
2182
|
const { action, level, disabled, onDisabledRows, table } = opts;
|
|
@@ -2324,6 +2431,43 @@ function classLevelActions(dict, forcedLevel) {
|
|
|
2324
2431
|
});
|
|
2325
2432
|
}
|
|
2326
2433
|
//#endregion
|
|
2434
|
+
//#region src/actions/db-action-input-form.decorator.ts
|
|
2435
|
+
/**
|
|
2436
|
+
* Parameter decorator that injects the `input` field of the action request
|
|
2437
|
+
* envelope (`{ ids?, input? }`) into the handler.
|
|
2438
|
+
*
|
|
2439
|
+
* Pairs the resolved value with two pieces of param-level metadata:
|
|
2440
|
+
*
|
|
2441
|
+
* 1. {@link MOOST_DB_ACTION_INPUT_FORM} — the compiled `.as` class plus its
|
|
2442
|
+
* name, consumed by {@link discoverActions} to:
|
|
2443
|
+
* - emit `inputForm: FormType.name` on the action's `/meta` entry, and
|
|
2444
|
+
* - register the type in the controller's form registry so
|
|
2445
|
+
* `GET /meta/form/:name` can serve the serialized schema.
|
|
2446
|
+
* 2. {@link MOOST_ATSCRIPT_TYPE} — just the type ref, providing a generic
|
|
2447
|
+
* hook any atscript-aware Moost pipe can read without knowing about the
|
|
2448
|
+
* moost-db-specific key.
|
|
2449
|
+
*
|
|
2450
|
+
* Validation is intentionally *not* performed here. To validate `input`
|
|
2451
|
+
* against `FormType`, install an atscript validator pipe globally
|
|
2452
|
+
* (`app.applyGlobalPipes(...)`) or scope it via `@Pipe(...)`. The pipe reads
|
|
2453
|
+
* `MOOST_ATSCRIPT_TYPE` off the param and runs `FormType.validator()`.
|
|
2454
|
+
*
|
|
2455
|
+
* Only one `@InputForm()` per action is supported. To collect multiple
|
|
2456
|
+
* structured inputs, compose them into a single `.as` interface and pass an
|
|
2457
|
+
* array form on the field whose user-facing intent is "list of items".
|
|
2458
|
+
*
|
|
2459
|
+
* @param formType A compiled `.as` interface class (carries `.validator()`,
|
|
2460
|
+
* `.metadata`, etc.).
|
|
2461
|
+
*/
|
|
2462
|
+
function InputForm(formType) {
|
|
2463
|
+
const mate = getMoostMate();
|
|
2464
|
+
const meta = {
|
|
2465
|
+
type: formType,
|
|
2466
|
+
name: formType.name
|
|
2467
|
+
};
|
|
2468
|
+
return ApplyDecorators(mate.decorate(MOOST_DB_ACTION_INPUT_FORM, meta), mate.decorate(MOOST_ATSCRIPT_TYPE, formType), Resolve(async () => current().get(dbActionInputSlot), "dbActionInputForm"));
|
|
2469
|
+
}
|
|
2470
|
+
//#endregion
|
|
2327
2471
|
//#region src/actions/per-row.ts
|
|
2328
2472
|
/**
|
|
2329
2473
|
* Lift a per-row predicate into the batch shape required by
|
|
@@ -2339,4 +2483,4 @@ function classLevelActions(dict, forcedLevel) {
|
|
|
2339
2483
|
*/
|
|
2340
2484
|
const perRow = (fn) => (rows) => rows.map(fn);
|
|
2341
2485
|
//#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 };
|
|
2486
|
+
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.62",
|
|
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.62"
|
|
62
62
|
},
|
|
63
63
|
"scripts": {
|
|
64
64
|
"postinstall": "asc -f dts",
|