@atscript/moost-db 0.1.54 → 0.1.56
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/index.cjs +1096 -124
- package/dist/index.d.cts +401 -25
- package/dist/index.d.mts +401 -25
- package/dist/index.mjs +1071 -126
- package/package.json +12 -10
package/dist/index.cjs
CHANGED
|
@@ -4,70 +4,7 @@ let _moostjs_event_http = require("@moostjs/event-http");
|
|
|
4
4
|
let moost = require("moost");
|
|
5
5
|
let _uniqu_url = require("@uniqu/url");
|
|
6
6
|
let _atscript_db = require("@atscript/db");
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* DI token under which the {@link AtscriptDbReadable} instance
|
|
10
|
-
* is exposed to the readable controller's constructor via `@Inject`.
|
|
11
|
-
*/
|
|
12
|
-
const READABLE_DEF = "__atscript_db_readable_def";
|
|
13
|
-
/**
|
|
14
|
-
* DI token under which the {@link AtscriptDbTable} instance
|
|
15
|
-
* is exposed to the controller's constructor via `@Inject`.
|
|
16
|
-
* Points to the same token as READABLE_DEF for backward compatibility.
|
|
17
|
-
*/
|
|
18
|
-
const TABLE_DEF = READABLE_DEF;
|
|
19
|
-
/**
|
|
20
|
-
* Combines the boilerplate needed to turn an {@link AsDbController}
|
|
21
|
-
* subclass into a fully wired HTTP controller for a given `@db.table` model.
|
|
22
|
-
*
|
|
23
|
-
* Internally applies three decorators:
|
|
24
|
-
* 1. **Provide** — registers the table instance under {@link TABLE_DEF}.
|
|
25
|
-
* 2. **Controller** — registers the class as a Moost HTTP controller
|
|
26
|
-
* with an optional route prefix. Defaults to `table.tableName`.
|
|
27
|
-
* 3. **Inherit** — copies metadata (routes, guards, etc.) from the
|
|
28
|
-
* parent class so they stay active in the derived controller.
|
|
29
|
-
*
|
|
30
|
-
* @param table The {@link AtscriptDbTable} instance for this controller.
|
|
31
|
-
* @param prefix Optional route prefix. Defaults to `table.tableName`.
|
|
32
|
-
*
|
|
33
|
-
* @example
|
|
34
|
-
* ```ts
|
|
35
|
-
* @TableController(usersTable)
|
|
36
|
-
* export class UsersController extends AsDbController<typeof UserModel> {}
|
|
37
|
-
* ```
|
|
38
|
-
*/
|
|
39
|
-
const TableController = (table, prefix) => {
|
|
40
|
-
const resolvedPath = prefix || table.type.metadata.get("db.http.path");
|
|
41
|
-
return (0, moost.ApplyDecorators)((0, moost.Provide)(TABLE_DEF, () => table), (0, moost.Controller)(resolvedPath || table.tableName), (0, moost.Inherit)());
|
|
42
|
-
};
|
|
43
|
-
/**
|
|
44
|
-
* Combines the boilerplate needed to turn an {@link AsDbReadableController}
|
|
45
|
-
* subclass into a fully wired HTTP controller for a given `@db.view` or `@db.table` model.
|
|
46
|
-
*
|
|
47
|
-
* @param readable The {@link AtscriptDbReadable} instance (table or view).
|
|
48
|
-
* @param prefix Optional route prefix. Defaults to `readable.tableName`.
|
|
49
|
-
*
|
|
50
|
-
* @example
|
|
51
|
-
* ```ts
|
|
52
|
-
* @ReadableController(activeTasksView)
|
|
53
|
-
* export class ActiveTasksController extends AsDbReadableController<typeof ActiveTasks> {}
|
|
54
|
-
* ```
|
|
55
|
-
*/
|
|
56
|
-
const ReadableController = (readable, prefix) => {
|
|
57
|
-
const resolvedPath = prefix || readable.type.metadata.get("db.http.path");
|
|
58
|
-
return (0, moost.ApplyDecorators)((0, moost.Provide)(READABLE_DEF, () => readable), (0, moost.Controller)(resolvedPath || readable.tableName), (0, moost.Inherit)());
|
|
59
|
-
};
|
|
60
|
-
/**
|
|
61
|
-
* Alias for {@link ReadableController} — use with view-backed controllers.
|
|
62
|
-
*
|
|
63
|
-
* @example
|
|
64
|
-
* ```ts
|
|
65
|
-
* @ViewController(activeTasksView)
|
|
66
|
-
* export class ActiveTasksController extends AsDbReadableController<typeof ActiveTasks> {}
|
|
67
|
-
* ```
|
|
68
|
-
*/
|
|
69
|
-
const ViewController = ReadableController;
|
|
70
|
-
//#endregion
|
|
7
|
+
let _wooksjs_http_body = require("@wooksjs/http-body");
|
|
71
8
|
//#region src/validation-interceptor.ts
|
|
72
9
|
const dbErrorCodeToStatus = { CONFLICT: 409 };
|
|
73
10
|
function transformValidationError(error, reply) {
|
|
@@ -178,16 +115,265 @@ var SelectControlDto = class {
|
|
|
178
115
|
(0, _atscript_typescript_utils.defineAnnotatedType)("object", SortControlDto).propPattern(/./, (0, _atscript_typescript_utils.defineAnnotatedType)("union").item((0, _atscript_typescript_utils.defineAnnotatedType)().designType("number").value(1).$type).item((0, _atscript_typescript_utils.defineAnnotatedType)().designType("number").value(-1).$type).$type);
|
|
179
116
|
(0, _atscript_typescript_utils.defineAnnotatedType)("object", SelectControlDto).propPattern(/./, (0, _atscript_typescript_utils.defineAnnotatedType)("union").item((0, _atscript_typescript_utils.defineAnnotatedType)().designType("number").value(1).$type).item((0, _atscript_typescript_utils.defineAnnotatedType)().designType("number").value(0).$type).$type);
|
|
180
117
|
//#endregion
|
|
181
|
-
//#region
|
|
182
|
-
|
|
183
|
-
|
|
118
|
+
//#region src/gate-utils.ts
|
|
119
|
+
/**
|
|
120
|
+
* Walks a Uniquery filter expression and returns the first field name that
|
|
121
|
+
* fails the `isAllowed` predicate, or `undefined` if every leaf field is
|
|
122
|
+
* allowed. Logical combinators (`$and`, `$or`, `$nor`, `$not`) are traversed;
|
|
123
|
+
* other `$`-prefixed keys are skipped.
|
|
124
|
+
*
|
|
125
|
+
* Shared by the DB readable's `@db.column.filterable` gate and the value-help
|
|
126
|
+
* controller's `@ui.dict.filterable` gate — only the predicate differs.
|
|
127
|
+
*/
|
|
128
|
+
function findFilterOffender(filter, isAllowed) {
|
|
129
|
+
if (!filter || typeof filter !== "object") return;
|
|
130
|
+
for (const [key, value] of Object.entries(filter)) {
|
|
131
|
+
if (key === "$and" || key === "$or" || key === "$nor") {
|
|
132
|
+
if (Array.isArray(value)) for (const sub of value) {
|
|
133
|
+
const inner = findFilterOffender(sub, isAllowed);
|
|
134
|
+
if (inner) return inner;
|
|
135
|
+
}
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (key === "$not") {
|
|
139
|
+
const inner = findFilterOffender(value, isAllowed);
|
|
140
|
+
if (inner) return inner;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (key.startsWith("$")) continue;
|
|
144
|
+
if (!isAllowed(key)) return key;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Walks a Uniquery `$sort` control (accepts string, string[], object, or
|
|
149
|
+
* array-of-{field: dir}) and returns the first field name that fails the
|
|
150
|
+
* `isAllowed` predicate, or `undefined` if every sort key is allowed.
|
|
151
|
+
*
|
|
152
|
+
* Shared by the DB readable's `@db.column.sortable` gate and the value-help
|
|
153
|
+
* controller's `@ui.dict.sortable` gate.
|
|
154
|
+
*/
|
|
155
|
+
function findSortOffender(sort, isAllowed) {
|
|
156
|
+
if (!sort) return void 0;
|
|
157
|
+
const check = (name) => isAllowed(name) ? void 0 : name;
|
|
158
|
+
if (typeof sort === "string") {
|
|
159
|
+
for (const part of sort.split(",")) {
|
|
160
|
+
const name = part.trim().replace(/^[-+]/, "").split(":")[0];
|
|
161
|
+
if (name) {
|
|
162
|
+
const bad = check(name);
|
|
163
|
+
if (bad) return bad;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (Array.isArray(sort)) {
|
|
169
|
+
for (const entry of sort) if (typeof entry === "string") {
|
|
170
|
+
const bad = check(entry.replace(/^[-+]/, ""));
|
|
171
|
+
if (bad) return bad;
|
|
172
|
+
} else if (entry && typeof entry === "object") for (const name of Object.keys(entry)) {
|
|
173
|
+
const bad = check(name);
|
|
174
|
+
if (bad) return bad;
|
|
175
|
+
}
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (typeof sort === "object") for (const name of Object.keys(sort)) {
|
|
179
|
+
const bad = check(name);
|
|
180
|
+
if (bad) return bad;
|
|
181
|
+
}
|
|
184
182
|
}
|
|
185
183
|
//#endregion
|
|
186
|
-
//#region
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
184
|
+
//#region src/actions/keys.ts
|
|
185
|
+
/** Method-level metadata key — written by `@DbAction(name, opts)`. */
|
|
186
|
+
const MOOST_DB_ACTION = "atscript_db_action";
|
|
187
|
+
/** Class-level metadata key — written by `@DbActions` and the level-pinned shortcuts. Stored as an array; decorators accumulate. */
|
|
188
|
+
const MOOST_DB_ACTIONS = "atscript_db_actions";
|
|
189
|
+
/** Param-level metadata key — written by `@DbActionPK()` / `@DbActionPKs()`. Drives level inference. */
|
|
190
|
+
const MOOST_DB_ACTION_PARAM = "atscript_db_action_param";
|
|
191
|
+
/**
|
|
192
|
+
* Shared method-decorator update used by `@DbAction` and `@DbActionDefault`:
|
|
193
|
+
* read the existing `MOOST_DB_ACTION` slot, merge the patch (later-applied
|
|
194
|
+
* fields win), and write it back. `name` is empty until `@DbAction` provides
|
|
195
|
+
* one — `discoverActions` warns and drops actions with no name.
|
|
196
|
+
*/
|
|
197
|
+
function mergeActionMeta(current, patch) {
|
|
198
|
+
const existing = current[MOOST_DB_ACTION];
|
|
199
|
+
return {
|
|
200
|
+
name: patch.name ?? existing?.name ?? "",
|
|
201
|
+
opts: {
|
|
202
|
+
...existing?.opts,
|
|
203
|
+
...patch.opts
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
//#endregion
|
|
208
|
+
//#region src/actions/discover.ts
|
|
209
|
+
/** Optional fields shared between method opts and class-level entries. */
|
|
210
|
+
const OPTIONAL_FIELDS = [
|
|
211
|
+
"icon",
|
|
212
|
+
"intent",
|
|
213
|
+
"description",
|
|
214
|
+
"order",
|
|
215
|
+
"default",
|
|
216
|
+
"promptText"
|
|
217
|
+
];
|
|
218
|
+
const WARN_PREFIX = "[moost-db actions]";
|
|
219
|
+
const actionsCache = /* @__PURE__ */ new WeakMap();
|
|
220
|
+
/**
|
|
221
|
+
* Discover all actions declared on a controller and produce the `/meta` array.
|
|
222
|
+
* Reads class + method metadata via `getMoostMate()` and resolves bound POST
|
|
223
|
+
* paths through the Moost controller overview.
|
|
224
|
+
*
|
|
225
|
+
* Result is memoized per controller constructor — discovery walks every
|
|
226
|
+
* handler entry and reads decorator metadata, which is wasted work to repeat
|
|
227
|
+
* across instances.
|
|
228
|
+
*/
|
|
229
|
+
function discoverActions(controllerCtor, app, logger) {
|
|
230
|
+
const cached = actionsCache.get(controllerCtor);
|
|
231
|
+
if (cached) return cached;
|
|
232
|
+
const overview = app.getControllersOverview?.()?.find((o) => o.type === controllerCtor);
|
|
233
|
+
const out = [];
|
|
234
|
+
collectMethodActions(controllerCtor, overview, logger, out);
|
|
235
|
+
collectClassActions(controllerCtor, logger, out);
|
|
236
|
+
applyDefaultPerLevel(out, logger);
|
|
237
|
+
actionsCache.set(controllerCtor, out);
|
|
238
|
+
return out;
|
|
239
|
+
}
|
|
240
|
+
function collectMethodActions(ctor, overview, logger, out) {
|
|
241
|
+
if (!overview) return;
|
|
242
|
+
const byMethod = /* @__PURE__ */ new Map();
|
|
243
|
+
for (const h of overview.handlers) {
|
|
244
|
+
const list = byMethod.get(h.method);
|
|
245
|
+
if (list) list.push(h);
|
|
246
|
+
else byMethod.set(h.method, [h]);
|
|
247
|
+
}
|
|
248
|
+
for (const [methodName, handlers] of byMethod) {
|
|
249
|
+
const methodMeta = handlers[0].meta;
|
|
250
|
+
const action = methodMeta[MOOST_DB_ACTION];
|
|
251
|
+
if (!action) continue;
|
|
252
|
+
if (!action.name) {
|
|
253
|
+
logger.warn(`${WARN_PREFIX} method "${methodName}" has @DbActionDefault() but no @DbAction(name) — dropping`);
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
const levelInfer = inferMethodLevel(methodMeta.params ?? [], action.name, logger);
|
|
257
|
+
if (!levelInfer) continue;
|
|
258
|
+
if (levelInfer.bodyConflict) {
|
|
259
|
+
logger.warn(`${WARN_PREFIX} action "${action.name}" cannot mix @DbActionPK*/@DbActionPKs with @Body() — dropping`);
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
const postEntry = handlers.find((h) => h.handler.type === "HTTP" && h.handler.method === "POST");
|
|
263
|
+
if (!postEntry) {
|
|
264
|
+
logger.warn(`${WARN_PREFIX} action "${action.name}" requires @Post(...); no POST handler bound to ${methodName} — dropping`);
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
const path = postEntry.registeredAs[0]?.path;
|
|
268
|
+
if (!path) {
|
|
269
|
+
logger.warn(`${WARN_PREFIX} action "${action.name}" — POST handler ${methodName} has no registered path — dropping`);
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
const label = action.opts.label ?? methodMeta.label;
|
|
273
|
+
if (!label) {
|
|
274
|
+
logger.warn(`${WARN_PREFIX} action "${action.name}" requires a label (opts.label or @Label) — dropping`);
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
const info = {
|
|
278
|
+
name: action.name,
|
|
279
|
+
label,
|
|
280
|
+
level: levelInfer.level,
|
|
281
|
+
processor: "backend",
|
|
282
|
+
value: path
|
|
283
|
+
};
|
|
284
|
+
copyOptionalFields(info, action.opts);
|
|
285
|
+
out.push(info);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
function inferMethodLevel(params, actionName, logger) {
|
|
289
|
+
let hasPk = false;
|
|
290
|
+
let hasPks = false;
|
|
291
|
+
let hasBody = false;
|
|
292
|
+
for (const p of params) {
|
|
293
|
+
const kind = p[MOOST_DB_ACTION_PARAM];
|
|
294
|
+
if (kind === "pk") hasPk = true;
|
|
295
|
+
else if (kind === "pks") hasPks = true;
|
|
296
|
+
if (p.paramSource === "BODY") hasBody = true;
|
|
297
|
+
}
|
|
298
|
+
if (hasPk && hasPks) {
|
|
299
|
+
logger.warn(`${WARN_PREFIX} action "${actionName}" has both @DbActionPK and @DbActionPKs — dropping`);
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
const level = hasPk ? "row" : hasPks ? "rows" : "table";
|
|
303
|
+
return {
|
|
304
|
+
level,
|
|
305
|
+
bodyConflict: hasBody && level !== "table"
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
function collectClassActions(ctor, logger, out) {
|
|
309
|
+
const list = (0, moost.getMoostMate)().read(ctor)?.[MOOST_DB_ACTIONS];
|
|
310
|
+
if (!list) return;
|
|
311
|
+
for (const { name, entry, forcedLevel } of list) {
|
|
312
|
+
const built = buildClassEntry(name, entry, forcedLevel, logger);
|
|
313
|
+
if (built) out.push(built);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
function buildClassEntry(name, entry, forcedLevel, logger) {
|
|
317
|
+
const level = forcedLevel ?? entry.level;
|
|
318
|
+
if (!level) {
|
|
319
|
+
logger.warn(`${WARN_PREFIX} class-level action "${name}" requires a level — dropping. Use @DbTableActions/@DbRowActions/@DbRowsActions or set "level" explicitly.`);
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
if (!entry.label) {
|
|
323
|
+
logger.warn(`${WARN_PREFIX} class-level action "${name}" requires a label — dropping`);
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
const processor = entry.processor;
|
|
327
|
+
let value;
|
|
328
|
+
if (processor === "navigate" || processor === "backend") {
|
|
329
|
+
const v = entry.value;
|
|
330
|
+
if (typeof v !== "string" || v === "") {
|
|
331
|
+
logger.warn(`${WARN_PREFIX} class-level action "${name}" with processor "${processor}" requires a non-empty "value" — dropping`);
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
value = v;
|
|
335
|
+
} else if (processor === "custom") {
|
|
336
|
+
const v = entry.value;
|
|
337
|
+
if (v !== void 0 && v !== null) {
|
|
338
|
+
logger.warn(`${WARN_PREFIX} class-level action "${name}" with processor "custom" forbids "value" (always derived from the dict key) — dropping`);
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
value = name;
|
|
342
|
+
} else {
|
|
343
|
+
logger.warn(`${WARN_PREFIX} class-level action "${name}" has unknown processor "${String(processor)}" — dropping`);
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
const info = {
|
|
347
|
+
name,
|
|
348
|
+
label: entry.label,
|
|
349
|
+
level,
|
|
350
|
+
processor,
|
|
351
|
+
value
|
|
190
352
|
};
|
|
353
|
+
copyOptionalFields(info, entry);
|
|
354
|
+
return info;
|
|
355
|
+
}
|
|
356
|
+
function applyDefaultPerLevel(actions, logger) {
|
|
357
|
+
const winners = /* @__PURE__ */ new Map();
|
|
358
|
+
for (const a of actions) {
|
|
359
|
+
if (!a.default) continue;
|
|
360
|
+
const existing = winners.get(a.level);
|
|
361
|
+
if (existing) {
|
|
362
|
+
a.default = false;
|
|
363
|
+
logger.warn(`${WARN_PREFIX} duplicate default action at level "${a.level}": "${existing}" wins, "${a.name}" demoted`);
|
|
364
|
+
} else winners.set(a.level, a.name);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
function copyOptionalFields(info, source) {
|
|
368
|
+
for (const key of OPTIONAL_FIELDS) {
|
|
369
|
+
const value = source[key];
|
|
370
|
+
if (value !== void 0) info[key] = value;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
//#endregion
|
|
374
|
+
//#region \0@oxc-project+runtime@0.120.0/helpers/decorateMetadata.js
|
|
375
|
+
function __decorateMetadata(k, v) {
|
|
376
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
191
377
|
}
|
|
192
378
|
//#endregion
|
|
193
379
|
//#region \0@oxc-project+runtime@0.120.0/helpers/decorate.js
|
|
@@ -198,24 +384,27 @@ function __decorate(decorators, target, key, desc) {
|
|
|
198
384
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
199
385
|
}
|
|
200
386
|
//#endregion
|
|
201
|
-
//#region src/as-
|
|
202
|
-
var _ref$
|
|
203
|
-
let
|
|
204
|
-
/**
|
|
205
|
-
|
|
387
|
+
//#region src/as-readable.controller.ts
|
|
388
|
+
var _ref$4;
|
|
389
|
+
let AsReadableController = class AsReadableController {
|
|
390
|
+
/** The Atscript interface this controller serves. */
|
|
391
|
+
boundType;
|
|
392
|
+
/** Short human-readable name for logging (usually the table/source name). */
|
|
393
|
+
controllerName;
|
|
206
394
|
/** Application-scoped logger. */
|
|
207
395
|
logger;
|
|
208
|
-
/** Cached serialized type definition (lazy, computed on first access). */
|
|
209
|
-
_serializedType;
|
|
210
396
|
/** Moost application instance. */
|
|
211
397
|
app;
|
|
398
|
+
/** Cached serialized type definition (lazy, computed on first access). */
|
|
399
|
+
_serializedType;
|
|
212
400
|
/** Cached full meta response (computed lazily on first meta() call). */
|
|
213
401
|
_metaResponse;
|
|
214
|
-
constructor(
|
|
215
|
-
this.
|
|
402
|
+
constructor(boundType, controllerName, app, kindTag = "readable") {
|
|
403
|
+
this.boundType = boundType;
|
|
404
|
+
this.controllerName = controllerName;
|
|
216
405
|
this.app = app;
|
|
217
|
-
this.logger = app.getLogger(`db [${
|
|
218
|
-
this.logger.info(`Initializing ${
|
|
406
|
+
this.logger = app.getLogger(`db [${controllerName}]`);
|
|
407
|
+
this.logger.info(`Initializing ${kindTag} controller`);
|
|
219
408
|
this._resolveHttpPath();
|
|
220
409
|
try {
|
|
221
410
|
const p = this.init();
|
|
@@ -236,12 +425,12 @@ let AsDbReadableController = class AsDbReadableController {
|
|
|
236
425
|
if (!prefix) prefix = (this.app.getControllersOverview?.()?.find((o) => o.type === this.constructor))?.computedPrefix;
|
|
237
426
|
if (prefix) {
|
|
238
427
|
if (!prefix.startsWith("/")) prefix = `/${prefix}`;
|
|
239
|
-
this.
|
|
428
|
+
this.boundType.metadata.set("db.http.path", prefix);
|
|
240
429
|
}
|
|
241
430
|
}
|
|
242
|
-
/** Lazily serializes the type (after all controllers have set
|
|
431
|
+
/** Lazily serializes the bound type (after all controllers have set @db.http.path). */
|
|
243
432
|
getSerializedType() {
|
|
244
|
-
if (!this._serializedType) this._serializedType = (0, _atscript_typescript_utils.serializeAnnotatedType)(this.
|
|
433
|
+
if (!this._serializedType) this._serializedType = (0, _atscript_typescript_utils.serializeAnnotatedType)(this.boundType, this.getSerializeOptions());
|
|
245
434
|
return this._serializedType;
|
|
246
435
|
}
|
|
247
436
|
/**
|
|
@@ -250,13 +439,23 @@ let AsDbReadableController = class AsDbReadableController {
|
|
|
250
439
|
init() {}
|
|
251
440
|
/**
|
|
252
441
|
* Returns serialization options for the `/meta` endpoint's type field.
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
*
|
|
442
|
+
*
|
|
443
|
+
* `refDepth: 0.5` is intentionally static — independent of `@db.depth.limit`
|
|
444
|
+
* (which is a security guard on nested writes, not a serialization policy).
|
|
445
|
+
* The shallow shape emits `{ field, type: { id, metadata } }` for every FK,
|
|
446
|
+
* which carries the target's `db.http.path` so clients can resolve value-help
|
|
447
|
+
* URLs and lazy-fetch target `/meta` when deeper structure is needed. Nav
|
|
448
|
+
* props (`@db.rel.from` / `@db.rel.to` / `@db.rel.via`) are not `.ref` nodes
|
|
449
|
+
* and always expand fully regardless of `refDepth` — the write-payload shape
|
|
450
|
+
* clients need is unaffected.
|
|
451
|
+
*
|
|
452
|
+
* Annotation whitelist: keeps `meta.*`, `expect.*`, and `db.rel.*`; strips
|
|
453
|
+
* other `db.*` (table, column, index, default, etc.). Override in subclass
|
|
454
|
+
* to customise.
|
|
256
455
|
*/
|
|
257
456
|
getSerializeOptions() {
|
|
258
457
|
return {
|
|
259
|
-
refDepth:
|
|
458
|
+
refDepth: .5,
|
|
260
459
|
processAnnotation: ({ key, value }) => {
|
|
261
460
|
if (key.startsWith("meta.") || key.startsWith("expect.") || key.startsWith("db.rel.")) return {
|
|
262
461
|
key,
|
|
@@ -276,7 +475,7 @@ let AsDbReadableController = class AsDbReadableController {
|
|
|
276
475
|
}
|
|
277
476
|
/**
|
|
278
477
|
* Whether this controller is read-only (no write endpoints).
|
|
279
|
-
* Returns `true`
|
|
478
|
+
* Returns `true` by default; {@link AsDbController} overrides to `false`.
|
|
280
479
|
*/
|
|
281
480
|
_isReadOnly() {
|
|
282
481
|
return true;
|
|
@@ -303,7 +502,7 @@ let AsDbReadableController = class AsDbReadableController {
|
|
|
303
502
|
validateInsights(insights) {
|
|
304
503
|
for (const [key] of insights) {
|
|
305
504
|
if (key === "*") continue;
|
|
306
|
-
if (!this.
|
|
505
|
+
if (!this.hasField(key)) return `Unknown field "${key}"`;
|
|
307
506
|
}
|
|
308
507
|
}
|
|
309
508
|
validateParsed(parsed, type) {
|
|
@@ -313,6 +512,201 @@ let AsDbReadableController = class AsDbReadableController {
|
|
|
313
512
|
const insightsError = this.validateInsights(parsed.insights);
|
|
314
513
|
if (insightsError) return new _moostjs_event_http.HttpError(400, insightsError);
|
|
315
514
|
}
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Shared filter/sort/search gate check. Subclasses assemble a {@link ReadableGates}
|
|
518
|
+
* config per request (or once in the constructor when static) and call this to
|
|
519
|
+
* get a uniform HTTP 400 response for any offending field/control.
|
|
520
|
+
*/
|
|
521
|
+
checkGates(filter, controls, gates) {
|
|
522
|
+
if (gates.filter) {
|
|
523
|
+
const bad = findFilterOffender(filter, gates.filter.predicate);
|
|
524
|
+
if (bad) return new _moostjs_event_http.HttpError(400, `Filtering on field "${bad}" is not permitted — add ${gates.filter.annotation} to enable.`);
|
|
525
|
+
}
|
|
526
|
+
if (gates.sort) {
|
|
527
|
+
const bad = findSortOffender(controls.$sort, gates.sort.predicate);
|
|
528
|
+
if (bad) return new _moostjs_event_http.HttpError(400, `Sorting on field "${bad}" is not permitted — add ${gates.sort.annotation} to enable.`);
|
|
529
|
+
}
|
|
530
|
+
if (gates.search && controls.$search && !gates.search.allowed) return new _moostjs_event_http.HttpError(400, gates.search.rejectionMessage);
|
|
531
|
+
}
|
|
532
|
+
parseQueryString(url) {
|
|
533
|
+
const idx = url.indexOf("?");
|
|
534
|
+
return (0, _uniqu_url.parseUrl)(idx >= 0 ? url.slice(idx + 1) : "");
|
|
535
|
+
}
|
|
536
|
+
async returnOne(result) {
|
|
537
|
+
const item = await result;
|
|
538
|
+
if (!item) return new _moostjs_event_http.HttpError(404);
|
|
539
|
+
return item;
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* **GET /meta** — returns the bound interface's metadata envelope.
|
|
543
|
+
*
|
|
544
|
+
* Base implementation delegates to {@link buildMetaResponse}, which subclasses
|
|
545
|
+
* override to add source-specific fields (relations, searchable flags, etc.).
|
|
546
|
+
* The response is cached on the instance; async overrides must cache any
|
|
547
|
+
* extra enrichment themselves.
|
|
548
|
+
*/
|
|
549
|
+
async meta() {
|
|
550
|
+
if (this._metaResponse) return this._metaResponse;
|
|
551
|
+
const response = await this.buildMetaResponse();
|
|
552
|
+
this._metaResponse = response;
|
|
553
|
+
return response;
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Builds the `/meta` payload. Override in subclasses to populate source-specific
|
|
557
|
+
* fields. Defaults return a minimal envelope with the serialized type and the
|
|
558
|
+
* read-only flag; value-help dicts populate their capability hints here.
|
|
559
|
+
* Subclasses that fully replace the envelope must call {@link buildActions}
|
|
560
|
+
* directly so `@DbAction*` decorators still surface.
|
|
561
|
+
*/
|
|
562
|
+
async buildMetaResponse() {
|
|
563
|
+
return {
|
|
564
|
+
searchable: false,
|
|
565
|
+
vectorSearchable: false,
|
|
566
|
+
searchIndexes: [],
|
|
567
|
+
primaryKeys: [],
|
|
568
|
+
readOnly: this._isReadOnly(),
|
|
569
|
+
relations: [],
|
|
570
|
+
fields: {},
|
|
571
|
+
type: this.getSerializedType(),
|
|
572
|
+
actions: this.buildActions()
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Discovers `@DbAction*` and `@DbActions`-style class metadata on this
|
|
577
|
+
* controller and produces the `actions` array. Returns `[]` for value-help
|
|
578
|
+
* controllers — see {@link AsValueHelpController#buildMetaResponse}.
|
|
579
|
+
*/
|
|
580
|
+
buildActions() {
|
|
581
|
+
return discoverActions(this.constructor, this.app, this.logger);
|
|
582
|
+
}
|
|
583
|
+
};
|
|
584
|
+
__decorate([
|
|
585
|
+
(0, _moostjs_event_http.Get)("meta"),
|
|
586
|
+
__decorateMetadata("design:type", Function),
|
|
587
|
+
__decorateMetadata("design:paramtypes", []),
|
|
588
|
+
__decorateMetadata("design:returntype", Promise)
|
|
589
|
+
], AsReadableController.prototype, "meta", null);
|
|
590
|
+
AsReadableController = __decorate([UseValidationErrorTransform(), __decorateMetadata("design:paramtypes", [
|
|
591
|
+
Object,
|
|
592
|
+
String,
|
|
593
|
+
typeof (_ref$4 = typeof moost.Moost !== "undefined" && moost.Moost) === "function" ? _ref$4 : Object,
|
|
594
|
+
Object
|
|
595
|
+
])], AsReadableController);
|
|
596
|
+
//#endregion
|
|
597
|
+
//#region src/decorators.ts
|
|
598
|
+
/**
|
|
599
|
+
* DI token under which the {@link AtscriptDbReadable} instance
|
|
600
|
+
* is exposed to the readable controller's constructor via `@Inject`.
|
|
601
|
+
*/
|
|
602
|
+
const READABLE_DEF = "__atscript_db_readable_def";
|
|
603
|
+
/**
|
|
604
|
+
* DI token under which the {@link AtscriptDbTable} instance
|
|
605
|
+
* is exposed to the controller's constructor via `@Inject`.
|
|
606
|
+
* Points to the same token as READABLE_DEF for backward compatibility.
|
|
607
|
+
*/
|
|
608
|
+
const TABLE_DEF = READABLE_DEF;
|
|
609
|
+
/**
|
|
610
|
+
* Combines the boilerplate needed to turn an {@link AsDbController}
|
|
611
|
+
* subclass into a fully wired HTTP controller for a given `@db.table` model.
|
|
612
|
+
*
|
|
613
|
+
* Internally applies three decorators:
|
|
614
|
+
* 1. **Provide** — registers the table instance under {@link TABLE_DEF}.
|
|
615
|
+
* 2. **Controller** — registers the class as a Moost HTTP controller
|
|
616
|
+
* with an optional route prefix. Defaults to `table.tableName`.
|
|
617
|
+
* 3. **Inherit** — copies metadata (routes, guards, etc.) from the
|
|
618
|
+
* parent class so they stay active in the derived controller.
|
|
619
|
+
*
|
|
620
|
+
* @param table The {@link AtscriptDbTable} instance for this controller.
|
|
621
|
+
* @param prefix Optional route prefix. Defaults to `table.tableName`.
|
|
622
|
+
*
|
|
623
|
+
* @example
|
|
624
|
+
* ```ts
|
|
625
|
+
* @TableController(usersTable)
|
|
626
|
+
* export class UsersController extends AsDbController<typeof UserModel> {}
|
|
627
|
+
* ```
|
|
628
|
+
*/
|
|
629
|
+
const TableController = (table, prefix) => {
|
|
630
|
+
const resolvedPath = prefix || table.type.metadata.get("db.http.path");
|
|
631
|
+
return (0, moost.ApplyDecorators)((0, moost.Provide)(TABLE_DEF, () => table), (0, moost.Controller)(resolvedPath || table.tableName), (0, moost.Inherit)());
|
|
632
|
+
};
|
|
633
|
+
/**
|
|
634
|
+
* Combines the boilerplate needed to turn an {@link AsDbReadableController}
|
|
635
|
+
* subclass into a fully wired HTTP controller for a given `@db.view` or `@db.table` model.
|
|
636
|
+
*
|
|
637
|
+
* @param readable The {@link AtscriptDbReadable} instance (table or view).
|
|
638
|
+
* @param prefix Optional route prefix. Defaults to `readable.tableName`.
|
|
639
|
+
*
|
|
640
|
+
* @example
|
|
641
|
+
* ```ts
|
|
642
|
+
* @ReadableController(activeTasksView)
|
|
643
|
+
* export class ActiveTasksController extends AsDbReadableController<typeof ActiveTasks> {}
|
|
644
|
+
* ```
|
|
645
|
+
*/
|
|
646
|
+
const ReadableController = (readable, prefix) => {
|
|
647
|
+
const resolvedPath = prefix || readable.type.metadata.get("db.http.path");
|
|
648
|
+
return (0, moost.ApplyDecorators)((0, moost.Provide)(READABLE_DEF, () => readable), (0, moost.Controller)(resolvedPath || readable.tableName), (0, moost.Inherit)());
|
|
649
|
+
};
|
|
650
|
+
/**
|
|
651
|
+
* Alias for {@link ReadableController} — use with view-backed controllers.
|
|
652
|
+
*
|
|
653
|
+
* @example
|
|
654
|
+
* ```ts
|
|
655
|
+
* @ViewController(activeTasksView)
|
|
656
|
+
* export class ActiveTasksController extends AsDbReadableController<typeof ActiveTasks> {}
|
|
657
|
+
* ```
|
|
658
|
+
*/
|
|
659
|
+
const ViewController = ReadableController;
|
|
660
|
+
//#endregion
|
|
661
|
+
//#region \0@oxc-project+runtime@0.120.0/helpers/decorateParam.js
|
|
662
|
+
function __decorateParam(paramIndex, decorator) {
|
|
663
|
+
return function(target, key) {
|
|
664
|
+
decorator(target, key, paramIndex);
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
//#endregion
|
|
668
|
+
//#region src/as-db-readable.controller.ts
|
|
669
|
+
var _ref$3, _ref2$2;
|
|
670
|
+
let AsDbReadableController = class AsDbReadableController extends AsReadableController {
|
|
671
|
+
/** Reference to the underlying readable (table or view). */
|
|
672
|
+
readable;
|
|
673
|
+
_gates;
|
|
674
|
+
constructor(readable, app) {
|
|
675
|
+
super(readable.type, readable.tableName, app, readable.isView ? "view" : "table");
|
|
676
|
+
this.readable = readable;
|
|
677
|
+
this._gates = this._buildGates();
|
|
678
|
+
}
|
|
679
|
+
_buildGates() {
|
|
680
|
+
const meta = this.readable.type.metadata;
|
|
681
|
+
const gates = {};
|
|
682
|
+
if (meta.get("db.table.filterable") === "manual") {
|
|
683
|
+
const allowed = this._collectAnnotated("db.column.filterable");
|
|
684
|
+
gates.filter = {
|
|
685
|
+
predicate: (f) => allowed.has(f),
|
|
686
|
+
annotation: "@db.column.filterable"
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
if (meta.get("db.table.sortable") === "manual") {
|
|
690
|
+
const allowed = this._collectAnnotated("db.column.sortable");
|
|
691
|
+
gates.sort = {
|
|
692
|
+
predicate: (f) => allowed.has(f),
|
|
693
|
+
annotation: "@db.column.sortable"
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
return gates;
|
|
697
|
+
}
|
|
698
|
+
_collectAnnotated(annotation) {
|
|
699
|
+
const out = /* @__PURE__ */ new Set();
|
|
700
|
+
for (const [path, entry] of this.readable.flatMap) if (entry.metadata.has(annotation)) out.add(path);
|
|
701
|
+
return out;
|
|
702
|
+
}
|
|
703
|
+
hasField(path) {
|
|
704
|
+
return this.readable.flatMap.has(path);
|
|
705
|
+
}
|
|
706
|
+
/** Validates $with relations against the readable. */
|
|
707
|
+
validateParsed(parsed, type) {
|
|
708
|
+
const baseError = super.validateParsed(parsed, type);
|
|
709
|
+
if (baseError) return baseError;
|
|
316
710
|
const withRelations = parsed.controls.$with;
|
|
317
711
|
if (withRelations?.length) {
|
|
318
712
|
const relations = this.readable.relations;
|
|
@@ -348,15 +742,6 @@ let AsDbReadableController = class AsDbReadableController {
|
|
|
348
742
|
transformProjection(projection) {
|
|
349
743
|
return projection;
|
|
350
744
|
}
|
|
351
|
-
parseQueryString(url) {
|
|
352
|
-
const idx = url.indexOf("?");
|
|
353
|
-
return (0, _uniqu_url.parseUrl)(idx >= 0 ? url.slice(idx + 1) : "");
|
|
354
|
-
}
|
|
355
|
-
async returnOne(result) {
|
|
356
|
-
const item = await result;
|
|
357
|
-
if (!item) return new _moostjs_event_http.HttpError(404);
|
|
358
|
-
return item;
|
|
359
|
-
}
|
|
360
745
|
/**
|
|
361
746
|
* Extracts a composite identifier object from query params.
|
|
362
747
|
* Tries composite primary key first, then compound unique indexes.
|
|
@@ -411,6 +796,8 @@ let AsDbReadableController = class AsDbReadableController {
|
|
|
411
796
|
}
|
|
412
797
|
const error = this.validateParsed(parsed, "query");
|
|
413
798
|
if (error) return error;
|
|
799
|
+
const gateError = this.checkGates(parsed.filter, controls, this._gates);
|
|
800
|
+
if (gateError) return gateError;
|
|
414
801
|
const [filter, select] = await Promise.all([this.transformFilter(parsed.filter), this.transformProjection(controls.$select)]);
|
|
415
802
|
if (controls.$count) return this.readable.count({
|
|
416
803
|
filter,
|
|
@@ -448,6 +835,8 @@ let AsDbReadableController = class AsDbReadableController {
|
|
|
448
835
|
const error = this.validateParsed(parsed, "pages");
|
|
449
836
|
if (error) return error;
|
|
450
837
|
const controls = parsed.controls;
|
|
838
|
+
const gateError = this.checkGates(parsed.filter, controls, this._gates);
|
|
839
|
+
if (gateError) return gateError;
|
|
451
840
|
const page = Math.max(Number(controls.$page || 1), 1);
|
|
452
841
|
const size = Math.max(Number(controls.$size || 10), 1);
|
|
453
842
|
const skip = (page - 1) * size;
|
|
@@ -514,28 +903,31 @@ let AsDbReadableController = class AsDbReadableController {
|
|
|
514
903
|
/**
|
|
515
904
|
* **GET /meta** — returns table/view metadata for UI.
|
|
516
905
|
*
|
|
517
|
-
*
|
|
518
|
-
*
|
|
519
|
-
*
|
|
520
|
-
* their own enrichment if needed.
|
|
906
|
+
* Overrides the base's minimal envelope to add relations, searchable flags,
|
|
907
|
+
* vector-searchable flags, field-descriptor-derived filter/sort hints, and
|
|
908
|
+
* the configured primary keys.
|
|
521
909
|
*/
|
|
522
|
-
|
|
523
|
-
if (this._metaResponse) return this._metaResponse;
|
|
910
|
+
async buildMetaResponse() {
|
|
524
911
|
const relations = [];
|
|
525
912
|
for (const [name, rel] of this.readable.relations) relations.push({
|
|
526
913
|
name,
|
|
527
914
|
direction: rel.direction,
|
|
528
915
|
isArray: rel.isArray
|
|
529
916
|
});
|
|
917
|
+
const filterableMode = this.readable.type.metadata.get("db.table.filterable") === "manual";
|
|
918
|
+
const sortableMode = this.readable.type.metadata.get("db.table.sortable") === "manual";
|
|
530
919
|
const fields = {};
|
|
531
920
|
for (const fd of this.readable.fieldDescriptors) {
|
|
532
921
|
if (fd.ignored) continue;
|
|
922
|
+
const annotations = fd.type?.metadata;
|
|
923
|
+
const annotatedFilterable = annotations?.has("db.column.filterable") ?? false;
|
|
924
|
+
const annotatedSortable = annotations?.has("db.column.sortable") ?? false;
|
|
533
925
|
fields[fd.path] = {
|
|
534
|
-
sortable: !!fd.isIndexed,
|
|
535
|
-
filterable: true
|
|
926
|
+
sortable: sortableMode ? annotatedSortable : !!fd.isIndexed,
|
|
927
|
+
filterable: filterableMode ? annotatedFilterable : true
|
|
536
928
|
};
|
|
537
929
|
}
|
|
538
|
-
|
|
930
|
+
return {
|
|
539
931
|
searchable: this.readable.isSearchable(),
|
|
540
932
|
vectorSearchable: this.readable.isVectorSearchable(),
|
|
541
933
|
searchIndexes: this.readable.getSearchIndexes(),
|
|
@@ -543,10 +935,9 @@ let AsDbReadableController = class AsDbReadableController {
|
|
|
543
935
|
readOnly: this._isReadOnly(),
|
|
544
936
|
relations,
|
|
545
937
|
fields,
|
|
546
|
-
type: this.getSerializedType()
|
|
938
|
+
type: this.getSerializedType(),
|
|
939
|
+
actions: this.buildActions()
|
|
547
940
|
};
|
|
548
|
-
this._metaResponse = response;
|
|
549
|
-
return response;
|
|
550
941
|
}
|
|
551
942
|
};
|
|
552
943
|
__decorate([
|
|
@@ -576,23 +967,17 @@ __decorate([
|
|
|
576
967
|
__decorateParam(0, (0, _moostjs_event_http.Query)()),
|
|
577
968
|
__decorateParam(1, (0, _moostjs_event_http.Url)()),
|
|
578
969
|
__decorateMetadata("design:type", Function),
|
|
579
|
-
__decorateMetadata("design:paramtypes", [typeof (_ref2$
|
|
970
|
+
__decorateMetadata("design:paramtypes", [typeof (_ref2$2 = typeof Record !== "undefined" && Record) === "function" ? _ref2$2 : Object, String]),
|
|
580
971
|
__decorateMetadata("design:returntype", Promise)
|
|
581
972
|
], AsDbReadableController.prototype, "getOneComposite", null);
|
|
582
|
-
__decorate([
|
|
583
|
-
(0, _moostjs_event_http.Get)("meta"),
|
|
584
|
-
__decorateMetadata("design:type", Function),
|
|
585
|
-
__decorateMetadata("design:paramtypes", []),
|
|
586
|
-
__decorateMetadata("design:returntype", Object)
|
|
587
|
-
], AsDbReadableController.prototype, "meta", null);
|
|
588
973
|
AsDbReadableController = __decorate([
|
|
589
|
-
|
|
974
|
+
(0, moost.Inherit)(),
|
|
590
975
|
__decorateParam(0, (0, moost.Inject)(READABLE_DEF)),
|
|
591
|
-
__decorateMetadata("design:paramtypes", [Object, typeof (_ref$
|
|
976
|
+
__decorateMetadata("design:paramtypes", [Object, typeof (_ref$3 = typeof moost.Moost !== "undefined" && moost.Moost) === "function" ? _ref$3 : Object])
|
|
592
977
|
], AsDbReadableController);
|
|
593
978
|
//#endregion
|
|
594
979
|
//#region src/as-db.controller.ts
|
|
595
|
-
var _ref, _ref2;
|
|
980
|
+
var _ref$2, _ref2$1;
|
|
596
981
|
let AsDbController = class AsDbController extends AsDbReadableController {
|
|
597
982
|
/** Reference to the underlying table (typed for write access). */
|
|
598
983
|
get table() {
|
|
@@ -713,15 +1098,575 @@ __decorate([
|
|
|
713
1098
|
(0, _moostjs_event_http.Delete)(""),
|
|
714
1099
|
__decorateParam(0, (0, _moostjs_event_http.Query)()),
|
|
715
1100
|
__decorateMetadata("design:type", Function),
|
|
716
|
-
__decorateMetadata("design:paramtypes", [typeof (_ref2 = typeof Record !== "undefined" && Record) === "function" ? _ref2 : Object]),
|
|
1101
|
+
__decorateMetadata("design:paramtypes", [typeof (_ref2$1 = typeof Record !== "undefined" && Record) === "function" ? _ref2$1 : Object]),
|
|
717
1102
|
__decorateMetadata("design:returntype", Promise)
|
|
718
1103
|
], AsDbController.prototype, "removeComposite", null);
|
|
719
1104
|
AsDbController = __decorate([
|
|
720
1105
|
(0, moost.Inherit)(),
|
|
721
1106
|
__decorateParam(0, (0, moost.Inject)(TABLE_DEF)),
|
|
722
|
-
__decorateMetadata("design:paramtypes", [Object, typeof (_ref = typeof moost.Moost !== "undefined" && moost.Moost) === "function" ? _ref : Object])
|
|
1107
|
+
__decorateMetadata("design:paramtypes", [Object, typeof (_ref$2 = typeof moost.Moost !== "undefined" && moost.Moost) === "function" ? _ref$2 : Object])
|
|
723
1108
|
], AsDbController);
|
|
724
1109
|
//#endregion
|
|
1110
|
+
//#region src/as-value-help.controller.ts
|
|
1111
|
+
var _ref$1, _ref2;
|
|
1112
|
+
let AsValueHelpController = class AsValueHelpController extends AsReadableController {
|
|
1113
|
+
/** Per-prop metadata map of the bound interface; eagerly built once. */
|
|
1114
|
+
fieldMeta;
|
|
1115
|
+
/**
|
|
1116
|
+
* Fields that participate in `$search` by default. Populated from
|
|
1117
|
+
* `@ui.dict.searchable`:
|
|
1118
|
+
* - If any prop carries `@ui.dict.searchable`, only those props are here.
|
|
1119
|
+
* - Else if the interface carries `@ui.dict.searchable`, every `string`-typed prop is here.
|
|
1120
|
+
* - Else every `string`-typed prop is here (hint is absent — default to all strings).
|
|
1121
|
+
*/
|
|
1122
|
+
searchableFields;
|
|
1123
|
+
/** The `@meta.id` field name on the bound interface, if any. */
|
|
1124
|
+
primaryKey;
|
|
1125
|
+
constructor(boundType, controllerName, app) {
|
|
1126
|
+
super(boundType, controllerName, app, "value-help");
|
|
1127
|
+
const fieldMeta = /* @__PURE__ */ new Map();
|
|
1128
|
+
const explicitlySearchable = [];
|
|
1129
|
+
const stringProps = [];
|
|
1130
|
+
let primaryKey;
|
|
1131
|
+
const interfaceSearchable = boundType.metadata.has("ui.dict.searchable");
|
|
1132
|
+
const asObj = boundType.type;
|
|
1133
|
+
if (asObj?.props) for (const [name, prop] of asObj.props) {
|
|
1134
|
+
const meta = prop.metadata;
|
|
1135
|
+
fieldMeta.set(name, meta);
|
|
1136
|
+
if (!primaryKey && meta.has("meta.id")) primaryKey = name;
|
|
1137
|
+
if (prop.type.designType === "string") stringProps.push(name);
|
|
1138
|
+
if (meta.has("ui.dict.searchable")) explicitlySearchable.push(name);
|
|
1139
|
+
}
|
|
1140
|
+
this.fieldMeta = fieldMeta;
|
|
1141
|
+
this.primaryKey = primaryKey;
|
|
1142
|
+
this.searchableFields = explicitlySearchable.length > 0 ? explicitlySearchable : interfaceSearchable ? stringProps : stringProps;
|
|
1143
|
+
}
|
|
1144
|
+
hasField(path) {
|
|
1145
|
+
return this.fieldMeta.has(path);
|
|
1146
|
+
}
|
|
1147
|
+
/**
|
|
1148
|
+
* **GET /query** — returns an array of matched rows (up to `$limit`).
|
|
1149
|
+
*/
|
|
1150
|
+
async runQuery(url) {
|
|
1151
|
+
const parsed = this.parseQueryString(url);
|
|
1152
|
+
const validateError = this.validateParsed(parsed, "query");
|
|
1153
|
+
if (validateError) return validateError;
|
|
1154
|
+
return (await this.query({
|
|
1155
|
+
filter: parsed.filter,
|
|
1156
|
+
controls: parsed.controls
|
|
1157
|
+
})).data;
|
|
1158
|
+
}
|
|
1159
|
+
/**
|
|
1160
|
+
* **GET /pages** — paginated row window plus total count.
|
|
1161
|
+
*/
|
|
1162
|
+
async runPages(url) {
|
|
1163
|
+
const parsed = this.parseQueryString(url);
|
|
1164
|
+
const validateError = this.validateParsed(parsed, "pages");
|
|
1165
|
+
if (validateError) return validateError;
|
|
1166
|
+
const controls = parsed.controls;
|
|
1167
|
+
const page = Math.max(Number(controls.$page || 1), 1);
|
|
1168
|
+
const size = Math.max(Number(controls.$size || 10), 1);
|
|
1169
|
+
const skip = (page - 1) * size;
|
|
1170
|
+
const result = await this.query({
|
|
1171
|
+
filter: parsed.filter,
|
|
1172
|
+
controls: {
|
|
1173
|
+
...controls,
|
|
1174
|
+
$skip: skip,
|
|
1175
|
+
$limit: size
|
|
1176
|
+
}
|
|
1177
|
+
});
|
|
1178
|
+
return {
|
|
1179
|
+
data: result.data,
|
|
1180
|
+
page,
|
|
1181
|
+
itemsPerPage: size,
|
|
1182
|
+
pages: Math.ceil(result.count / size),
|
|
1183
|
+
count: result.count
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
/**
|
|
1187
|
+
* **GET /one/:id** — retrieves a single row by primary key.
|
|
1188
|
+
*/
|
|
1189
|
+
async runGetOne(id) {
|
|
1190
|
+
return this.returnOne(this.getOne(id));
|
|
1191
|
+
}
|
|
1192
|
+
/**
|
|
1193
|
+
* **GET /one?<pk>=<val>** — retrieves a single row by PK query param (fallback).
|
|
1194
|
+
*/
|
|
1195
|
+
async runGetOneComposite(query) {
|
|
1196
|
+
const pk = this.primaryKey;
|
|
1197
|
+
if (!pk) return new _moostjs_event_http.HttpError(400, "No primary key (@meta.id) on value-help interface");
|
|
1198
|
+
const id = query[pk];
|
|
1199
|
+
if (id === void 0) return new _moostjs_event_http.HttpError(400, `Missing PK field "${pk}"`);
|
|
1200
|
+
return this.returnOne(this.getOne(id));
|
|
1201
|
+
}
|
|
1202
|
+
/**
|
|
1203
|
+
* Meta response surfaces `@ui.dict.*` annotations as **hints** for the
|
|
1204
|
+
* client picker UI (which controls to render); the server does not enforce
|
|
1205
|
+
* these flags at request time.
|
|
1206
|
+
*/
|
|
1207
|
+
async buildMetaResponse() {
|
|
1208
|
+
const fields = {};
|
|
1209
|
+
for (const [path, meta] of this.fieldMeta) fields[path] = {
|
|
1210
|
+
sortable: meta.has("ui.dict.sortable"),
|
|
1211
|
+
filterable: meta.has("ui.dict.filterable")
|
|
1212
|
+
};
|
|
1213
|
+
return {
|
|
1214
|
+
searchable: this.searchableFields.length > 0,
|
|
1215
|
+
vectorSearchable: false,
|
|
1216
|
+
searchIndexes: [],
|
|
1217
|
+
primaryKeys: this.primaryKey ? [this.primaryKey] : [],
|
|
1218
|
+
readOnly: this._isReadOnly(),
|
|
1219
|
+
relations: [],
|
|
1220
|
+
fields,
|
|
1221
|
+
type: this.getSerializedType(),
|
|
1222
|
+
actions: []
|
|
1223
|
+
};
|
|
1224
|
+
}
|
|
1225
|
+
buildActions() {
|
|
1226
|
+
return [];
|
|
1227
|
+
}
|
|
1228
|
+
};
|
|
1229
|
+
__decorate([
|
|
1230
|
+
(0, _moostjs_event_http.Get)("query"),
|
|
1231
|
+
__decorateParam(0, (0, _moostjs_event_http.Url)()),
|
|
1232
|
+
__decorateMetadata("design:type", Function),
|
|
1233
|
+
__decorateMetadata("design:paramtypes", [String]),
|
|
1234
|
+
__decorateMetadata("design:returntype", Promise)
|
|
1235
|
+
], AsValueHelpController.prototype, "runQuery", null);
|
|
1236
|
+
__decorate([
|
|
1237
|
+
(0, _moostjs_event_http.Get)("pages"),
|
|
1238
|
+
__decorateParam(0, (0, _moostjs_event_http.Url)()),
|
|
1239
|
+
__decorateMetadata("design:type", Function),
|
|
1240
|
+
__decorateMetadata("design:paramtypes", [String]),
|
|
1241
|
+
__decorateMetadata("design:returntype", Promise)
|
|
1242
|
+
], AsValueHelpController.prototype, "runPages", null);
|
|
1243
|
+
__decorate([
|
|
1244
|
+
(0, _moostjs_event_http.Get)("one/:id"),
|
|
1245
|
+
__decorateParam(0, (0, moost.Param)("id")),
|
|
1246
|
+
__decorateMetadata("design:type", Function),
|
|
1247
|
+
__decorateMetadata("design:paramtypes", [String]),
|
|
1248
|
+
__decorateMetadata("design:returntype", Promise)
|
|
1249
|
+
], AsValueHelpController.prototype, "runGetOne", null);
|
|
1250
|
+
__decorate([
|
|
1251
|
+
(0, _moostjs_event_http.Get)("one"),
|
|
1252
|
+
__decorateParam(0, (0, _moostjs_event_http.Query)()),
|
|
1253
|
+
__decorateMetadata("design:type", Function),
|
|
1254
|
+
__decorateMetadata("design:paramtypes", [typeof (_ref2 = typeof Record !== "undefined" && Record) === "function" ? _ref2 : Object]),
|
|
1255
|
+
__decorateMetadata("design:returntype", Promise)
|
|
1256
|
+
], AsValueHelpController.prototype, "runGetOneComposite", null);
|
|
1257
|
+
AsValueHelpController = __decorate([(0, moost.Inherit)(), __decorateMetadata("design:paramtypes", [
|
|
1258
|
+
Object,
|
|
1259
|
+
String,
|
|
1260
|
+
typeof (_ref$1 = typeof moost.Moost !== "undefined" && moost.Moost) === "function" ? _ref$1 : Object
|
|
1261
|
+
])], AsValueHelpController);
|
|
1262
|
+
//#endregion
|
|
1263
|
+
//#region src/as-json-value-help.controller.ts
|
|
1264
|
+
var _ref;
|
|
1265
|
+
let AsJsonValueHelpController = class AsJsonValueHelpController extends AsValueHelpController {
|
|
1266
|
+
rows;
|
|
1267
|
+
_pkIndex;
|
|
1268
|
+
constructor(boundType, rows, app, controllerName) {
|
|
1269
|
+
const name = controllerName || boundType.metadata.get("db.table") || "value-help";
|
|
1270
|
+
super(boundType, name, app);
|
|
1271
|
+
this.rows = rows;
|
|
1272
|
+
if (this.primaryKey) {
|
|
1273
|
+
const pk = this.primaryKey;
|
|
1274
|
+
const index = /* @__PURE__ */ new Map();
|
|
1275
|
+
for (const row of rows) index.set(String(row[pk]), row);
|
|
1276
|
+
this._pkIndex = index;
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
async query(controls) {
|
|
1280
|
+
let rows = this.rows;
|
|
1281
|
+
if (controls.filter && Object.keys(controls.filter).length > 0) rows = rows.filter((row) => matchFilter(row, controls.filter));
|
|
1282
|
+
const search = controls.controls.$search;
|
|
1283
|
+
if (search) {
|
|
1284
|
+
const needle = search.toLowerCase();
|
|
1285
|
+
const fields = this.searchableFields;
|
|
1286
|
+
rows = rows.filter((row) => {
|
|
1287
|
+
for (const field of fields) {
|
|
1288
|
+
const v = row[field];
|
|
1289
|
+
if (typeof v === "string" && v.toLowerCase().includes(needle)) return true;
|
|
1290
|
+
}
|
|
1291
|
+
return false;
|
|
1292
|
+
});
|
|
1293
|
+
}
|
|
1294
|
+
if (controls.controls.$sort) rows = sortRows(rows, controls.controls.$sort);
|
|
1295
|
+
const total = rows.length;
|
|
1296
|
+
const skip = Math.max(0, Number(controls.controls.$skip ?? 0));
|
|
1297
|
+
const limit = Math.max(0, Number(controls.controls.$limit ?? total - skip));
|
|
1298
|
+
return {
|
|
1299
|
+
data: applySelect(rows.slice(skip, skip + limit), controls.controls.$select),
|
|
1300
|
+
count: total
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
async getOne(id) {
|
|
1304
|
+
return this._pkIndex?.get(String(id)) ?? null;
|
|
1305
|
+
}
|
|
1306
|
+
};
|
|
1307
|
+
AsJsonValueHelpController = __decorate([(0, moost.Inherit)(), __decorateMetadata("design:paramtypes", [
|
|
1308
|
+
Object,
|
|
1309
|
+
Array,
|
|
1310
|
+
typeof (_ref = typeof moost.Moost !== "undefined" && moost.Moost) === "function" ? _ref : Object,
|
|
1311
|
+
String
|
|
1312
|
+
])], AsJsonValueHelpController);
|
|
1313
|
+
function matchFilter(row, filter) {
|
|
1314
|
+
if (!filter || typeof filter !== "object") return true;
|
|
1315
|
+
for (const [key, value] of Object.entries(filter)) {
|
|
1316
|
+
if (key === "$and") {
|
|
1317
|
+
if (!Array.isArray(value)) continue;
|
|
1318
|
+
if (!value.every((clause) => matchFilter(row, clause))) return false;
|
|
1319
|
+
continue;
|
|
1320
|
+
}
|
|
1321
|
+
if (key === "$or") {
|
|
1322
|
+
if (!Array.isArray(value)) continue;
|
|
1323
|
+
if (!value.some((clause) => matchFilter(row, clause))) return false;
|
|
1324
|
+
continue;
|
|
1325
|
+
}
|
|
1326
|
+
if (key === "$nor") {
|
|
1327
|
+
if (!Array.isArray(value)) continue;
|
|
1328
|
+
if (value.some((clause) => matchFilter(row, clause))) return false;
|
|
1329
|
+
continue;
|
|
1330
|
+
}
|
|
1331
|
+
if (key === "$not") {
|
|
1332
|
+
if (matchFilter(row, value)) return false;
|
|
1333
|
+
continue;
|
|
1334
|
+
}
|
|
1335
|
+
if (key.startsWith("$")) continue;
|
|
1336
|
+
const fieldValue = row[key];
|
|
1337
|
+
if (!matchFieldPredicate(fieldValue, value)) return false;
|
|
1338
|
+
}
|
|
1339
|
+
return true;
|
|
1340
|
+
}
|
|
1341
|
+
function matchFieldPredicate(fieldValue, predicate) {
|
|
1342
|
+
if (predicate === null || typeof predicate !== "object" || Array.isArray(predicate)) return fieldValue === predicate;
|
|
1343
|
+
for (const [op, operand] of Object.entries(predicate)) switch (op) {
|
|
1344
|
+
case "$eq":
|
|
1345
|
+
if (fieldValue !== operand) return false;
|
|
1346
|
+
break;
|
|
1347
|
+
case "$ne":
|
|
1348
|
+
if (fieldValue === operand) return false;
|
|
1349
|
+
break;
|
|
1350
|
+
case "$in":
|
|
1351
|
+
if (!Array.isArray(operand) || !operand.includes(fieldValue)) return false;
|
|
1352
|
+
break;
|
|
1353
|
+
case "$nin":
|
|
1354
|
+
if (!Array.isArray(operand) || operand.includes(fieldValue)) return false;
|
|
1355
|
+
break;
|
|
1356
|
+
case "$gt":
|
|
1357
|
+
if (!(fieldValue > operand)) return false;
|
|
1358
|
+
break;
|
|
1359
|
+
case "$gte":
|
|
1360
|
+
if (!(fieldValue >= operand)) return false;
|
|
1361
|
+
break;
|
|
1362
|
+
case "$lt":
|
|
1363
|
+
if (!(fieldValue < operand)) return false;
|
|
1364
|
+
break;
|
|
1365
|
+
case "$lte":
|
|
1366
|
+
if (!(fieldValue <= operand)) return false;
|
|
1367
|
+
break;
|
|
1368
|
+
case "$regex": {
|
|
1369
|
+
const re = operand instanceof RegExp ? operand : new RegExp(String(operand));
|
|
1370
|
+
if (typeof fieldValue !== "string" || !re.test(fieldValue)) return false;
|
|
1371
|
+
break;
|
|
1372
|
+
}
|
|
1373
|
+
default: if (fieldValue !== operand) return false;
|
|
1374
|
+
}
|
|
1375
|
+
return true;
|
|
1376
|
+
}
|
|
1377
|
+
function sortRows(rows, sort) {
|
|
1378
|
+
const keys = [];
|
|
1379
|
+
const push = (name, explicit) => {
|
|
1380
|
+
const clean = name.replace(/^[-+]/, "");
|
|
1381
|
+
const dir = explicit ?? (name.startsWith("-") ? -1 : 1);
|
|
1382
|
+
if (clean) keys.push({
|
|
1383
|
+
name: clean,
|
|
1384
|
+
dir
|
|
1385
|
+
});
|
|
1386
|
+
};
|
|
1387
|
+
if (typeof sort === "string") for (const part of sort.split(",")) {
|
|
1388
|
+
const trimmed = part.trim();
|
|
1389
|
+
if (!trimmed) continue;
|
|
1390
|
+
const [name, dir] = trimmed.split(":");
|
|
1391
|
+
push(name, dir === "desc" ? -1 : dir === "asc" ? 1 : void 0);
|
|
1392
|
+
}
|
|
1393
|
+
else if (Array.isArray(sort)) {
|
|
1394
|
+
for (const entry of sort) if (typeof entry === "string") push(entry);
|
|
1395
|
+
else if (entry && typeof entry === "object") for (const [name, d] of Object.entries(entry)) push(name, d === "desc" || d === -1 ? -1 : 1);
|
|
1396
|
+
} else if (sort && typeof sort === "object") for (const [name, d] of Object.entries(sort)) push(name, d === "desc" || d === -1 ? -1 : 1);
|
|
1397
|
+
if (keys.length === 0) return rows;
|
|
1398
|
+
const out = rows.slice();
|
|
1399
|
+
out.sort((a, b) => {
|
|
1400
|
+
for (const { name, dir } of keys) {
|
|
1401
|
+
const av = a[name];
|
|
1402
|
+
const bv = b[name];
|
|
1403
|
+
if (av === bv) continue;
|
|
1404
|
+
if (av === void 0 || av === null) return -1 * dir;
|
|
1405
|
+
if (bv === void 0 || bv === null) return 1 * dir;
|
|
1406
|
+
if (av < bv) return -1 * dir;
|
|
1407
|
+
if (av > bv) return 1 * dir;
|
|
1408
|
+
}
|
|
1409
|
+
return 0;
|
|
1410
|
+
});
|
|
1411
|
+
return out;
|
|
1412
|
+
}
|
|
1413
|
+
function applySelect(rows, select) {
|
|
1414
|
+
if (!select?.length) return rows;
|
|
1415
|
+
return rows.map((row) => {
|
|
1416
|
+
const out = {};
|
|
1417
|
+
for (const key of select) out[key] = row[key];
|
|
1418
|
+
return out;
|
|
1419
|
+
});
|
|
1420
|
+
}
|
|
1421
|
+
//#endregion
|
|
1422
|
+
//#region src/actions/db-action.decorator.ts
|
|
1423
|
+
/**
|
|
1424
|
+
* Mark a controller method as a database action surfaced via `/meta`.
|
|
1425
|
+
*
|
|
1426
|
+
* Metadata-only — pair with `@Post(...)` for Moost to bind the route. The
|
|
1427
|
+
* meta builder reads this metadata plus the bound POST path lazily and
|
|
1428
|
+
* emits the action with `processor: 'backend'`. Order vs.
|
|
1429
|
+
* `@DbActionDefault()` does not matter — both merge into the same slot.
|
|
1430
|
+
*
|
|
1431
|
+
* @example
|
|
1432
|
+
* ```ts
|
|
1433
|
+
* @Post('actions/block')
|
|
1434
|
+
* @DbAction('block', { label: 'Block', icon: 'i-as-block', intent: 'negative' })
|
|
1435
|
+
* async blockUser(@DbActionPK() id: string) { ... }
|
|
1436
|
+
* ```
|
|
1437
|
+
*/
|
|
1438
|
+
function DbAction(name, opts = {}) {
|
|
1439
|
+
return (0, moost.getMoostMate)().decorate((current) => {
|
|
1440
|
+
const meta = current;
|
|
1441
|
+
return {
|
|
1442
|
+
...current,
|
|
1443
|
+
[MOOST_DB_ACTION]: mergeActionMeta(meta, {
|
|
1444
|
+
name,
|
|
1445
|
+
opts
|
|
1446
|
+
})
|
|
1447
|
+
};
|
|
1448
|
+
});
|
|
1449
|
+
}
|
|
1450
|
+
//#endregion
|
|
1451
|
+
//#region src/actions/db-action-default.decorator.ts
|
|
1452
|
+
/**
|
|
1453
|
+
* Sugar that flips `default: true` on the same method's `@DbAction` metadata.
|
|
1454
|
+
* Equivalent to passing `opts.default = true`. Decorator order does not matter.
|
|
1455
|
+
*/
|
|
1456
|
+
function DbActionDefault() {
|
|
1457
|
+
return (0, moost.getMoostMate)().decorate((current) => {
|
|
1458
|
+
const meta = current;
|
|
1459
|
+
return {
|
|
1460
|
+
...current,
|
|
1461
|
+
[MOOST_DB_ACTION]: mergeActionMeta(meta, { opts: { default: true } })
|
|
1462
|
+
};
|
|
1463
|
+
});
|
|
1464
|
+
}
|
|
1465
|
+
//#endregion
|
|
1466
|
+
//#region src/actions/pk-source.ts
|
|
1467
|
+
/**
|
|
1468
|
+
* Extract the PK validation source from a controller instance. Looks for
|
|
1469
|
+
* `readable` (set by {@link AsDbReadableController}) or `table` (set by
|
|
1470
|
+
* {@link AsDbController}).
|
|
1471
|
+
*
|
|
1472
|
+
* If the controller has no typed table attached (e.g. a value-help
|
|
1473
|
+
* controller, or a plain Moost controller without `@TableController`),
|
|
1474
|
+
* throws an HTTP 500 — this is a **server misconfiguration**, not a client
|
|
1475
|
+
* error. The body parser has nothing to validate against, so the request
|
|
1476
|
+
* cannot proceed. Use `@Body()` and parse the PK manually if you need to
|
|
1477
|
+
* accept PK-shaped bodies on a controller without an attached table.
|
|
1478
|
+
*/
|
|
1479
|
+
function resolvePkSource(controller) {
|
|
1480
|
+
const c = controller;
|
|
1481
|
+
const candidate = c.readable ?? c.table;
|
|
1482
|
+
if (!isPkValidationSource(candidate)) throw new _moostjs_event_http.HttpError(500, "@DbActionPK/@DbActionPKs requires a controller with an attached table (via @TableController / @ReadableController). Use @Body() instead if your controller has no typed table.");
|
|
1483
|
+
return candidate;
|
|
1484
|
+
}
|
|
1485
|
+
function isPkValidationSource(value) {
|
|
1486
|
+
if (!value || typeof value !== "object") return false;
|
|
1487
|
+
const v = value;
|
|
1488
|
+
return Array.isArray(v.primaryKeys) && Array.isArray(v.fieldDescriptors);
|
|
1489
|
+
}
|
|
1490
|
+
/**
|
|
1491
|
+
* Build a parameter decorator that parses the JSON request body, validates
|
|
1492
|
+
* it against the bound table's PK schema with `validate`, and tags the param
|
|
1493
|
+
* so {@link discoverActions} can infer the action's `level`.
|
|
1494
|
+
*/
|
|
1495
|
+
function createPkParamDecorator(kind, validate, resolverName) {
|
|
1496
|
+
return (0, moost.ApplyDecorators)((0, moost.getMoostMate)().decorate(MOOST_DB_ACTION_PARAM, kind), (0, moost.Resolve)(async () => {
|
|
1497
|
+
const body = await (0, _wooksjs_http_body.useBody)().parseBody();
|
|
1498
|
+
validate(body, resolvePkSource((0, moost.useControllerContext)().getController()));
|
|
1499
|
+
return body;
|
|
1500
|
+
}, resolverName));
|
|
1501
|
+
}
|
|
1502
|
+
//#endregion
|
|
1503
|
+
//#region src/actions/pk-validation.ts
|
|
1504
|
+
/**
|
|
1505
|
+
* Validate a JSON-decoded body against a single-row PK shape (scalar or
|
|
1506
|
+
* composite). Throws {@link ValidatorError} with structured `errors` so the
|
|
1507
|
+
* existing validation interceptor returns HTTP 400.
|
|
1508
|
+
*/
|
|
1509
|
+
function validateSinglePk(body, source, path = "") {
|
|
1510
|
+
const errors = collectPkErrors(body, source, path);
|
|
1511
|
+
if (errors.length > 0) throw new _atscript_typescript_utils.ValidatorError(errors);
|
|
1512
|
+
}
|
|
1513
|
+
/**
|
|
1514
|
+
* Validate a JSON-decoded body against an array of PK shapes (`@DbActionPKs`).
|
|
1515
|
+
* The body MUST be an array; each element is validated against the PK schema.
|
|
1516
|
+
*/
|
|
1517
|
+
function validateMultiPk(body, source) {
|
|
1518
|
+
if (!Array.isArray(body)) throw new _atscript_typescript_utils.ValidatorError([{
|
|
1519
|
+
path: "",
|
|
1520
|
+
message: "Expected JSON array of primary keys",
|
|
1521
|
+
details: []
|
|
1522
|
+
}]);
|
|
1523
|
+
const errors = [];
|
|
1524
|
+
for (let i = 0; i < body.length; i++) errors.push(...collectPkErrors(body[i], source, `[${i}]`));
|
|
1525
|
+
if (errors.length > 0) throw new _atscript_typescript_utils.ValidatorError(errors);
|
|
1526
|
+
}
|
|
1527
|
+
function collectPkErrors(value, source, pathPrefix) {
|
|
1528
|
+
const pkFields = source.primaryKeys;
|
|
1529
|
+
if (pkFields.length === 0) return [{
|
|
1530
|
+
path: pathPrefix,
|
|
1531
|
+
message: "Table has no primary key configured",
|
|
1532
|
+
details: []
|
|
1533
|
+
}];
|
|
1534
|
+
const errors = [];
|
|
1535
|
+
if (pkFields.length === 1) {
|
|
1536
|
+
const err = checkScalar(value, findFieldDescriptor(source, pkFields[0]), pathPrefix);
|
|
1537
|
+
if (err) errors.push(err);
|
|
1538
|
+
return errors;
|
|
1539
|
+
}
|
|
1540
|
+
if (!isPlainObject(value)) {
|
|
1541
|
+
errors.push({
|
|
1542
|
+
path: pathPrefix,
|
|
1543
|
+
message: "Expected JSON object for composite primary key",
|
|
1544
|
+
details: []
|
|
1545
|
+
});
|
|
1546
|
+
return errors;
|
|
1547
|
+
}
|
|
1548
|
+
for (const fieldName of pkFields) {
|
|
1549
|
+
const sub = pathPrefix ? `${pathPrefix}.${fieldName}` : fieldName;
|
|
1550
|
+
if (!(fieldName in value)) {
|
|
1551
|
+
errors.push({
|
|
1552
|
+
path: sub,
|
|
1553
|
+
message: `Missing primary-key field "${fieldName}"`,
|
|
1554
|
+
details: []
|
|
1555
|
+
});
|
|
1556
|
+
continue;
|
|
1557
|
+
}
|
|
1558
|
+
const fd = findFieldDescriptor(source, fieldName);
|
|
1559
|
+
const err = checkScalar(value[fieldName], fd, sub);
|
|
1560
|
+
if (err) errors.push(err);
|
|
1561
|
+
}
|
|
1562
|
+
return errors;
|
|
1563
|
+
}
|
|
1564
|
+
function findFieldDescriptor(source, name) {
|
|
1565
|
+
for (const fd of source.fieldDescriptors) if (fd.path === name) return fd;
|
|
1566
|
+
}
|
|
1567
|
+
function checkScalar(value, fd, path) {
|
|
1568
|
+
const expected = fd?.designType ?? "string";
|
|
1569
|
+
if (expected === "string" && typeof value !== "string") return scalarMismatch(path, expected, value);
|
|
1570
|
+
if (expected === "number" && typeof value !== "number") return scalarMismatch(path, expected, value);
|
|
1571
|
+
if (expected === "boolean" && typeof value !== "boolean") return scalarMismatch(path, expected, value);
|
|
1572
|
+
}
|
|
1573
|
+
function scalarMismatch(path, expected, value) {
|
|
1574
|
+
return {
|
|
1575
|
+
path,
|
|
1576
|
+
message: `Expected primary-key value to be ${expected}, got ${describe(value)}`,
|
|
1577
|
+
details: []
|
|
1578
|
+
};
|
|
1579
|
+
}
|
|
1580
|
+
function describe(value) {
|
|
1581
|
+
if (value === null) return "null";
|
|
1582
|
+
if (Array.isArray(value)) return "array";
|
|
1583
|
+
return typeof value;
|
|
1584
|
+
}
|
|
1585
|
+
function isPlainObject(value) {
|
|
1586
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1587
|
+
}
|
|
1588
|
+
//#endregion
|
|
1589
|
+
//#region src/actions/db-action-pk.decorator.ts
|
|
1590
|
+
/**
|
|
1591
|
+
* Parameter resolver that reads the primary key from the JSON request body
|
|
1592
|
+
* and validates it against the bound table's PK schema.
|
|
1593
|
+
*
|
|
1594
|
+
* - Single-field PK → JSON-encoded scalar (`"abc"`, `42`, `true`).
|
|
1595
|
+
* - Composite PK → JSON object with all PK fields.
|
|
1596
|
+
*
|
|
1597
|
+
* Validation is strict — no type coercion. Mismatches throw a
|
|
1598
|
+
* `ValidatorError` which the existing validation interceptor surfaces as
|
|
1599
|
+
* HTTP 400 with the same envelope as DTO failures.
|
|
1600
|
+
*
|
|
1601
|
+
* Marks the param so {@link discoverActions} can infer the action's `level`
|
|
1602
|
+
* as `'row'`.
|
|
1603
|
+
*/
|
|
1604
|
+
function DbActionPK() {
|
|
1605
|
+
return createPkParamDecorator("pk", validateSinglePk, "dbActionPk");
|
|
1606
|
+
}
|
|
1607
|
+
//#endregion
|
|
1608
|
+
//#region src/actions/db-action-pks.decorator.ts
|
|
1609
|
+
/**
|
|
1610
|
+
* Parameter resolver that reads a JSON array of primary keys from the request
|
|
1611
|
+
* body and validates each entry against the bound table's PK schema.
|
|
1612
|
+
*
|
|
1613
|
+
* - Scalar PK → JSON array of scalars (`["a","b","c"]`).
|
|
1614
|
+
* - Composite PK → JSON array of objects.
|
|
1615
|
+
*
|
|
1616
|
+
* Validation is strict — no type coercion. Marks the param so
|
|
1617
|
+
* {@link discoverActions} can infer the action's `level` as `'rows'`.
|
|
1618
|
+
*/
|
|
1619
|
+
function DbActionPKs() {
|
|
1620
|
+
return createPkParamDecorator("pks", validateMultiPk, "dbActionPks");
|
|
1621
|
+
}
|
|
1622
|
+
//#endregion
|
|
1623
|
+
//#region src/actions/db-actions.decorator.ts
|
|
1624
|
+
/**
|
|
1625
|
+
* Declare class-level actions on a controller. Entries are flat dicts with
|
|
1626
|
+
* `processor: 'navigate' | 'custom' | 'backend'` matching the `/meta` wire
|
|
1627
|
+
* shape (see {@link TDbActionsEntry}). Each entry MUST specify `level`. Use
|
|
1628
|
+
* the level-pinned shortcuts (`@DbTableActions`, `@DbRowActions`,
|
|
1629
|
+
* `@DbRowsActions`) to avoid repeating `level`.
|
|
1630
|
+
*
|
|
1631
|
+
* The dictionary key serves as the action `name`. Entries do NOT bind any
|
|
1632
|
+
* HTTP route — the meta builder surfaces them in `/meta` only. For
|
|
1633
|
+
* `processor: 'backend'`, the dev-supplied `value` MUST point to a real
|
|
1634
|
+
* `@Post`-bound endpoint accepting the level-determined body shape.
|
|
1635
|
+
*
|
|
1636
|
+
* Multiple `@DbActions` (and shortcut) decorators on the same class
|
|
1637
|
+
* accumulate.
|
|
1638
|
+
*/
|
|
1639
|
+
function DbActions(dict) {
|
|
1640
|
+
return classLevelActions(dict);
|
|
1641
|
+
}
|
|
1642
|
+
/** Sugar for `@DbActions` with `level: 'table'` injected into each entry. */
|
|
1643
|
+
function DbTableActions(dict) {
|
|
1644
|
+
return classLevelActions(dict, "table");
|
|
1645
|
+
}
|
|
1646
|
+
/** Sugar for `@DbActions` with `level: 'row'` injected into each entry. */
|
|
1647
|
+
function DbRowActions(dict) {
|
|
1648
|
+
return classLevelActions(dict, "row");
|
|
1649
|
+
}
|
|
1650
|
+
/** Sugar for `@DbActions` with `level: 'rows'` injected into each entry. */
|
|
1651
|
+
function DbRowsActions(dict) {
|
|
1652
|
+
return classLevelActions(dict, "rows");
|
|
1653
|
+
}
|
|
1654
|
+
function classLevelActions(dict, forcedLevel) {
|
|
1655
|
+
const entries = [];
|
|
1656
|
+
for (const [name, entry] of Object.entries(dict)) entries.push({
|
|
1657
|
+
name,
|
|
1658
|
+
entry,
|
|
1659
|
+
forcedLevel
|
|
1660
|
+
});
|
|
1661
|
+
return (0, moost.getMoostMate)().decorate((current) => {
|
|
1662
|
+
const existing = current["atscript_db_actions"] ?? [];
|
|
1663
|
+
return {
|
|
1664
|
+
...current,
|
|
1665
|
+
[MOOST_DB_ACTIONS]: [...existing, ...entries]
|
|
1666
|
+
};
|
|
1667
|
+
});
|
|
1668
|
+
}
|
|
1669
|
+
//#endregion
|
|
725
1670
|
Object.defineProperty(exports, "AsDbController", {
|
|
726
1671
|
enumerable: true,
|
|
727
1672
|
get: function() {
|
|
@@ -734,10 +1679,37 @@ Object.defineProperty(exports, "AsDbReadableController", {
|
|
|
734
1679
|
return AsDbReadableController;
|
|
735
1680
|
}
|
|
736
1681
|
});
|
|
1682
|
+
Object.defineProperty(exports, "AsJsonValueHelpController", {
|
|
1683
|
+
enumerable: true,
|
|
1684
|
+
get: function() {
|
|
1685
|
+
return AsJsonValueHelpController;
|
|
1686
|
+
}
|
|
1687
|
+
});
|
|
1688
|
+
Object.defineProperty(exports, "AsReadableController", {
|
|
1689
|
+
enumerable: true,
|
|
1690
|
+
get: function() {
|
|
1691
|
+
return AsReadableController;
|
|
1692
|
+
}
|
|
1693
|
+
});
|
|
1694
|
+
Object.defineProperty(exports, "AsValueHelpController", {
|
|
1695
|
+
enumerable: true,
|
|
1696
|
+
get: function() {
|
|
1697
|
+
return AsValueHelpController;
|
|
1698
|
+
}
|
|
1699
|
+
});
|
|
1700
|
+
exports.DbAction = DbAction;
|
|
1701
|
+
exports.DbActionDefault = DbActionDefault;
|
|
1702
|
+
exports.DbActionPK = DbActionPK;
|
|
1703
|
+
exports.DbActionPKs = DbActionPKs;
|
|
1704
|
+
exports.DbActions = DbActions;
|
|
1705
|
+
exports.DbRowActions = DbRowActions;
|
|
1706
|
+
exports.DbRowsActions = DbRowsActions;
|
|
1707
|
+
exports.DbTableActions = DbTableActions;
|
|
737
1708
|
exports.READABLE_DEF = READABLE_DEF;
|
|
738
1709
|
exports.ReadableController = ReadableController;
|
|
739
1710
|
exports.TABLE_DEF = TABLE_DEF;
|
|
740
1711
|
exports.TableController = TableController;
|
|
741
1712
|
exports.UseValidationErrorTransform = UseValidationErrorTransform;
|
|
742
1713
|
exports.ViewController = ViewController;
|
|
1714
|
+
exports.discoverActions = discoverActions;
|
|
743
1715
|
exports.validationErrorTransform = validationErrorTransform;
|