@atscript/moost-db 0.1.140 → 0.1.141
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 +80 -32
- package/dist/index.d.cts +48 -20
- package/dist/index.d.mts +48 -20
- package/dist/index.mjs +81 -33
- package/package.json +7 -7
package/dist/index.cjs
CHANGED
|
@@ -546,14 +546,18 @@ function resolveProp(type, field) {
|
|
|
546
546
|
/**
|
|
547
547
|
* Follows `def.ref` hop by hop until a prop without a `ref` (a primary key or
|
|
548
548
|
* a plain column) is reached. Cycle-safe (visited on `<typeId>.<field>`) and
|
|
549
|
-
* bounded by chain length.
|
|
549
|
+
* bounded by chain length. Only chain hops count — a plain ref (`field: ""`,
|
|
550
|
+
* a column typed with a named type) is the end of the chain. A hop into a
|
|
551
|
+
* `@db.alias` type (a view's join alias, since 0.1.141) continues on the
|
|
552
|
+
* aliased table / view — the alias is no value domain for a picker.
|
|
550
553
|
*/
|
|
551
554
|
function resolveTerminalRef(def) {
|
|
552
555
|
const ref = def.ref;
|
|
553
|
-
if (!ref) return void 0;
|
|
556
|
+
if (!ref?.field) return void 0;
|
|
554
557
|
let type = ref.type();
|
|
555
558
|
let field = ref.field;
|
|
556
559
|
if (!type) return void 0;
|
|
560
|
+
type = (0, _atscript_db.aliasTargetOf)(type) ?? type;
|
|
557
561
|
let fk = def.metadata.has("db.rel.FK");
|
|
558
562
|
const visited = new Set([`${type.id ?? ""}.${field}`]);
|
|
559
563
|
for (;;) {
|
|
@@ -561,9 +565,10 @@ function resolveTerminalRef(def) {
|
|
|
561
565
|
if (!prop) break;
|
|
562
566
|
if (prop.metadata.has("db.rel.FK")) fk = true;
|
|
563
567
|
const next = prop.ref;
|
|
564
|
-
if (!next) break;
|
|
565
|
-
const
|
|
566
|
-
if (!
|
|
568
|
+
if (!next?.field) break;
|
|
569
|
+
const resolved = next.type();
|
|
570
|
+
if (!resolved) break;
|
|
571
|
+
const nextType = (0, _atscript_db.aliasTargetOf)(resolved) ?? resolved;
|
|
567
572
|
const key = `${nextType.id ?? ""}.${next.field}`;
|
|
568
573
|
if (visited.has(key)) break;
|
|
569
574
|
visited.add(key);
|
|
@@ -615,7 +620,7 @@ function walk(node, def, shallow) {
|
|
|
615
620
|
const sProp = sType.props[name];
|
|
616
621
|
if (!sProp) continue;
|
|
617
622
|
if (isNav(prop.metadata)) continue;
|
|
618
|
-
if (prop.ref && sProp.ref && !("type" in sProp.ref.type)) {
|
|
623
|
+
if (prop.ref?.field && sProp.ref && !("type" in sProp.ref.type)) {
|
|
619
624
|
const terminal = resolveTerminalRef(prop);
|
|
620
625
|
if (terminal) {
|
|
621
626
|
const direct = prop.ref.type();
|
|
@@ -732,9 +737,9 @@ let AsReadableController = class AsReadableController {
|
|
|
732
737
|
* The shallow shape emits `{ field, type: { id, metadata } }` for every FK,
|
|
733
738
|
* which carries the target's `db.http.path` so clients can resolve value-help
|
|
734
739
|
* URLs and lazy-fetch target `/meta` when deeper structure is needed. Nav
|
|
735
|
-
* props (`@db.rel.from` / `@db.rel.to` / `@db.rel.via`)
|
|
736
|
-
* and always expand fully regardless of
|
|
737
|
-
* clients need is unaffected.
|
|
740
|
+
* props (`@db.rel.from` / `@db.rel.to` / `@db.rel.via`) carry only a plain
|
|
741
|
+
* `ref` (`field: ""`) and their bodies always expand fully regardless of
|
|
742
|
+
* `refDepth` — the write-payload shape clients need is unaffected.
|
|
738
743
|
*
|
|
739
744
|
* Annotation whitelist: keeps `meta.*`, `expect.*`, and `db.rel.*`; strips
|
|
740
745
|
* other `db.*` (table, column, index, default, etc.). Override in subclass
|
|
@@ -973,10 +978,14 @@ AsReadableController = __decorate([UseValidationErrorTransform(), __decorateMeta
|
|
|
973
978
|
])], AsReadableController);
|
|
974
979
|
//#endregion
|
|
975
980
|
//#region src/actions/verdict.ts
|
|
976
|
-
/** Assert that a `disabled` predicate returned
|
|
981
|
+
/** Assert that a `disabled` predicate returned an array of the expected length; throws HTTP 500 otherwise. */
|
|
977
982
|
function assertVerdictLength(action, verdicts, expected) {
|
|
978
983
|
if (!Array.isArray(verdicts) || verdicts.length !== expected) throw new _moostjs_event_http.HttpError(500, `Action "${action}" disabled predicate returned an invalid verdict array`);
|
|
979
984
|
}
|
|
985
|
+
/** The reason a verdict carries — a non-empty string; `undefined` for `true` / falsy verdicts. */
|
|
986
|
+
function verdictReason(verdict) {
|
|
987
|
+
return typeof verdict === "string" && verdict !== "" ? verdict : void 0;
|
|
988
|
+
}
|
|
980
989
|
//#endregion
|
|
981
990
|
//#region src/actions/list-augmenter.ts
|
|
982
991
|
const candidateCache = /* @__PURE__ */ new WeakMap();
|
|
@@ -1017,7 +1026,8 @@ function computeStripFields(candidates, resolvedProjection) {
|
|
|
1017
1026
|
return strip;
|
|
1018
1027
|
}
|
|
1019
1028
|
/**
|
|
1020
|
-
* Sets `$actions` on every row
|
|
1029
|
+
* Sets `$actions` on every row (plus `$disabledReasons` on rows where a
|
|
1030
|
+
* predicate returned a reason string) and strips the columns fetched only for an
|
|
1021
1031
|
* action's `requiredFields` — IN PLACE; returns the same array, typed as
|
|
1022
1032
|
* augmented.
|
|
1023
1033
|
*/
|
|
@@ -1034,15 +1044,19 @@ function augmentRowsWithActions(args) {
|
|
|
1034
1044
|
for (let i = 0; i < rows.length; i++) {
|
|
1035
1045
|
const row = rows[i];
|
|
1036
1046
|
const names = [];
|
|
1047
|
+
let reasons;
|
|
1037
1048
|
for (let j = 0; j < candidates.length; j++) {
|
|
1038
|
-
const
|
|
1039
|
-
|
|
1040
|
-
|
|
1049
|
+
const name = candidates[j].envelope.info.name;
|
|
1050
|
+
const verdict = verdicts[j]?.[i];
|
|
1051
|
+
if (!verdict) {
|
|
1052
|
+
names.push(name);
|
|
1041
1053
|
continue;
|
|
1042
1054
|
}
|
|
1043
|
-
|
|
1055
|
+
const reason = verdictReason(verdict);
|
|
1056
|
+
if (reason !== void 0) (reasons ??= {})[name] = reason;
|
|
1044
1057
|
}
|
|
1045
1058
|
row.$actions = names;
|
|
1059
|
+
if (reasons) row.$disabledReasons = reasons;
|
|
1046
1060
|
}
|
|
1047
1061
|
if (resolvedProjection !== null) {
|
|
1048
1062
|
const stripFields = computeStripFields(candidates, resolvedProjection);
|
|
@@ -2433,6 +2447,7 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
2433
2447
|
if (geoIndexedPhysical.has(fd.physicalName)) entry.geo = true;
|
|
2434
2448
|
if (this._writeOnlySet.has(path)) entry.writeOnly = true;
|
|
2435
2449
|
if (cap.bucketable) entry.bucketable = true;
|
|
2450
|
+
if (fd.derived) entry.derived = true;
|
|
2436
2451
|
fields[path] = entry;
|
|
2437
2452
|
}
|
|
2438
2453
|
return {
|
|
@@ -3116,6 +3131,7 @@ function assertExposed(app, models, options) {
|
|
|
3116
3131
|
}
|
|
3117
3132
|
const missing = [];
|
|
3118
3133
|
for (const model of models) {
|
|
3134
|
+
if ((0, _atscript_db.aliasTargetOf)(model)) continue;
|
|
3119
3135
|
const httpPath = model.metadata.get("db.http.path");
|
|
3120
3136
|
if (!auditAll && httpPath === void 0) continue;
|
|
3121
3137
|
if (excluded.has(model) || exposed.has(model)) continue;
|
|
@@ -3127,31 +3143,58 @@ function assertExposed(app, models, options) {
|
|
|
3127
3143
|
}
|
|
3128
3144
|
//#endregion
|
|
3129
3145
|
//#region src/actions/action-disabled-error.ts
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3146
|
+
/** Distinct reasons quoted in a mixed `'rows'` message before `(+N more)`. */
|
|
3147
|
+
const MAX_LISTED_REASONS = 3;
|
|
3148
|
+
function sharedReason(reasons) {
|
|
3149
|
+
const first = reasons[0];
|
|
3150
|
+
if (first === null || first === void 0) return void 0;
|
|
3151
|
+
return reasons.every((r) => r === first) ? first : void 0;
|
|
3152
|
+
}
|
|
3153
|
+
function rowsMessage(action, count, reasons) {
|
|
3154
|
+
const base = `Action "${action}" is disabled for ${count} of the selected rows`;
|
|
3155
|
+
const distinct = [...new Set(reasons.filter((r) => r !== null))];
|
|
3156
|
+
if (distinct.length === 0) return base;
|
|
3157
|
+
const listed = distinct.slice(0, MAX_LISTED_REASONS).join("; ");
|
|
3158
|
+
const more = distinct.length - MAX_LISTED_REASONS;
|
|
3159
|
+
return `${base}: ${listed}${more > 0 ? ` (+${more} more)` : ""}`;
|
|
3133
3160
|
}
|
|
3134
3161
|
/**
|
|
3135
3162
|
* Thrown by the gate interceptor when `disabled` returns truthy. Composes
|
|
3136
3163
|
* with Moost's existing error mapper to produce HTTP 409 with the wire body
|
|
3137
3164
|
* defined by {@link ActionDisabledErrorBody}.
|
|
3138
3165
|
*
|
|
3139
|
-
* - `'row'`-level rejection: pass `(action, id)` — the
|
|
3140
|
-
*
|
|
3141
|
-
*
|
|
3142
|
-
* of
|
|
3166
|
+
* - `'row'`-level rejection: pass `(action, id, undefined, [reason])` — the
|
|
3167
|
+
* body emits `id` (+ `reason` when given).
|
|
3168
|
+
* - `'rows'`-level rejection: pass `(action, undefined, ids, reasons?)` — the
|
|
3169
|
+
* body emits `ids` (the FULL list of failing IDs in reject mode; the FULL
|
|
3170
|
+
* list of request IDs in skip mode with zero survivors) and, when any
|
|
3171
|
+
* reason exists, `reasons` aligned with `ids`.
|
|
3172
|
+
*
|
|
3173
|
+
* `reasons` entries are verdict reasons (`verdictReason`): anything but a
|
|
3174
|
+
* non-empty string counts as "no reason".
|
|
3143
3175
|
*/
|
|
3144
3176
|
var ActionDisabledError = class extends _moostjs_event_http.HttpError {
|
|
3145
3177
|
name = "ActionDisabledError";
|
|
3146
|
-
constructor(action, id, ids) {
|
|
3178
|
+
constructor(action, id, ids, reasons) {
|
|
3147
3179
|
const body = {
|
|
3148
3180
|
name: "ActionDisabledError",
|
|
3149
|
-
message:
|
|
3181
|
+
message: "",
|
|
3150
3182
|
statusCode: 409,
|
|
3151
3183
|
action
|
|
3152
3184
|
};
|
|
3153
|
-
if (ids !== void 0)
|
|
3154
|
-
|
|
3185
|
+
if (ids !== void 0) {
|
|
3186
|
+
const aligned = ids.map((_, i) => verdictReason(reasons?.[i]) ?? null);
|
|
3187
|
+
const reason = sharedReason(aligned);
|
|
3188
|
+
body.message = reason ?? rowsMessage(action, ids.length, aligned);
|
|
3189
|
+
body.ids = ids;
|
|
3190
|
+
if (reason !== void 0) body.reason = reason;
|
|
3191
|
+
if (aligned.some((r) => r !== null)) body.reasons = aligned;
|
|
3192
|
+
} else {
|
|
3193
|
+
const reason = verdictReason(reasons?.[0]);
|
|
3194
|
+
body.message = reason ?? `Action "${action}" is disabled for this row`;
|
|
3195
|
+
if (id !== void 0) body.id = id;
|
|
3196
|
+
if (reason !== void 0) body.reason = reason;
|
|
3197
|
+
}
|
|
3155
3198
|
super(409, body);
|
|
3156
3199
|
}
|
|
3157
3200
|
};
|
|
@@ -3442,7 +3485,7 @@ function buildGateInterceptor(opts) {
|
|
|
3442
3485
|
if (level === "row") {
|
|
3443
3486
|
const verdicts = disabled([await ctx.get(dbActionRowSlot)]);
|
|
3444
3487
|
assertVerdictLength(action, verdicts, 1);
|
|
3445
|
-
if (verdicts[0]) throw new ActionDisabledError(action, await ctx.get(dbActionIdSlot));
|
|
3488
|
+
if (verdicts[0]) throw new ActionDisabledError(action, await ctx.get(dbActionIdSlot), void 0, [verdictReason(verdicts[0])]);
|
|
3446
3489
|
return;
|
|
3447
3490
|
}
|
|
3448
3491
|
const ids = await ctx.get(dbActionIdsSlot);
|
|
@@ -3452,26 +3495,30 @@ function buildGateInterceptor(opts) {
|
|
|
3452
3495
|
const verdicts = disabled(existingRows);
|
|
3453
3496
|
assertVerdictLength(action, verdicts, existingRows.length);
|
|
3454
3497
|
const failingIds = [];
|
|
3498
|
+
const failingReasons = [];
|
|
3455
3499
|
const passingRows = [];
|
|
3456
3500
|
const passingIds = [];
|
|
3457
3501
|
let verdictIndex = 0;
|
|
3458
3502
|
for (let i = 0; i < ids.length; i++) {
|
|
3459
3503
|
const row = rows[i];
|
|
3460
|
-
|
|
3461
|
-
|
|
3504
|
+
const verdict = row === void 0 ? void 0 : verdicts[verdictIndex++];
|
|
3505
|
+
if (row === void 0 || verdict) {
|
|
3506
|
+
failingIds.push(ids[i]);
|
|
3507
|
+
failingReasons.push(verdictReason(verdict));
|
|
3508
|
+
} else {
|
|
3462
3509
|
passingRows.push(row);
|
|
3463
3510
|
passingIds.push(ids[i]);
|
|
3464
3511
|
}
|
|
3465
3512
|
}
|
|
3466
3513
|
if (onDisabledRows === "skip") {
|
|
3467
|
-
if (passingRows.length === 0) throw new ActionDisabledError(action, void 0, [...ids]);
|
|
3514
|
+
if (passingRows.length === 0) throw new ActionDisabledError(action, void 0, [...ids], failingReasons);
|
|
3468
3515
|
if (failingIds.length > 0) {
|
|
3469
3516
|
ctx.set(dbActionRowsSlot, Promise.resolve(passingRows));
|
|
3470
3517
|
ctx.set(dbActionIdsSlot, Promise.resolve(passingIds));
|
|
3471
3518
|
}
|
|
3472
3519
|
return;
|
|
3473
3520
|
}
|
|
3474
|
-
if (failingIds.length > 0) throw new ActionDisabledError(action, void 0, failingIds);
|
|
3521
|
+
if (failingIds.length > 0) throw new ActionDisabledError(action, void 0, failingIds, failingReasons);
|
|
3475
3522
|
}, GATE_PRIORITY);
|
|
3476
3523
|
}
|
|
3477
3524
|
/** Thin interceptor for `@DbActionRow*` without `disabled` — injects only the bound table. */
|
|
@@ -3760,7 +3807,8 @@ function InputForm(formType, validatorOpts) {
|
|
|
3760
3807
|
/**
|
|
3761
3808
|
* Lift a per-row predicate into the batch shape required by
|
|
3762
3809
|
* `@DbAction` opts.`disabled` and class-level dict `disabled`. Polarity is
|
|
3763
|
-
* preserved — `true` from `fn` means the action is disabled for that row
|
|
3810
|
+
* preserved — `true` from `fn` means the action is disabled for that row; a
|
|
3811
|
+
* non-empty string disables it with that reason.
|
|
3764
3812
|
*
|
|
3765
3813
|
* ```ts
|
|
3766
3814
|
* @DbAction<Order>('archive', {
|
package/dist/index.d.cts
CHANGED
|
@@ -77,9 +77,9 @@ declare abstract class AsReadableController<T extends TAtscriptAnnotatedType = T
|
|
|
77
77
|
* The shallow shape emits `{ field, type: { id, metadata } }` for every FK,
|
|
78
78
|
* which carries the target's `db.http.path` so clients can resolve value-help
|
|
79
79
|
* URLs and lazy-fetch target `/meta` when deeper structure is needed. Nav
|
|
80
|
-
* props (`@db.rel.from` / `@db.rel.to` / `@db.rel.via`)
|
|
81
|
-
* and always expand fully regardless of
|
|
82
|
-
* clients need is unaffected.
|
|
80
|
+
* props (`@db.rel.from` / `@db.rel.to` / `@db.rel.via`) carry only a plain
|
|
81
|
+
* `ref` (`field: ""`) and their bodies always expand fully regardless of
|
|
82
|
+
* `refDepth` — the write-payload shape clients need is unaffected.
|
|
83
83
|
*
|
|
84
84
|
* Annotation whitelist: keeps `meta.*`, `expect.*`, and `db.rel.*`; strips
|
|
85
85
|
* other `db.*` (table, column, index, default, etc.). Override in subclass
|
|
@@ -589,7 +589,7 @@ declare class AsDbReadableController<T extends TAtscriptAnnotatedType = TAtscrip
|
|
|
589
589
|
*
|
|
590
590
|
* Convention (not enforced): name decoration keys with a `$` prefix, like
|
|
591
591
|
* `$actions` and `$distance`, so they can never collide with a field name.
|
|
592
|
-
* Do not overwrite `$actions`. Columns the
|
|
592
|
+
* Do not overwrite `$actions` or `$disabledReasons`. Columns the
|
|
593
593
|
* hook needs but the client did not select must be added in
|
|
594
594
|
* {@link transformProjection} — they are then part of the response.
|
|
595
595
|
*
|
|
@@ -1006,6 +1006,15 @@ declare class AsJsonValueHelpController<T extends TAtscriptAnnotatedType = TAtsc
|
|
|
1006
1006
|
}
|
|
1007
1007
|
//#endregion
|
|
1008
1008
|
//#region src/actions/types.d.ts
|
|
1009
|
+
/**
|
|
1010
|
+
* One entry of a `disabled` predicate's result. Truthy = the action is
|
|
1011
|
+
* disabled for that row; falsy (`false`, `""`) = enabled. A non-empty string
|
|
1012
|
+
* disables the action AND carries a human-readable reason — surfaced in the
|
|
1013
|
+
* 409 `ActionDisabledError` message/body and in the row's `$disabledReasons`.
|
|
1014
|
+
*
|
|
1015
|
+
* @since 0.1.141 (`string`)
|
|
1016
|
+
*/
|
|
1017
|
+
type TDbActionDisabledVerdict = boolean | string;
|
|
1009
1018
|
/** `'rows'`-level batch policy — controls whether failing rows reject or are filtered out. */
|
|
1010
1019
|
type TOnDisabledRows = "reject" | "skip";
|
|
1011
1020
|
/**
|
|
@@ -1034,15 +1043,17 @@ interface WithGate<TRow, R extends readonly FlatKey<TRow>[]> {
|
|
|
1034
1043
|
*/
|
|
1035
1044
|
requiredFields: R;
|
|
1036
1045
|
/**
|
|
1037
|
-
* Sync batch gate predicate — returns a parallel
|
|
1038
|
-
*
|
|
1046
|
+
* Sync batch gate predicate — returns a parallel array aligned with the
|
|
1047
|
+
* input. Per entry: truthy = disabled for the corresponding row; a
|
|
1048
|
+
* non-empty string also gives the reason (see
|
|
1049
|
+
* {@link TDbActionDisabledVerdict}). The `rows`
|
|
1039
1050
|
* argument is type-narrowed to `Pick<FlatOf<TRow>, R[number]>[]`; reading
|
|
1040
1051
|
* a field not listed in `requiredFields` is a compile error.
|
|
1041
1052
|
*
|
|
1042
1053
|
* Promise return is NOT permitted — the predicate is consumed in the
|
|
1043
1054
|
* same tick by the gate and the augmenter.
|
|
1044
1055
|
*/
|
|
1045
|
-
disabled?: (rows: DisabledRowsArg<TRow, R>) =>
|
|
1056
|
+
disabled?: (rows: DisabledRowsArg<TRow, R>) => TDbActionDisabledVerdict[];
|
|
1046
1057
|
/**
|
|
1047
1058
|
* `'rows'`-level batch policy. Default `'reject'`.
|
|
1048
1059
|
*
|
|
@@ -1062,7 +1073,7 @@ interface WithGate<TRow, R extends readonly FlatKey<TRow>[]> {
|
|
|
1062
1073
|
*/
|
|
1063
1074
|
interface LooseGate {
|
|
1064
1075
|
requiredFields?: string[];
|
|
1065
|
-
disabled?: (rows: any[]) =>
|
|
1076
|
+
disabled?: (rows: any[]) => TDbActionDisabledVerdict[];
|
|
1066
1077
|
onDisabledRows?: TOnDisabledRows;
|
|
1067
1078
|
}
|
|
1068
1079
|
type GateOpts<TRow, R extends readonly FlatKey<TRow>[]> = unknown extends TRow ? LooseGate : NoGate | WithGate<TRow, R>;
|
|
@@ -1700,17 +1711,21 @@ declare const useDbActionInput: import("@wooksjs/event-core").WookComposable<{
|
|
|
1700
1711
|
* Wire-body shape for server-side gate rejections. The `name` discriminator
|
|
1701
1712
|
* lets `@atscript/db-client` recognise the response and construct the typed
|
|
1702
1713
|
* `ActionDisabledError` subclass. The shape extends Moost's standard
|
|
1703
|
-
* `ServerError` envelope (`{ message, statusCode, errors? }`) with
|
|
1704
|
-
* additional fields:
|
|
1714
|
+
* `ServerError` envelope (`{ message, statusCode, errors? }`) with:
|
|
1705
1715
|
*
|
|
1706
1716
|
* - `name: 'ActionDisabledError'` — discriminator the client matches.
|
|
1707
1717
|
* - `action` — the `@DbAction` name that rejected the request.
|
|
1708
1718
|
* - `id?` — present only for `'row'`-level rejections.
|
|
1709
1719
|
* - `ids?` — present only for `'rows'`-level rejections.
|
|
1720
|
+
* - `reason?` — the reason shared by every rejected row, when the
|
|
1721
|
+
* `disabled` predicate returned one (since 0.1.141).
|
|
1722
|
+
* - `reasons?` — `'rows'` level only: index-aligned with `ids`, `null` where
|
|
1723
|
+
* that row carries no reason. Present only when at least one reason exists
|
|
1724
|
+
* (since 0.1.141).
|
|
1710
1725
|
*
|
|
1711
1726
|
* `message` is populated with a human-readable string so generic
|
|
1712
1727
|
* `ClientError` consumers (which read `body.message`) still get something
|
|
1713
|
-
* useful without typed-catch dispatch.
|
|
1728
|
+
* useful without typed-catch dispatch — the reason itself when there is one.
|
|
1714
1729
|
*/
|
|
1715
1730
|
interface ActionDisabledErrorBody {
|
|
1716
1731
|
name: "ActionDisabledError";
|
|
@@ -1719,27 +1734,37 @@ interface ActionDisabledErrorBody {
|
|
|
1719
1734
|
action: string;
|
|
1720
1735
|
id?: Record<string, unknown>;
|
|
1721
1736
|
ids?: Record<string, unknown>[];
|
|
1737
|
+
/** @since 0.1.141 */
|
|
1738
|
+
reason?: string;
|
|
1739
|
+
/** @since 0.1.141 */
|
|
1740
|
+
reasons?: (string | null)[];
|
|
1722
1741
|
}
|
|
1723
1742
|
/**
|
|
1724
1743
|
* Thrown by the gate interceptor when `disabled` returns truthy. Composes
|
|
1725
1744
|
* with Moost's existing error mapper to produce HTTP 409 with the wire body
|
|
1726
1745
|
* defined by {@link ActionDisabledErrorBody}.
|
|
1727
1746
|
*
|
|
1728
|
-
* - `'row'`-level rejection: pass `(action, id)` — the
|
|
1729
|
-
*
|
|
1730
|
-
*
|
|
1731
|
-
* of
|
|
1747
|
+
* - `'row'`-level rejection: pass `(action, id, undefined, [reason])` — the
|
|
1748
|
+
* body emits `id` (+ `reason` when given).
|
|
1749
|
+
* - `'rows'`-level rejection: pass `(action, undefined, ids, reasons?)` — the
|
|
1750
|
+
* body emits `ids` (the FULL list of failing IDs in reject mode; the FULL
|
|
1751
|
+
* list of request IDs in skip mode with zero survivors) and, when any
|
|
1752
|
+
* reason exists, `reasons` aligned with `ids`.
|
|
1753
|
+
*
|
|
1754
|
+
* `reasons` entries are verdict reasons (`verdictReason`): anything but a
|
|
1755
|
+
* non-empty string counts as "no reason".
|
|
1732
1756
|
*/
|
|
1733
1757
|
declare class ActionDisabledError extends HttpError<ActionDisabledErrorBody> {
|
|
1734
1758
|
name: string;
|
|
1735
|
-
constructor(action: string, id?: Record<string, unknown>, ids?: Record<string, unknown>[]);
|
|
1759
|
+
constructor(action: string, id?: Record<string, unknown>, ids?: Record<string, unknown>[], reasons?: readonly (string | null | undefined)[]);
|
|
1736
1760
|
}
|
|
1737
1761
|
//#endregion
|
|
1738
1762
|
//#region src/actions/per-row.d.ts
|
|
1739
1763
|
/**
|
|
1740
1764
|
* Lift a per-row predicate into the batch shape required by
|
|
1741
1765
|
* `@DbAction` opts.`disabled` and class-level dict `disabled`. Polarity is
|
|
1742
|
-
* preserved — `true` from `fn` means the action is disabled for that row
|
|
1766
|
+
* preserved — `true` from `fn` means the action is disabled for that row; a
|
|
1767
|
+
* non-empty string disables it with that reason.
|
|
1743
1768
|
*
|
|
1744
1769
|
* ```ts
|
|
1745
1770
|
* @DbAction<Order>('archive', {
|
|
@@ -1748,7 +1773,7 @@ declare class ActionDisabledError extends HttpError<ActionDisabledErrorBody> {
|
|
|
1748
1773
|
* })
|
|
1749
1774
|
* ```
|
|
1750
1775
|
*/
|
|
1751
|
-
declare const perRow: <TRow>(fn: (row: TRow) =>
|
|
1776
|
+
declare const perRow: <TRow>(fn: (row: TRow) => TDbActionDisabledVerdict) => (rows: TRow[]) => TDbActionDisabledVerdict[];
|
|
1752
1777
|
//#endregion
|
|
1753
1778
|
//#region src/permissions/crud-controls.d.ts
|
|
1754
1779
|
declare const QUERY_CONTROLS: readonly string[];
|
|
@@ -1785,7 +1810,10 @@ declare function resolveProp(type: TAtscriptAnnotatedType, field: string): TAtsc
|
|
|
1785
1810
|
/**
|
|
1786
1811
|
* Follows `def.ref` hop by hop until a prop without a `ref` (a primary key or
|
|
1787
1812
|
* a plain column) is reached. Cycle-safe (visited on `<typeId>.<field>`) and
|
|
1788
|
-
* bounded by chain length.
|
|
1813
|
+
* bounded by chain length. Only chain hops count — a plain ref (`field: ""`,
|
|
1814
|
+
* a column typed with a named type) is the end of the chain. A hop into a
|
|
1815
|
+
* `@db.alias` type (a view's join alias, since 0.1.141) continues on the
|
|
1816
|
+
* aliased table / view — the alias is no value domain for a picker.
|
|
1789
1817
|
*/
|
|
1790
1818
|
declare function resolveTerminalRef(def: TAtscriptAnnotatedType): TTerminalRef | undefined;
|
|
1791
1819
|
/**
|
|
@@ -1801,4 +1829,4 @@ declare function resolveTerminalRef(def: TAtscriptAnnotatedType): TTerminalRef |
|
|
|
1801
1829
|
*/
|
|
1802
1830
|
declare function applyTerminalRefs(serialized: TSerializedAnnotatedType, runtime: TAtscriptAnnotatedType, options: TSerializeOptions): TSerializedAnnotatedType;
|
|
1803
1831
|
//#endregion
|
|
1804
|
-
export { ActionDisabledError, type ActionDisabledErrorBody, AsDbController, AsDbReadableController, AsJsonValueHelpController, AsReadableController, AsValueHelpController, type AtscriptDbMate, type AtscriptDbMeta, type AtscriptDbParamsMeta, DEFAULT_DB_SPACE, DbAction, DbActionDefault, type DbActionEnvelope, DbActionID, DbActionIDs, type DbActionOpts, DbActionRow, DbActionRows, DbActions, DbRowActions, DbRowsActions, DbTableActions, FieldCapabilityIndex, type IdValidationSource, InputForm, ONE_CONTROLS, PAGES_CONTROLS, QUERY_CONTROLS, READABLE_DEF, ReadableController, TABLE_DEF, TAssertExposedOptions, type TCapabilityReadable, type TCapabilityVerdict, TControllerBindingOptions, type TCrudOp, type TCrudPermissions, type TDbActionInfo, type TDbActionInputFormMeta, type TDbActionIntent, type TDbActionLevel, type TDbActionMeta, type TDbActionParamKind, type TDbActionProcessor, type TDbActionsEntry, type TDbActionsEntryUnpinned, type TDbClassActionMeta, TDbDecorateContext, TDbDecorateEndpoint, type TDbRemoveGuardContext, type TDbWriteAction, type TDbWriteGuardContext, type TFieldCapability, type THttpErrorEntry, type TQueryPathOp, type TQueryPathRefs, TReadableBinding, type TReadableBindingMeta, type TTerminalRef, TableController, UseValidationErrorTransform, ValueHelpQuery, ViewController, applyTerminalRefs, assertExposed, badRequest, clearDbSpaces, collectQueryPaths, dbActionBodySlot, dbActionInputSlot, discoverActions, errorEnvelope, findReadableBinding, getAtscriptDbMate, getControllerFormType, perRow, provideDbSpace, resolveBoundReadable, resolveDbSpace, resolveProp, resolveTerminalRef, useDbActionId, useDbActionIds, useDbActionInput, useDbActionRow, useDbActionRows, validationErrorTransform };
|
|
1832
|
+
export { ActionDisabledError, type ActionDisabledErrorBody, AsDbController, AsDbReadableController, AsJsonValueHelpController, AsReadableController, AsValueHelpController, type AtscriptDbMate, type AtscriptDbMeta, type AtscriptDbParamsMeta, DEFAULT_DB_SPACE, DbAction, DbActionDefault, type DbActionEnvelope, DbActionID, DbActionIDs, type DbActionOpts, DbActionRow, DbActionRows, DbActions, DbRowActions, DbRowsActions, DbTableActions, FieldCapabilityIndex, type IdValidationSource, InputForm, ONE_CONTROLS, PAGES_CONTROLS, QUERY_CONTROLS, READABLE_DEF, ReadableController, TABLE_DEF, TAssertExposedOptions, type TCapabilityReadable, type TCapabilityVerdict, TControllerBindingOptions, type TCrudOp, type TCrudPermissions, type TDbActionDisabledVerdict, type TDbActionInfo, type TDbActionInputFormMeta, type TDbActionIntent, type TDbActionLevel, type TDbActionMeta, type TDbActionParamKind, type TDbActionProcessor, type TDbActionsEntry, type TDbActionsEntryUnpinned, type TDbClassActionMeta, TDbDecorateContext, TDbDecorateEndpoint, type TDbRemoveGuardContext, type TDbWriteAction, type TDbWriteGuardContext, type TFieldCapability, type THttpErrorEntry, type TQueryPathOp, type TQueryPathRefs, TReadableBinding, type TReadableBindingMeta, type TTerminalRef, TableController, UseValidationErrorTransform, ValueHelpQuery, ViewController, applyTerminalRefs, assertExposed, badRequest, clearDbSpaces, collectQueryPaths, dbActionBodySlot, dbActionInputSlot, discoverActions, errorEnvelope, findReadableBinding, getAtscriptDbMate, getControllerFormType, perRow, provideDbSpace, resolveBoundReadable, resolveDbSpace, resolveProp, resolveTerminalRef, useDbActionId, useDbActionIds, useDbActionInput, useDbActionRow, useDbActionRows, validationErrorTransform };
|
package/dist/index.d.mts
CHANGED
|
@@ -77,9 +77,9 @@ declare abstract class AsReadableController<T extends TAtscriptAnnotatedType = T
|
|
|
77
77
|
* The shallow shape emits `{ field, type: { id, metadata } }` for every FK,
|
|
78
78
|
* which carries the target's `db.http.path` so clients can resolve value-help
|
|
79
79
|
* URLs and lazy-fetch target `/meta` when deeper structure is needed. Nav
|
|
80
|
-
* props (`@db.rel.from` / `@db.rel.to` / `@db.rel.via`)
|
|
81
|
-
* and always expand fully regardless of
|
|
82
|
-
* clients need is unaffected.
|
|
80
|
+
* props (`@db.rel.from` / `@db.rel.to` / `@db.rel.via`) carry only a plain
|
|
81
|
+
* `ref` (`field: ""`) and their bodies always expand fully regardless of
|
|
82
|
+
* `refDepth` — the write-payload shape clients need is unaffected.
|
|
83
83
|
*
|
|
84
84
|
* Annotation whitelist: keeps `meta.*`, `expect.*`, and `db.rel.*`; strips
|
|
85
85
|
* other `db.*` (table, column, index, default, etc.). Override in subclass
|
|
@@ -589,7 +589,7 @@ declare class AsDbReadableController<T extends TAtscriptAnnotatedType = TAtscrip
|
|
|
589
589
|
*
|
|
590
590
|
* Convention (not enforced): name decoration keys with a `$` prefix, like
|
|
591
591
|
* `$actions` and `$distance`, so they can never collide with a field name.
|
|
592
|
-
* Do not overwrite `$actions`. Columns the
|
|
592
|
+
* Do not overwrite `$actions` or `$disabledReasons`. Columns the
|
|
593
593
|
* hook needs but the client did not select must be added in
|
|
594
594
|
* {@link transformProjection} — they are then part of the response.
|
|
595
595
|
*
|
|
@@ -1006,6 +1006,15 @@ declare class AsJsonValueHelpController<T extends TAtscriptAnnotatedType = TAtsc
|
|
|
1006
1006
|
}
|
|
1007
1007
|
//#endregion
|
|
1008
1008
|
//#region src/actions/types.d.ts
|
|
1009
|
+
/**
|
|
1010
|
+
* One entry of a `disabled` predicate's result. Truthy = the action is
|
|
1011
|
+
* disabled for that row; falsy (`false`, `""`) = enabled. A non-empty string
|
|
1012
|
+
* disables the action AND carries a human-readable reason — surfaced in the
|
|
1013
|
+
* 409 `ActionDisabledError` message/body and in the row's `$disabledReasons`.
|
|
1014
|
+
*
|
|
1015
|
+
* @since 0.1.141 (`string`)
|
|
1016
|
+
*/
|
|
1017
|
+
type TDbActionDisabledVerdict = boolean | string;
|
|
1009
1018
|
/** `'rows'`-level batch policy — controls whether failing rows reject or are filtered out. */
|
|
1010
1019
|
type TOnDisabledRows = "reject" | "skip";
|
|
1011
1020
|
/**
|
|
@@ -1034,15 +1043,17 @@ interface WithGate<TRow, R extends readonly FlatKey<TRow>[]> {
|
|
|
1034
1043
|
*/
|
|
1035
1044
|
requiredFields: R;
|
|
1036
1045
|
/**
|
|
1037
|
-
* Sync batch gate predicate — returns a parallel
|
|
1038
|
-
*
|
|
1046
|
+
* Sync batch gate predicate — returns a parallel array aligned with the
|
|
1047
|
+
* input. Per entry: truthy = disabled for the corresponding row; a
|
|
1048
|
+
* non-empty string also gives the reason (see
|
|
1049
|
+
* {@link TDbActionDisabledVerdict}). The `rows`
|
|
1039
1050
|
* argument is type-narrowed to `Pick<FlatOf<TRow>, R[number]>[]`; reading
|
|
1040
1051
|
* a field not listed in `requiredFields` is a compile error.
|
|
1041
1052
|
*
|
|
1042
1053
|
* Promise return is NOT permitted — the predicate is consumed in the
|
|
1043
1054
|
* same tick by the gate and the augmenter.
|
|
1044
1055
|
*/
|
|
1045
|
-
disabled?: (rows: DisabledRowsArg<TRow, R>) =>
|
|
1056
|
+
disabled?: (rows: DisabledRowsArg<TRow, R>) => TDbActionDisabledVerdict[];
|
|
1046
1057
|
/**
|
|
1047
1058
|
* `'rows'`-level batch policy. Default `'reject'`.
|
|
1048
1059
|
*
|
|
@@ -1062,7 +1073,7 @@ interface WithGate<TRow, R extends readonly FlatKey<TRow>[]> {
|
|
|
1062
1073
|
*/
|
|
1063
1074
|
interface LooseGate {
|
|
1064
1075
|
requiredFields?: string[];
|
|
1065
|
-
disabled?: (rows: any[]) =>
|
|
1076
|
+
disabled?: (rows: any[]) => TDbActionDisabledVerdict[];
|
|
1066
1077
|
onDisabledRows?: TOnDisabledRows;
|
|
1067
1078
|
}
|
|
1068
1079
|
type GateOpts<TRow, R extends readonly FlatKey<TRow>[]> = unknown extends TRow ? LooseGate : NoGate | WithGate<TRow, R>;
|
|
@@ -1700,17 +1711,21 @@ declare const useDbActionInput: import("@wooksjs/event-core").WookComposable<{
|
|
|
1700
1711
|
* Wire-body shape for server-side gate rejections. The `name` discriminator
|
|
1701
1712
|
* lets `@atscript/db-client` recognise the response and construct the typed
|
|
1702
1713
|
* `ActionDisabledError` subclass. The shape extends Moost's standard
|
|
1703
|
-
* `ServerError` envelope (`{ message, statusCode, errors? }`) with
|
|
1704
|
-
* additional fields:
|
|
1714
|
+
* `ServerError` envelope (`{ message, statusCode, errors? }`) with:
|
|
1705
1715
|
*
|
|
1706
1716
|
* - `name: 'ActionDisabledError'` — discriminator the client matches.
|
|
1707
1717
|
* - `action` — the `@DbAction` name that rejected the request.
|
|
1708
1718
|
* - `id?` — present only for `'row'`-level rejections.
|
|
1709
1719
|
* - `ids?` — present only for `'rows'`-level rejections.
|
|
1720
|
+
* - `reason?` — the reason shared by every rejected row, when the
|
|
1721
|
+
* `disabled` predicate returned one (since 0.1.141).
|
|
1722
|
+
* - `reasons?` — `'rows'` level only: index-aligned with `ids`, `null` where
|
|
1723
|
+
* that row carries no reason. Present only when at least one reason exists
|
|
1724
|
+
* (since 0.1.141).
|
|
1710
1725
|
*
|
|
1711
1726
|
* `message` is populated with a human-readable string so generic
|
|
1712
1727
|
* `ClientError` consumers (which read `body.message`) still get something
|
|
1713
|
-
* useful without typed-catch dispatch.
|
|
1728
|
+
* useful without typed-catch dispatch — the reason itself when there is one.
|
|
1714
1729
|
*/
|
|
1715
1730
|
interface ActionDisabledErrorBody {
|
|
1716
1731
|
name: "ActionDisabledError";
|
|
@@ -1719,27 +1734,37 @@ interface ActionDisabledErrorBody {
|
|
|
1719
1734
|
action: string;
|
|
1720
1735
|
id?: Record<string, unknown>;
|
|
1721
1736
|
ids?: Record<string, unknown>[];
|
|
1737
|
+
/** @since 0.1.141 */
|
|
1738
|
+
reason?: string;
|
|
1739
|
+
/** @since 0.1.141 */
|
|
1740
|
+
reasons?: (string | null)[];
|
|
1722
1741
|
}
|
|
1723
1742
|
/**
|
|
1724
1743
|
* Thrown by the gate interceptor when `disabled` returns truthy. Composes
|
|
1725
1744
|
* with Moost's existing error mapper to produce HTTP 409 with the wire body
|
|
1726
1745
|
* defined by {@link ActionDisabledErrorBody}.
|
|
1727
1746
|
*
|
|
1728
|
-
* - `'row'`-level rejection: pass `(action, id)` — the
|
|
1729
|
-
*
|
|
1730
|
-
*
|
|
1731
|
-
* of
|
|
1747
|
+
* - `'row'`-level rejection: pass `(action, id, undefined, [reason])` — the
|
|
1748
|
+
* body emits `id` (+ `reason` when given).
|
|
1749
|
+
* - `'rows'`-level rejection: pass `(action, undefined, ids, reasons?)` — the
|
|
1750
|
+
* body emits `ids` (the FULL list of failing IDs in reject mode; the FULL
|
|
1751
|
+
* list of request IDs in skip mode with zero survivors) and, when any
|
|
1752
|
+
* reason exists, `reasons` aligned with `ids`.
|
|
1753
|
+
*
|
|
1754
|
+
* `reasons` entries are verdict reasons (`verdictReason`): anything but a
|
|
1755
|
+
* non-empty string counts as "no reason".
|
|
1732
1756
|
*/
|
|
1733
1757
|
declare class ActionDisabledError extends HttpError<ActionDisabledErrorBody> {
|
|
1734
1758
|
name: string;
|
|
1735
|
-
constructor(action: string, id?: Record<string, unknown>, ids?: Record<string, unknown>[]);
|
|
1759
|
+
constructor(action: string, id?: Record<string, unknown>, ids?: Record<string, unknown>[], reasons?: readonly (string | null | undefined)[]);
|
|
1736
1760
|
}
|
|
1737
1761
|
//#endregion
|
|
1738
1762
|
//#region src/actions/per-row.d.ts
|
|
1739
1763
|
/**
|
|
1740
1764
|
* Lift a per-row predicate into the batch shape required by
|
|
1741
1765
|
* `@DbAction` opts.`disabled` and class-level dict `disabled`. Polarity is
|
|
1742
|
-
* preserved — `true` from `fn` means the action is disabled for that row
|
|
1766
|
+
* preserved — `true` from `fn` means the action is disabled for that row; a
|
|
1767
|
+
* non-empty string disables it with that reason.
|
|
1743
1768
|
*
|
|
1744
1769
|
* ```ts
|
|
1745
1770
|
* @DbAction<Order>('archive', {
|
|
@@ -1748,7 +1773,7 @@ declare class ActionDisabledError extends HttpError<ActionDisabledErrorBody> {
|
|
|
1748
1773
|
* })
|
|
1749
1774
|
* ```
|
|
1750
1775
|
*/
|
|
1751
|
-
declare const perRow: <TRow>(fn: (row: TRow) =>
|
|
1776
|
+
declare const perRow: <TRow>(fn: (row: TRow) => TDbActionDisabledVerdict) => (rows: TRow[]) => TDbActionDisabledVerdict[];
|
|
1752
1777
|
//#endregion
|
|
1753
1778
|
//#region src/permissions/crud-controls.d.ts
|
|
1754
1779
|
declare const QUERY_CONTROLS: readonly string[];
|
|
@@ -1785,7 +1810,10 @@ declare function resolveProp(type: TAtscriptAnnotatedType, field: string): TAtsc
|
|
|
1785
1810
|
/**
|
|
1786
1811
|
* Follows `def.ref` hop by hop until a prop without a `ref` (a primary key or
|
|
1787
1812
|
* a plain column) is reached. Cycle-safe (visited on `<typeId>.<field>`) and
|
|
1788
|
-
* bounded by chain length.
|
|
1813
|
+
* bounded by chain length. Only chain hops count — a plain ref (`field: ""`,
|
|
1814
|
+
* a column typed with a named type) is the end of the chain. A hop into a
|
|
1815
|
+
* `@db.alias` type (a view's join alias, since 0.1.141) continues on the
|
|
1816
|
+
* aliased table / view — the alias is no value domain for a picker.
|
|
1789
1817
|
*/
|
|
1790
1818
|
declare function resolveTerminalRef(def: TAtscriptAnnotatedType): TTerminalRef | undefined;
|
|
1791
1819
|
/**
|
|
@@ -1801,4 +1829,4 @@ declare function resolveTerminalRef(def: TAtscriptAnnotatedType): TTerminalRef |
|
|
|
1801
1829
|
*/
|
|
1802
1830
|
declare function applyTerminalRefs(serialized: TSerializedAnnotatedType, runtime: TAtscriptAnnotatedType, options: TSerializeOptions): TSerializedAnnotatedType;
|
|
1803
1831
|
//#endregion
|
|
1804
|
-
export { ActionDisabledError, type ActionDisabledErrorBody, AsDbController, AsDbReadableController, AsJsonValueHelpController, AsReadableController, AsValueHelpController, type AtscriptDbMate, type AtscriptDbMeta, type AtscriptDbParamsMeta, DEFAULT_DB_SPACE, DbAction, DbActionDefault, type DbActionEnvelope, DbActionID, DbActionIDs, type DbActionOpts, DbActionRow, DbActionRows, DbActions, DbRowActions, DbRowsActions, DbTableActions, FieldCapabilityIndex, type IdValidationSource, InputForm, ONE_CONTROLS, PAGES_CONTROLS, QUERY_CONTROLS, READABLE_DEF, ReadableController, TABLE_DEF, TAssertExposedOptions, type TCapabilityReadable, type TCapabilityVerdict, TControllerBindingOptions, type TCrudOp, type TCrudPermissions, type TDbActionInfo, type TDbActionInputFormMeta, type TDbActionIntent, type TDbActionLevel, type TDbActionMeta, type TDbActionParamKind, type TDbActionProcessor, type TDbActionsEntry, type TDbActionsEntryUnpinned, type TDbClassActionMeta, TDbDecorateContext, TDbDecorateEndpoint, type TDbRemoveGuardContext, type TDbWriteAction, type TDbWriteGuardContext, type TFieldCapability, type THttpErrorEntry, type TQueryPathOp, type TQueryPathRefs, TReadableBinding, type TReadableBindingMeta, type TTerminalRef, TableController, UseValidationErrorTransform, ValueHelpQuery, ViewController, applyTerminalRefs, assertExposed, badRequest, clearDbSpaces, collectQueryPaths, dbActionBodySlot, dbActionInputSlot, discoverActions, errorEnvelope, findReadableBinding, getAtscriptDbMate, getControllerFormType, perRow, provideDbSpace, resolveBoundReadable, resolveDbSpace, resolveProp, resolveTerminalRef, useDbActionId, useDbActionIds, useDbActionInput, useDbActionRow, useDbActionRows, validationErrorTransform };
|
|
1832
|
+
export { ActionDisabledError, type ActionDisabledErrorBody, AsDbController, AsDbReadableController, AsJsonValueHelpController, AsReadableController, AsValueHelpController, type AtscriptDbMate, type AtscriptDbMeta, type AtscriptDbParamsMeta, DEFAULT_DB_SPACE, DbAction, DbActionDefault, type DbActionEnvelope, DbActionID, DbActionIDs, type DbActionOpts, DbActionRow, DbActionRows, DbActions, DbRowActions, DbRowsActions, DbTableActions, FieldCapabilityIndex, type IdValidationSource, InputForm, ONE_CONTROLS, PAGES_CONTROLS, QUERY_CONTROLS, READABLE_DEF, ReadableController, TABLE_DEF, TAssertExposedOptions, type TCapabilityReadable, type TCapabilityVerdict, TControllerBindingOptions, type TCrudOp, type TCrudPermissions, type TDbActionDisabledVerdict, type TDbActionInfo, type TDbActionInputFormMeta, type TDbActionIntent, type TDbActionLevel, type TDbActionMeta, type TDbActionParamKind, type TDbActionProcessor, type TDbActionsEntry, type TDbActionsEntryUnpinned, type TDbClassActionMeta, TDbDecorateContext, TDbDecorateEndpoint, type TDbRemoveGuardContext, type TDbWriteAction, type TDbWriteGuardContext, type TFieldCapability, type THttpErrorEntry, type TQueryPathOp, type TQueryPathRefs, TReadableBinding, type TReadableBindingMeta, type TTerminalRef, TableController, UseValidationErrorTransform, ValueHelpQuery, ViewController, applyTerminalRefs, assertExposed, badRequest, clearDbSpaces, collectQueryPaths, dbActionBodySlot, dbActionInputSlot, discoverActions, errorEnvelope, findReadableBinding, getAtscriptDbMate, getControllerFormType, perRow, provideDbSpace, resolveBoundReadable, resolveDbSpace, resolveProp, resolveTerminalRef, useDbActionId, useDbActionIds, useDbActionInput, useDbActionRow, useDbActionRows, validationErrorTransform };
|
package/dist/index.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { ValidatorError, defineAnnotatedType, isAnnotatedType, serializeAnnotate
|
|
|
3
3
|
import { Body, Delete, Get, HttpError, Patch, Post, Put, Query, Url } from "@moostjs/event-http";
|
|
4
4
|
import { ApplyDecorators, Controller, Inherit, Inject, Intercept, Moost, Optional, Param, Provide, Resolve, TInterceptorPriority, defineBeforeInterceptor, defineInterceptor, getMoostMate, useControllerContext } from "moost";
|
|
5
5
|
import { parseUrl } from "@uniqu/url";
|
|
6
|
-
import { ADAPTER_FILTER_REASON, ALL_AGGREGATE_FNS, DbError, ENCRYPTED_REASON, acceptedOperatorsHint, bucketSourceVerdict, canFilterLeaf, checkHavingKeys, classifyQueryPath, collectQueryPaths, collectQueryPaths as collectQueryPaths$1, findAncestorInSet, isJsonValueField, isPlainObject, narrowerFilterOps, normalizeComputedSelect, reconcileCas, unsupportedOperatorMessage } from "@atscript/db";
|
|
6
|
+
import { ADAPTER_FILTER_REASON, ALL_AGGREGATE_FNS, DbError, ENCRYPTED_REASON, acceptedOperatorsHint, aliasTargetOf, bucketSourceVerdict, canFilterLeaf, checkHavingKeys, classifyQueryPath, collectQueryPaths, collectQueryPaths as collectQueryPaths$1, findAncestorInSet, isJsonValueField, isPlainObject, narrowerFilterOps, normalizeComputedSelect, reconcileCas, unsupportedOperatorMessage } from "@atscript/db";
|
|
7
7
|
import { BUCKET_UNITS } from "@uniqu/core";
|
|
8
8
|
import { buildMemoryPredicate, projectRow, sortRows } from "@atscript/db-memory";
|
|
9
9
|
import { cached, current, defineWook, key } from "@wooksjs/event-core";
|
|
@@ -545,14 +545,18 @@ function resolveProp(type, field) {
|
|
|
545
545
|
/**
|
|
546
546
|
* Follows `def.ref` hop by hop until a prop without a `ref` (a primary key or
|
|
547
547
|
* a plain column) is reached. Cycle-safe (visited on `<typeId>.<field>`) and
|
|
548
|
-
* bounded by chain length.
|
|
548
|
+
* bounded by chain length. Only chain hops count — a plain ref (`field: ""`,
|
|
549
|
+
* a column typed with a named type) is the end of the chain. A hop into a
|
|
550
|
+
* `@db.alias` type (a view's join alias, since 0.1.141) continues on the
|
|
551
|
+
* aliased table / view — the alias is no value domain for a picker.
|
|
549
552
|
*/
|
|
550
553
|
function resolveTerminalRef(def) {
|
|
551
554
|
const ref = def.ref;
|
|
552
|
-
if (!ref) return void 0;
|
|
555
|
+
if (!ref?.field) return void 0;
|
|
553
556
|
let type = ref.type();
|
|
554
557
|
let field = ref.field;
|
|
555
558
|
if (!type) return void 0;
|
|
559
|
+
type = aliasTargetOf(type) ?? type;
|
|
556
560
|
let fk = def.metadata.has("db.rel.FK");
|
|
557
561
|
const visited = new Set([`${type.id ?? ""}.${field}`]);
|
|
558
562
|
for (;;) {
|
|
@@ -560,9 +564,10 @@ function resolveTerminalRef(def) {
|
|
|
560
564
|
if (!prop) break;
|
|
561
565
|
if (prop.metadata.has("db.rel.FK")) fk = true;
|
|
562
566
|
const next = prop.ref;
|
|
563
|
-
if (!next) break;
|
|
564
|
-
const
|
|
565
|
-
if (!
|
|
567
|
+
if (!next?.field) break;
|
|
568
|
+
const resolved = next.type();
|
|
569
|
+
if (!resolved) break;
|
|
570
|
+
const nextType = aliasTargetOf(resolved) ?? resolved;
|
|
566
571
|
const key = `${nextType.id ?? ""}.${next.field}`;
|
|
567
572
|
if (visited.has(key)) break;
|
|
568
573
|
visited.add(key);
|
|
@@ -614,7 +619,7 @@ function walk(node, def, shallow) {
|
|
|
614
619
|
const sProp = sType.props[name];
|
|
615
620
|
if (!sProp) continue;
|
|
616
621
|
if (isNav(prop.metadata)) continue;
|
|
617
|
-
if (prop.ref && sProp.ref && !("type" in sProp.ref.type)) {
|
|
622
|
+
if (prop.ref?.field && sProp.ref && !("type" in sProp.ref.type)) {
|
|
618
623
|
const terminal = resolveTerminalRef(prop);
|
|
619
624
|
if (terminal) {
|
|
620
625
|
const direct = prop.ref.type();
|
|
@@ -731,9 +736,9 @@ let AsReadableController = class AsReadableController {
|
|
|
731
736
|
* The shallow shape emits `{ field, type: { id, metadata } }` for every FK,
|
|
732
737
|
* which carries the target's `db.http.path` so clients can resolve value-help
|
|
733
738
|
* URLs and lazy-fetch target `/meta` when deeper structure is needed. Nav
|
|
734
|
-
* props (`@db.rel.from` / `@db.rel.to` / `@db.rel.via`)
|
|
735
|
-
* and always expand fully regardless of
|
|
736
|
-
* clients need is unaffected.
|
|
739
|
+
* props (`@db.rel.from` / `@db.rel.to` / `@db.rel.via`) carry only a plain
|
|
740
|
+
* `ref` (`field: ""`) and their bodies always expand fully regardless of
|
|
741
|
+
* `refDepth` — the write-payload shape clients need is unaffected.
|
|
737
742
|
*
|
|
738
743
|
* Annotation whitelist: keeps `meta.*`, `expect.*`, and `db.rel.*`; strips
|
|
739
744
|
* other `db.*` (table, column, index, default, etc.). Override in subclass
|
|
@@ -972,10 +977,14 @@ AsReadableController = __decorate([UseValidationErrorTransform(), __decorateMeta
|
|
|
972
977
|
])], AsReadableController);
|
|
973
978
|
//#endregion
|
|
974
979
|
//#region src/actions/verdict.ts
|
|
975
|
-
/** Assert that a `disabled` predicate returned
|
|
980
|
+
/** Assert that a `disabled` predicate returned an array of the expected length; throws HTTP 500 otherwise. */
|
|
976
981
|
function assertVerdictLength(action, verdicts, expected) {
|
|
977
982
|
if (!Array.isArray(verdicts) || verdicts.length !== expected) throw new HttpError(500, `Action "${action}" disabled predicate returned an invalid verdict array`);
|
|
978
983
|
}
|
|
984
|
+
/** The reason a verdict carries — a non-empty string; `undefined` for `true` / falsy verdicts. */
|
|
985
|
+
function verdictReason(verdict) {
|
|
986
|
+
return typeof verdict === "string" && verdict !== "" ? verdict : void 0;
|
|
987
|
+
}
|
|
979
988
|
//#endregion
|
|
980
989
|
//#region src/actions/list-augmenter.ts
|
|
981
990
|
const candidateCache = /* @__PURE__ */ new WeakMap();
|
|
@@ -1016,7 +1025,8 @@ function computeStripFields(candidates, resolvedProjection) {
|
|
|
1016
1025
|
return strip;
|
|
1017
1026
|
}
|
|
1018
1027
|
/**
|
|
1019
|
-
* Sets `$actions` on every row
|
|
1028
|
+
* Sets `$actions` on every row (plus `$disabledReasons` on rows where a
|
|
1029
|
+
* predicate returned a reason string) and strips the columns fetched only for an
|
|
1020
1030
|
* action's `requiredFields` — IN PLACE; returns the same array, typed as
|
|
1021
1031
|
* augmented.
|
|
1022
1032
|
*/
|
|
@@ -1033,15 +1043,19 @@ function augmentRowsWithActions(args) {
|
|
|
1033
1043
|
for (let i = 0; i < rows.length; i++) {
|
|
1034
1044
|
const row = rows[i];
|
|
1035
1045
|
const names = [];
|
|
1046
|
+
let reasons;
|
|
1036
1047
|
for (let j = 0; j < candidates.length; j++) {
|
|
1037
|
-
const
|
|
1038
|
-
|
|
1039
|
-
|
|
1048
|
+
const name = candidates[j].envelope.info.name;
|
|
1049
|
+
const verdict = verdicts[j]?.[i];
|
|
1050
|
+
if (!verdict) {
|
|
1051
|
+
names.push(name);
|
|
1040
1052
|
continue;
|
|
1041
1053
|
}
|
|
1042
|
-
|
|
1054
|
+
const reason = verdictReason(verdict);
|
|
1055
|
+
if (reason !== void 0) (reasons ??= {})[name] = reason;
|
|
1043
1056
|
}
|
|
1044
1057
|
row.$actions = names;
|
|
1058
|
+
if (reasons) row.$disabledReasons = reasons;
|
|
1045
1059
|
}
|
|
1046
1060
|
if (resolvedProjection !== null) {
|
|
1047
1061
|
const stripFields = computeStripFields(candidates, resolvedProjection);
|
|
@@ -2432,6 +2446,7 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
|
|
|
2432
2446
|
if (geoIndexedPhysical.has(fd.physicalName)) entry.geo = true;
|
|
2433
2447
|
if (this._writeOnlySet.has(path)) entry.writeOnly = true;
|
|
2434
2448
|
if (cap.bucketable) entry.bucketable = true;
|
|
2449
|
+
if (fd.derived) entry.derived = true;
|
|
2435
2450
|
fields[path] = entry;
|
|
2436
2451
|
}
|
|
2437
2452
|
return {
|
|
@@ -3115,6 +3130,7 @@ function assertExposed(app, models, options) {
|
|
|
3115
3130
|
}
|
|
3116
3131
|
const missing = [];
|
|
3117
3132
|
for (const model of models) {
|
|
3133
|
+
if (aliasTargetOf(model)) continue;
|
|
3118
3134
|
const httpPath = model.metadata.get("db.http.path");
|
|
3119
3135
|
if (!auditAll && httpPath === void 0) continue;
|
|
3120
3136
|
if (excluded.has(model) || exposed.has(model)) continue;
|
|
@@ -3126,31 +3142,58 @@ function assertExposed(app, models, options) {
|
|
|
3126
3142
|
}
|
|
3127
3143
|
//#endregion
|
|
3128
3144
|
//#region src/actions/action-disabled-error.ts
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3145
|
+
/** Distinct reasons quoted in a mixed `'rows'` message before `(+N more)`. */
|
|
3146
|
+
const MAX_LISTED_REASONS = 3;
|
|
3147
|
+
function sharedReason(reasons) {
|
|
3148
|
+
const first = reasons[0];
|
|
3149
|
+
if (first === null || first === void 0) return void 0;
|
|
3150
|
+
return reasons.every((r) => r === first) ? first : void 0;
|
|
3151
|
+
}
|
|
3152
|
+
function rowsMessage(action, count, reasons) {
|
|
3153
|
+
const base = `Action "${action}" is disabled for ${count} of the selected rows`;
|
|
3154
|
+
const distinct = [...new Set(reasons.filter((r) => r !== null))];
|
|
3155
|
+
if (distinct.length === 0) return base;
|
|
3156
|
+
const listed = distinct.slice(0, MAX_LISTED_REASONS).join("; ");
|
|
3157
|
+
const more = distinct.length - MAX_LISTED_REASONS;
|
|
3158
|
+
return `${base}: ${listed}${more > 0 ? ` (+${more} more)` : ""}`;
|
|
3132
3159
|
}
|
|
3133
3160
|
/**
|
|
3134
3161
|
* Thrown by the gate interceptor when `disabled` returns truthy. Composes
|
|
3135
3162
|
* with Moost's existing error mapper to produce HTTP 409 with the wire body
|
|
3136
3163
|
* defined by {@link ActionDisabledErrorBody}.
|
|
3137
3164
|
*
|
|
3138
|
-
* - `'row'`-level rejection: pass `(action, id)` — the
|
|
3139
|
-
*
|
|
3140
|
-
*
|
|
3141
|
-
* of
|
|
3165
|
+
* - `'row'`-level rejection: pass `(action, id, undefined, [reason])` — the
|
|
3166
|
+
* body emits `id` (+ `reason` when given).
|
|
3167
|
+
* - `'rows'`-level rejection: pass `(action, undefined, ids, reasons?)` — the
|
|
3168
|
+
* body emits `ids` (the FULL list of failing IDs in reject mode; the FULL
|
|
3169
|
+
* list of request IDs in skip mode with zero survivors) and, when any
|
|
3170
|
+
* reason exists, `reasons` aligned with `ids`.
|
|
3171
|
+
*
|
|
3172
|
+
* `reasons` entries are verdict reasons (`verdictReason`): anything but a
|
|
3173
|
+
* non-empty string counts as "no reason".
|
|
3142
3174
|
*/
|
|
3143
3175
|
var ActionDisabledError = class extends HttpError {
|
|
3144
3176
|
name = "ActionDisabledError";
|
|
3145
|
-
constructor(action, id, ids) {
|
|
3177
|
+
constructor(action, id, ids, reasons) {
|
|
3146
3178
|
const body = {
|
|
3147
3179
|
name: "ActionDisabledError",
|
|
3148
|
-
message:
|
|
3180
|
+
message: "",
|
|
3149
3181
|
statusCode: 409,
|
|
3150
3182
|
action
|
|
3151
3183
|
};
|
|
3152
|
-
if (ids !== void 0)
|
|
3153
|
-
|
|
3184
|
+
if (ids !== void 0) {
|
|
3185
|
+
const aligned = ids.map((_, i) => verdictReason(reasons?.[i]) ?? null);
|
|
3186
|
+
const reason = sharedReason(aligned);
|
|
3187
|
+
body.message = reason ?? rowsMessage(action, ids.length, aligned);
|
|
3188
|
+
body.ids = ids;
|
|
3189
|
+
if (reason !== void 0) body.reason = reason;
|
|
3190
|
+
if (aligned.some((r) => r !== null)) body.reasons = aligned;
|
|
3191
|
+
} else {
|
|
3192
|
+
const reason = verdictReason(reasons?.[0]);
|
|
3193
|
+
body.message = reason ?? `Action "${action}" is disabled for this row`;
|
|
3194
|
+
if (id !== void 0) body.id = id;
|
|
3195
|
+
if (reason !== void 0) body.reason = reason;
|
|
3196
|
+
}
|
|
3154
3197
|
super(409, body);
|
|
3155
3198
|
}
|
|
3156
3199
|
};
|
|
@@ -3441,7 +3484,7 @@ function buildGateInterceptor(opts) {
|
|
|
3441
3484
|
if (level === "row") {
|
|
3442
3485
|
const verdicts = disabled([await ctx.get(dbActionRowSlot)]);
|
|
3443
3486
|
assertVerdictLength(action, verdicts, 1);
|
|
3444
|
-
if (verdicts[0]) throw new ActionDisabledError(action, await ctx.get(dbActionIdSlot));
|
|
3487
|
+
if (verdicts[0]) throw new ActionDisabledError(action, await ctx.get(dbActionIdSlot), void 0, [verdictReason(verdicts[0])]);
|
|
3445
3488
|
return;
|
|
3446
3489
|
}
|
|
3447
3490
|
const ids = await ctx.get(dbActionIdsSlot);
|
|
@@ -3451,26 +3494,30 @@ function buildGateInterceptor(opts) {
|
|
|
3451
3494
|
const verdicts = disabled(existingRows);
|
|
3452
3495
|
assertVerdictLength(action, verdicts, existingRows.length);
|
|
3453
3496
|
const failingIds = [];
|
|
3497
|
+
const failingReasons = [];
|
|
3454
3498
|
const passingRows = [];
|
|
3455
3499
|
const passingIds = [];
|
|
3456
3500
|
let verdictIndex = 0;
|
|
3457
3501
|
for (let i = 0; i < ids.length; i++) {
|
|
3458
3502
|
const row = rows[i];
|
|
3459
|
-
|
|
3460
|
-
|
|
3503
|
+
const verdict = row === void 0 ? void 0 : verdicts[verdictIndex++];
|
|
3504
|
+
if (row === void 0 || verdict) {
|
|
3505
|
+
failingIds.push(ids[i]);
|
|
3506
|
+
failingReasons.push(verdictReason(verdict));
|
|
3507
|
+
} else {
|
|
3461
3508
|
passingRows.push(row);
|
|
3462
3509
|
passingIds.push(ids[i]);
|
|
3463
3510
|
}
|
|
3464
3511
|
}
|
|
3465
3512
|
if (onDisabledRows === "skip") {
|
|
3466
|
-
if (passingRows.length === 0) throw new ActionDisabledError(action, void 0, [...ids]);
|
|
3513
|
+
if (passingRows.length === 0) throw new ActionDisabledError(action, void 0, [...ids], failingReasons);
|
|
3467
3514
|
if (failingIds.length > 0) {
|
|
3468
3515
|
ctx.set(dbActionRowsSlot, Promise.resolve(passingRows));
|
|
3469
3516
|
ctx.set(dbActionIdsSlot, Promise.resolve(passingIds));
|
|
3470
3517
|
}
|
|
3471
3518
|
return;
|
|
3472
3519
|
}
|
|
3473
|
-
if (failingIds.length > 0) throw new ActionDisabledError(action, void 0, failingIds);
|
|
3520
|
+
if (failingIds.length > 0) throw new ActionDisabledError(action, void 0, failingIds, failingReasons);
|
|
3474
3521
|
}, GATE_PRIORITY);
|
|
3475
3522
|
}
|
|
3476
3523
|
/** Thin interceptor for `@DbActionRow*` without `disabled` — injects only the bound table. */
|
|
@@ -3759,7 +3806,8 @@ function InputForm(formType, validatorOpts) {
|
|
|
3759
3806
|
/**
|
|
3760
3807
|
* Lift a per-row predicate into the batch shape required by
|
|
3761
3808
|
* `@DbAction` opts.`disabled` and class-level dict `disabled`. Polarity is
|
|
3762
|
-
* preserved — `true` from `fn` means the action is disabled for that row
|
|
3809
|
+
* preserved — `true` from `fn` means the action is disabled for that row; a
|
|
3810
|
+
* non-empty string disables it with that reason.
|
|
3763
3811
|
*
|
|
3764
3812
|
* ```ts
|
|
3765
3813
|
* @DbAction<Order>('archive', {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atscript/moost-db",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.141",
|
|
4
4
|
"description": "Generic database controller for Moost with Atscript.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"annotations",
|
|
@@ -44,27 +44,27 @@
|
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"@uniqu/url": "^0.1.11",
|
|
47
|
-
"@atscript/db-memory": "^0.1.
|
|
47
|
+
"@atscript/db-memory": "^0.1.141"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
|
-
"@atscript/core": "^0.1.
|
|
51
|
-
"@atscript/typescript": "^0.1.
|
|
50
|
+
"@atscript/core": "^0.1.95",
|
|
51
|
+
"@atscript/typescript": "^0.1.95",
|
|
52
52
|
"@moostjs/event-http": "^0.6.37",
|
|
53
53
|
"@uniqu/core": "^0.1.11",
|
|
54
54
|
"@wooksjs/event-core": "^0.7.23",
|
|
55
55
|
"@wooksjs/event-http": "^0.7.23",
|
|
56
56
|
"@wooksjs/http-body": "^0.7.23",
|
|
57
57
|
"moost": "^0.6.37",
|
|
58
|
-
"unplugin-atscript": "^0.1.
|
|
58
|
+
"unplugin-atscript": "^0.1.95"
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
|
-
"@atscript/typescript": "^0.1.
|
|
61
|
+
"@atscript/typescript": "^0.1.95",
|
|
62
62
|
"@moostjs/event-http": "^0.6.37",
|
|
63
63
|
"@uniqu/core": "^0.1.11",
|
|
64
64
|
"@wooksjs/event-core": "^0.7.23",
|
|
65
65
|
"@wooksjs/http-body": "^0.7.23",
|
|
66
66
|
"moost": "^0.6.37",
|
|
67
|
-
"@atscript/db": "^0.1.
|
|
67
|
+
"@atscript/db": "^0.1.141"
|
|
68
68
|
},
|
|
69
69
|
"scripts": {
|
|
70
70
|
"postinstall": "asc -f dts",
|