@atscript/moost-db 0.1.62 → 0.1.64
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 +97 -6
- package/dist/index.d.cts +25 -0
- package/dist/index.d.mts +25 -0
- package/dist/index.mjs +97 -6
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -691,6 +691,42 @@ let AsReadableController = class AsReadableController {
|
|
|
691
691
|
const idx = url.indexOf("?");
|
|
692
692
|
return (0, _uniqu_url.parseUrl)(idx >= 0 ? url.slice(idx + 1) : "");
|
|
693
693
|
}
|
|
694
|
+
/**
|
|
695
|
+
* Parse a URL keeping only `$*` control keywords; report whether any
|
|
696
|
+
* non-control parts were present. Used by `/one` routes where the
|
|
697
|
+
* uniquery lexer cannot tokenise PK values containing `-` and other
|
|
698
|
+
* reserved chars, so non-control parts must be stripped before lexing.
|
|
699
|
+
* `/one/:id` rejects stray filter params with 400 via `hasNonControl`;
|
|
700
|
+
* `/one` (composite) ignores it because the composite-key params have
|
|
701
|
+
* already been extracted via `@Query()`.
|
|
702
|
+
*/
|
|
703
|
+
parseControlsOnlyFromUrl(url) {
|
|
704
|
+
const idx = url.indexOf("?");
|
|
705
|
+
const qs = idx >= 0 ? url.slice(idx + 1) : "";
|
|
706
|
+
if (!qs) return {
|
|
707
|
+
parsed: (0, _uniqu_url.parseUrl)(""),
|
|
708
|
+
hasNonControl: false
|
|
709
|
+
};
|
|
710
|
+
const kept = [];
|
|
711
|
+
let hasNonControl = false;
|
|
712
|
+
for (const part of qs.split("&")) {
|
|
713
|
+
if (!part) continue;
|
|
714
|
+
const eq = part.indexOf("=");
|
|
715
|
+
const rawKey = eq === -1 ? part : part.slice(0, eq);
|
|
716
|
+
let key;
|
|
717
|
+
try {
|
|
718
|
+
key = decodeURIComponent(rawKey);
|
|
719
|
+
} catch {
|
|
720
|
+
key = rawKey;
|
|
721
|
+
}
|
|
722
|
+
if (key.startsWith("$")) kept.push(part);
|
|
723
|
+
else hasNonControl = true;
|
|
724
|
+
}
|
|
725
|
+
return {
|
|
726
|
+
parsed: (0, _uniqu_url.parseUrl)(kept.join("&")),
|
|
727
|
+
hasNonControl
|
|
728
|
+
};
|
|
729
|
+
}
|
|
694
730
|
async returnOne(result) {
|
|
695
731
|
const item = await result;
|
|
696
732
|
if (!item) return new _moostjs_event_http.HttpError(404);
|
|
@@ -955,14 +991,27 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
|
|
|
955
991
|
_gates;
|
|
956
992
|
_preferredIdSet;
|
|
957
993
|
_overlayIsNoOp;
|
|
994
|
+
/** path → sibling-ref path for `@db.amount.currency.ref` / `@db.unit.ref`. */
|
|
995
|
+
_quantityRefByPath;
|
|
958
996
|
constructor(readable, app) {
|
|
959
997
|
super(readable.type, readable.tableName, app, readable.isView ? "view" : "table");
|
|
960
998
|
this.readable = readable;
|
|
961
999
|
this._gates = this._buildGates();
|
|
962
1000
|
this._preferredIdSet = new Set(readable.preferredId ?? []);
|
|
1001
|
+
this._quantityRefByPath = this._collectQuantityRefs();
|
|
963
1002
|
const defaultOverlay = AsReadableController.prototype.applyMetaOverlay;
|
|
964
1003
|
this._overlayIsNoOp = this.applyMetaOverlay === defaultOverlay;
|
|
965
1004
|
}
|
|
1005
|
+
_collectQuantityRefs() {
|
|
1006
|
+
const out = /* @__PURE__ */ new Map();
|
|
1007
|
+
if (!this.readable.flatMap) return out;
|
|
1008
|
+
for (const [path, entry] of this.readable.flatMap) {
|
|
1009
|
+
const meta = entry?.metadata;
|
|
1010
|
+
const ref = meta?.get("db.amount.currency.ref") ?? meta?.get("db.unit.ref");
|
|
1011
|
+
if (ref) out.set(path, ref);
|
|
1012
|
+
}
|
|
1013
|
+
return out;
|
|
1014
|
+
}
|
|
966
1015
|
_buildGates() {
|
|
967
1016
|
const meta = this.readable.type.metadata;
|
|
968
1017
|
const gates = {};
|
|
@@ -1030,9 +1079,11 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
|
|
|
1030
1079
|
return projection;
|
|
1031
1080
|
}
|
|
1032
1081
|
widenPreferredIdProjection(projection) {
|
|
1033
|
-
|
|
1034
|
-
if (
|
|
1035
|
-
|
|
1082
|
+
const widened = this.widenQuantityRefProjection(projection);
|
|
1083
|
+
if (widened instanceof _moostjs_event_http.HttpError) return widened;
|
|
1084
|
+
if (this._preferredIdSet.size === 0 || widened === void 0) return widened;
|
|
1085
|
+
if (Array.isArray(widened)) return this._widenArrayProjection(widened);
|
|
1086
|
+
return this._widenMapProjection(widened);
|
|
1036
1087
|
}
|
|
1037
1088
|
_widenArrayProjection(projection) {
|
|
1038
1089
|
const stringItems = /* @__PURE__ */ new Set();
|
|
@@ -1072,6 +1123,46 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
|
|
|
1072
1123
|
for (const field of this._preferredIdSet) widened[field] = 1;
|
|
1073
1124
|
return widened;
|
|
1074
1125
|
}
|
|
1126
|
+
/**
|
|
1127
|
+
* Auto-includes the sibling-ref field whenever its `@db.amount.currency.ref`
|
|
1128
|
+
* / `@db.unit.ref` quantity is selected — UI must never get a value without
|
|
1129
|
+
* its dimension. No-op when `$select` is undefined (full row covers it).
|
|
1130
|
+
*/
|
|
1131
|
+
widenQuantityRefProjection(projection) {
|
|
1132
|
+
if (this._quantityRefByPath.size === 0 || projection === void 0) return projection;
|
|
1133
|
+
if (Array.isArray(projection)) return this._widenQuantityArrayProjection(projection);
|
|
1134
|
+
return this._widenQuantityMapProjection(projection);
|
|
1135
|
+
}
|
|
1136
|
+
_widenQuantityArrayProjection(projection) {
|
|
1137
|
+
const stringItems = /* @__PURE__ */ new Set();
|
|
1138
|
+
for (const item of projection) if (typeof item === "string") stringItems.add(item);
|
|
1139
|
+
const toAdd = [];
|
|
1140
|
+
for (const [valuePath, refPath] of this._quantityRefByPath) if (stringItems.has(valuePath) && !stringItems.has(refPath)) {
|
|
1141
|
+
toAdd.push(refPath);
|
|
1142
|
+
stringItems.add(refPath);
|
|
1143
|
+
}
|
|
1144
|
+
if (toAdd.length === 0) return projection;
|
|
1145
|
+
return [...projection, ...toAdd];
|
|
1146
|
+
}
|
|
1147
|
+
_widenQuantityMapProjection(projection) {
|
|
1148
|
+
const entries = Object.entries(projection);
|
|
1149
|
+
if (entries.length === 0) return projection;
|
|
1150
|
+
const included = /* @__PURE__ */ new Set();
|
|
1151
|
+
const excluded = /* @__PURE__ */ new Set();
|
|
1152
|
+
for (const [k, v] of entries) if (v === 1 || v === true) included.add(k);
|
|
1153
|
+
else if (v === 0 || v === false) excluded.add(k);
|
|
1154
|
+
if (included.size > 0 && excluded.size > 0) return new _moostjs_event_http.HttpError(400, "Mixed inclusion/exclusion $select maps are not supported");
|
|
1155
|
+
if (excluded.size === 0) {
|
|
1156
|
+
const toAdd = [];
|
|
1157
|
+
for (const [valuePath, refPath] of this._quantityRefByPath) if (included.has(valuePath) && !included.has(refPath)) toAdd.push(refPath);
|
|
1158
|
+
if (toAdd.length === 0) return projection;
|
|
1159
|
+
const widened = {};
|
|
1160
|
+
for (const k of included) widened[k] = 1;
|
|
1161
|
+
for (const k of toAdd) widened[k] = 1;
|
|
1162
|
+
return widened;
|
|
1163
|
+
}
|
|
1164
|
+
return projection;
|
|
1165
|
+
}
|
|
1075
1166
|
/** WHY: the URL parser only auto-coerces `$count`; every other boolean control reaches us as `"true"`/`"1"` and would fail DTO validation. */
|
|
1076
1167
|
_coerceActionsControl(controls) {
|
|
1077
1168
|
const v = controls.$actions;
|
|
@@ -1298,9 +1389,9 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
|
|
|
1298
1389
|
* **GET /one/:id** — retrieves a single record by ID or unique property.
|
|
1299
1390
|
*/
|
|
1300
1391
|
async getOne(id, url) {
|
|
1301
|
-
const parsed = this.
|
|
1392
|
+
const { parsed, hasNonControl } = this.parseControlsOnlyFromUrl(url);
|
|
1393
|
+
if (hasNonControl) return new _moostjs_event_http.HttpError(400, "Filtering is not allowed for \"one\" endpoint");
|
|
1302
1394
|
this._coerceActionsControl(parsed.controls);
|
|
1303
|
-
if (Object.keys(parsed.filter).length > 0) return new _moostjs_event_http.HttpError(400, "Filtering is not allowed for \"one\" endpoint");
|
|
1304
1395
|
const error = this.validateParsed(parsed, "getOne");
|
|
1305
1396
|
if (error) return error;
|
|
1306
1397
|
const rawSelect = await this.transformProjection(parsed.controls.$select);
|
|
@@ -1315,7 +1406,7 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
|
|
|
1315
1406
|
async getOneComposite(query, url) {
|
|
1316
1407
|
const idObj = this.extractIdShape(query);
|
|
1317
1408
|
if (idObj instanceof _moostjs_event_http.HttpError) return idObj;
|
|
1318
|
-
const parsed = this.
|
|
1409
|
+
const { parsed } = this.parseControlsOnlyFromUrl(url);
|
|
1319
1410
|
this._coerceActionsControl(parsed.controls);
|
|
1320
1411
|
const rawSelect = await this.transformProjection(parsed.controls.$select);
|
|
1321
1412
|
const select = this.widenPreferredIdProjection(rawSelect);
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as _uniqu_url0 from "@uniqu/url";
|
|
2
|
+
import { parseUrl } from "@uniqu/url";
|
|
2
3
|
import { TAtscriptAnnotatedType, TAtscriptDataType, TSerializeOptions, TSerializedAnnotatedType, Validator } from "@atscript/typescript/utils";
|
|
3
4
|
import { AtscriptDbReadable, AtscriptDbTable, FilterExpr, FlatOf, TCrudOp, TCrudPermissions, TCrudPermissions as TCrudPermissions$1, TDbActionInfo, TDbActionInfo as TDbActionInfo$1, TDbActionIntent, TDbActionIntent as TDbActionIntent$1, TDbActionLevel, TDbActionLevel as TDbActionLevel$1, TDbActionProcessor, TDbFieldMeta, TIdentification, TMetaResponse, Uniquery, UniqueryControls } from "@atscript/db";
|
|
4
5
|
import { HttpError } from "@moostjs/event-http";
|
|
@@ -106,6 +107,19 @@ declare abstract class AsReadableController<T extends TAtscriptAnnotatedType = T
|
|
|
106
107
|
*/
|
|
107
108
|
protected checkGates(filter: FilterExpr | undefined, controls: Record<string, unknown>, gates: ReadableGates): HttpError | undefined;
|
|
108
109
|
protected parseQueryString(url: string): _uniqu_url0.UrlQuery;
|
|
110
|
+
/**
|
|
111
|
+
* Parse a URL keeping only `$*` control keywords; report whether any
|
|
112
|
+
* non-control parts were present. Used by `/one` routes where the
|
|
113
|
+
* uniquery lexer cannot tokenise PK values containing `-` and other
|
|
114
|
+
* reserved chars, so non-control parts must be stripped before lexing.
|
|
115
|
+
* `/one/:id` rejects stray filter params with 400 via `hasNonControl`;
|
|
116
|
+
* `/one` (composite) ignores it because the composite-key params have
|
|
117
|
+
* already been extracted via `@Query()`.
|
|
118
|
+
*/
|
|
119
|
+
protected parseControlsOnlyFromUrl(url: string): {
|
|
120
|
+
parsed: ReturnType<typeof parseUrl>;
|
|
121
|
+
hasNonControl: boolean;
|
|
122
|
+
};
|
|
109
123
|
protected returnOne(result: Promise<DataType | null>): Promise<DataType | HttpError>;
|
|
110
124
|
/**
|
|
111
125
|
* **GET /meta** — returns the bound interface's metadata envelope. The
|
|
@@ -166,7 +180,10 @@ declare class AsDbReadableController<T extends TAtscriptAnnotatedType = TAtscrip
|
|
|
166
180
|
private readonly _gates;
|
|
167
181
|
private readonly _preferredIdSet;
|
|
168
182
|
private readonly _overlayIsNoOp;
|
|
183
|
+
/** path → sibling-ref path for `@db.amount.currency.ref` / `@db.unit.ref`. */
|
|
184
|
+
private readonly _quantityRefByPath;
|
|
169
185
|
constructor(readable: AtscriptDbReadable<T>, app: Moost);
|
|
186
|
+
private _collectQuantityRefs;
|
|
170
187
|
private _buildGates;
|
|
171
188
|
private _collectAnnotated;
|
|
172
189
|
protected hasField(path: string): boolean;
|
|
@@ -191,6 +208,14 @@ declare class AsDbReadableController<T extends TAtscriptAnnotatedType = TAtscrip
|
|
|
191
208
|
private widenPreferredIdProjection;
|
|
192
209
|
private _widenArrayProjection;
|
|
193
210
|
private _widenMapProjection;
|
|
211
|
+
/**
|
|
212
|
+
* Auto-includes the sibling-ref field whenever its `@db.amount.currency.ref`
|
|
213
|
+
* / `@db.unit.ref` quantity is selected — UI must never get a value without
|
|
214
|
+
* its dimension. No-op when `$select` is undefined (full row covers it).
|
|
215
|
+
*/
|
|
216
|
+
private widenQuantityRefProjection;
|
|
217
|
+
private _widenQuantityArrayProjection;
|
|
218
|
+
private _widenQuantityMapProjection;
|
|
194
219
|
/** WHY: the URL parser only auto-coerces `$count`; every other boolean control reaches us as `"true"`/`"1"` and would fail DTO validation. */
|
|
195
220
|
private _coerceActionsControl;
|
|
196
221
|
/** Normalize a post-`widenPreferredIdProjection` $select into `string[] | null` (`null` = all fields). */
|
package/dist/index.d.mts
CHANGED
|
@@ -3,6 +3,7 @@ import { HttpError } from "@moostjs/event-http";
|
|
|
3
3
|
import * as moost from "moost";
|
|
4
4
|
import { Moost, TConsoleBase } from "moost";
|
|
5
5
|
import * as _uniqu_url0 from "@uniqu/url";
|
|
6
|
+
import { parseUrl } from "@uniqu/url";
|
|
6
7
|
import { AtscriptDbReadable, AtscriptDbTable, FilterExpr, FlatOf, TCrudOp, TCrudPermissions, TCrudPermissions as TCrudPermissions$1, TDbActionInfo, TDbActionInfo as TDbActionInfo$1, TDbActionIntent, TDbActionIntent as TDbActionIntent$1, TDbActionLevel, TDbActionLevel as TDbActionLevel$1, TDbActionProcessor, TDbFieldMeta, TIdentification, TMetaResponse, Uniquery, UniqueryControls } from "@atscript/db";
|
|
7
8
|
import * as _wooksjs_event_core0 from "@wooksjs/event-core";
|
|
8
9
|
|
|
@@ -106,6 +107,19 @@ declare abstract class AsReadableController<T extends TAtscriptAnnotatedType = T
|
|
|
106
107
|
*/
|
|
107
108
|
protected checkGates(filter: FilterExpr | undefined, controls: Record<string, unknown>, gates: ReadableGates): HttpError | undefined;
|
|
108
109
|
protected parseQueryString(url: string): _uniqu_url0.UrlQuery;
|
|
110
|
+
/**
|
|
111
|
+
* Parse a URL keeping only `$*` control keywords; report whether any
|
|
112
|
+
* non-control parts were present. Used by `/one` routes where the
|
|
113
|
+
* uniquery lexer cannot tokenise PK values containing `-` and other
|
|
114
|
+
* reserved chars, so non-control parts must be stripped before lexing.
|
|
115
|
+
* `/one/:id` rejects stray filter params with 400 via `hasNonControl`;
|
|
116
|
+
* `/one` (composite) ignores it because the composite-key params have
|
|
117
|
+
* already been extracted via `@Query()`.
|
|
118
|
+
*/
|
|
119
|
+
protected parseControlsOnlyFromUrl(url: string): {
|
|
120
|
+
parsed: ReturnType<typeof parseUrl>;
|
|
121
|
+
hasNonControl: boolean;
|
|
122
|
+
};
|
|
109
123
|
protected returnOne(result: Promise<DataType | null>): Promise<DataType | HttpError>;
|
|
110
124
|
/**
|
|
111
125
|
* **GET /meta** — returns the bound interface's metadata envelope. The
|
|
@@ -166,7 +180,10 @@ declare class AsDbReadableController<T extends TAtscriptAnnotatedType = TAtscrip
|
|
|
166
180
|
private readonly _gates;
|
|
167
181
|
private readonly _preferredIdSet;
|
|
168
182
|
private readonly _overlayIsNoOp;
|
|
183
|
+
/** path → sibling-ref path for `@db.amount.currency.ref` / `@db.unit.ref`. */
|
|
184
|
+
private readonly _quantityRefByPath;
|
|
169
185
|
constructor(readable: AtscriptDbReadable<T>, app: Moost);
|
|
186
|
+
private _collectQuantityRefs;
|
|
170
187
|
private _buildGates;
|
|
171
188
|
private _collectAnnotated;
|
|
172
189
|
protected hasField(path: string): boolean;
|
|
@@ -191,6 +208,14 @@ declare class AsDbReadableController<T extends TAtscriptAnnotatedType = TAtscrip
|
|
|
191
208
|
private widenPreferredIdProjection;
|
|
192
209
|
private _widenArrayProjection;
|
|
193
210
|
private _widenMapProjection;
|
|
211
|
+
/**
|
|
212
|
+
* Auto-includes the sibling-ref field whenever its `@db.amount.currency.ref`
|
|
213
|
+
* / `@db.unit.ref` quantity is selected — UI must never get a value without
|
|
214
|
+
* its dimension. No-op when `$select` is undefined (full row covers it).
|
|
215
|
+
*/
|
|
216
|
+
private widenQuantityRefProjection;
|
|
217
|
+
private _widenQuantityArrayProjection;
|
|
218
|
+
private _widenQuantityMapProjection;
|
|
194
219
|
/** WHY: the URL parser only auto-coerces `$count`; every other boolean control reaches us as `"true"`/`"1"` and would fail DTO validation. */
|
|
195
220
|
private _coerceActionsControl;
|
|
196
221
|
/** Normalize a post-`widenPreferredIdProjection` $select into `string[] | null` (`null` = all fields). */
|
package/dist/index.mjs
CHANGED
|
@@ -690,6 +690,42 @@ let AsReadableController = class AsReadableController {
|
|
|
690
690
|
const idx = url.indexOf("?");
|
|
691
691
|
return parseUrl(idx >= 0 ? url.slice(idx + 1) : "");
|
|
692
692
|
}
|
|
693
|
+
/**
|
|
694
|
+
* Parse a URL keeping only `$*` control keywords; report whether any
|
|
695
|
+
* non-control parts were present. Used by `/one` routes where the
|
|
696
|
+
* uniquery lexer cannot tokenise PK values containing `-` and other
|
|
697
|
+
* reserved chars, so non-control parts must be stripped before lexing.
|
|
698
|
+
* `/one/:id` rejects stray filter params with 400 via `hasNonControl`;
|
|
699
|
+
* `/one` (composite) ignores it because the composite-key params have
|
|
700
|
+
* already been extracted via `@Query()`.
|
|
701
|
+
*/
|
|
702
|
+
parseControlsOnlyFromUrl(url) {
|
|
703
|
+
const idx = url.indexOf("?");
|
|
704
|
+
const qs = idx >= 0 ? url.slice(idx + 1) : "";
|
|
705
|
+
if (!qs) return {
|
|
706
|
+
parsed: parseUrl(""),
|
|
707
|
+
hasNonControl: false
|
|
708
|
+
};
|
|
709
|
+
const kept = [];
|
|
710
|
+
let hasNonControl = false;
|
|
711
|
+
for (const part of qs.split("&")) {
|
|
712
|
+
if (!part) continue;
|
|
713
|
+
const eq = part.indexOf("=");
|
|
714
|
+
const rawKey = eq === -1 ? part : part.slice(0, eq);
|
|
715
|
+
let key;
|
|
716
|
+
try {
|
|
717
|
+
key = decodeURIComponent(rawKey);
|
|
718
|
+
} catch {
|
|
719
|
+
key = rawKey;
|
|
720
|
+
}
|
|
721
|
+
if (key.startsWith("$")) kept.push(part);
|
|
722
|
+
else hasNonControl = true;
|
|
723
|
+
}
|
|
724
|
+
return {
|
|
725
|
+
parsed: parseUrl(kept.join("&")),
|
|
726
|
+
hasNonControl
|
|
727
|
+
};
|
|
728
|
+
}
|
|
693
729
|
async returnOne(result) {
|
|
694
730
|
const item = await result;
|
|
695
731
|
if (!item) return new HttpError(404);
|
|
@@ -954,14 +990,27 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
|
|
|
954
990
|
_gates;
|
|
955
991
|
_preferredIdSet;
|
|
956
992
|
_overlayIsNoOp;
|
|
993
|
+
/** path → sibling-ref path for `@db.amount.currency.ref` / `@db.unit.ref`. */
|
|
994
|
+
_quantityRefByPath;
|
|
957
995
|
constructor(readable, app) {
|
|
958
996
|
super(readable.type, readable.tableName, app, readable.isView ? "view" : "table");
|
|
959
997
|
this.readable = readable;
|
|
960
998
|
this._gates = this._buildGates();
|
|
961
999
|
this._preferredIdSet = new Set(readable.preferredId ?? []);
|
|
1000
|
+
this._quantityRefByPath = this._collectQuantityRefs();
|
|
962
1001
|
const defaultOverlay = AsReadableController.prototype.applyMetaOverlay;
|
|
963
1002
|
this._overlayIsNoOp = this.applyMetaOverlay === defaultOverlay;
|
|
964
1003
|
}
|
|
1004
|
+
_collectQuantityRefs() {
|
|
1005
|
+
const out = /* @__PURE__ */ new Map();
|
|
1006
|
+
if (!this.readable.flatMap) return out;
|
|
1007
|
+
for (const [path, entry] of this.readable.flatMap) {
|
|
1008
|
+
const meta = entry?.metadata;
|
|
1009
|
+
const ref = meta?.get("db.amount.currency.ref") ?? meta?.get("db.unit.ref");
|
|
1010
|
+
if (ref) out.set(path, ref);
|
|
1011
|
+
}
|
|
1012
|
+
return out;
|
|
1013
|
+
}
|
|
965
1014
|
_buildGates() {
|
|
966
1015
|
const meta = this.readable.type.metadata;
|
|
967
1016
|
const gates = {};
|
|
@@ -1029,9 +1078,11 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
|
|
|
1029
1078
|
return projection;
|
|
1030
1079
|
}
|
|
1031
1080
|
widenPreferredIdProjection(projection) {
|
|
1032
|
-
|
|
1033
|
-
if (
|
|
1034
|
-
|
|
1081
|
+
const widened = this.widenQuantityRefProjection(projection);
|
|
1082
|
+
if (widened instanceof HttpError) return widened;
|
|
1083
|
+
if (this._preferredIdSet.size === 0 || widened === void 0) return widened;
|
|
1084
|
+
if (Array.isArray(widened)) return this._widenArrayProjection(widened);
|
|
1085
|
+
return this._widenMapProjection(widened);
|
|
1035
1086
|
}
|
|
1036
1087
|
_widenArrayProjection(projection) {
|
|
1037
1088
|
const stringItems = /* @__PURE__ */ new Set();
|
|
@@ -1071,6 +1122,46 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
|
|
|
1071
1122
|
for (const field of this._preferredIdSet) widened[field] = 1;
|
|
1072
1123
|
return widened;
|
|
1073
1124
|
}
|
|
1125
|
+
/**
|
|
1126
|
+
* Auto-includes the sibling-ref field whenever its `@db.amount.currency.ref`
|
|
1127
|
+
* / `@db.unit.ref` quantity is selected — UI must never get a value without
|
|
1128
|
+
* its dimension. No-op when `$select` is undefined (full row covers it).
|
|
1129
|
+
*/
|
|
1130
|
+
widenQuantityRefProjection(projection) {
|
|
1131
|
+
if (this._quantityRefByPath.size === 0 || projection === void 0) return projection;
|
|
1132
|
+
if (Array.isArray(projection)) return this._widenQuantityArrayProjection(projection);
|
|
1133
|
+
return this._widenQuantityMapProjection(projection);
|
|
1134
|
+
}
|
|
1135
|
+
_widenQuantityArrayProjection(projection) {
|
|
1136
|
+
const stringItems = /* @__PURE__ */ new Set();
|
|
1137
|
+
for (const item of projection) if (typeof item === "string") stringItems.add(item);
|
|
1138
|
+
const toAdd = [];
|
|
1139
|
+
for (const [valuePath, refPath] of this._quantityRefByPath) if (stringItems.has(valuePath) && !stringItems.has(refPath)) {
|
|
1140
|
+
toAdd.push(refPath);
|
|
1141
|
+
stringItems.add(refPath);
|
|
1142
|
+
}
|
|
1143
|
+
if (toAdd.length === 0) return projection;
|
|
1144
|
+
return [...projection, ...toAdd];
|
|
1145
|
+
}
|
|
1146
|
+
_widenQuantityMapProjection(projection) {
|
|
1147
|
+
const entries = Object.entries(projection);
|
|
1148
|
+
if (entries.length === 0) return projection;
|
|
1149
|
+
const included = /* @__PURE__ */ new Set();
|
|
1150
|
+
const excluded = /* @__PURE__ */ new Set();
|
|
1151
|
+
for (const [k, v] of entries) if (v === 1 || v === true) included.add(k);
|
|
1152
|
+
else if (v === 0 || v === false) excluded.add(k);
|
|
1153
|
+
if (included.size > 0 && excluded.size > 0) return new HttpError(400, "Mixed inclusion/exclusion $select maps are not supported");
|
|
1154
|
+
if (excluded.size === 0) {
|
|
1155
|
+
const toAdd = [];
|
|
1156
|
+
for (const [valuePath, refPath] of this._quantityRefByPath) if (included.has(valuePath) && !included.has(refPath)) toAdd.push(refPath);
|
|
1157
|
+
if (toAdd.length === 0) return projection;
|
|
1158
|
+
const widened = {};
|
|
1159
|
+
for (const k of included) widened[k] = 1;
|
|
1160
|
+
for (const k of toAdd) widened[k] = 1;
|
|
1161
|
+
return widened;
|
|
1162
|
+
}
|
|
1163
|
+
return projection;
|
|
1164
|
+
}
|
|
1074
1165
|
/** WHY: the URL parser only auto-coerces `$count`; every other boolean control reaches us as `"true"`/`"1"` and would fail DTO validation. */
|
|
1075
1166
|
_coerceActionsControl(controls) {
|
|
1076
1167
|
const v = controls.$actions;
|
|
@@ -1297,9 +1388,9 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
|
|
|
1297
1388
|
* **GET /one/:id** — retrieves a single record by ID or unique property.
|
|
1298
1389
|
*/
|
|
1299
1390
|
async getOne(id, url) {
|
|
1300
|
-
const parsed = this.
|
|
1391
|
+
const { parsed, hasNonControl } = this.parseControlsOnlyFromUrl(url);
|
|
1392
|
+
if (hasNonControl) return new HttpError(400, "Filtering is not allowed for \"one\" endpoint");
|
|
1301
1393
|
this._coerceActionsControl(parsed.controls);
|
|
1302
|
-
if (Object.keys(parsed.filter).length > 0) return new HttpError(400, "Filtering is not allowed for \"one\" endpoint");
|
|
1303
1394
|
const error = this.validateParsed(parsed, "getOne");
|
|
1304
1395
|
if (error) return error;
|
|
1305
1396
|
const rawSelect = await this.transformProjection(parsed.controls.$select);
|
|
@@ -1314,7 +1405,7 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
|
|
|
1314
1405
|
async getOneComposite(query, url) {
|
|
1315
1406
|
const idObj = this.extractIdShape(query);
|
|
1316
1407
|
if (idObj instanceof HttpError) return idObj;
|
|
1317
|
-
const parsed = this.
|
|
1408
|
+
const { parsed } = this.parseControlsOnlyFromUrl(url);
|
|
1318
1409
|
this._coerceActionsControl(parsed.controls);
|
|
1319
1410
|
const rawSelect = await this.transformProjection(parsed.controls.$select);
|
|
1320
1411
|
const select = this.widenPreferredIdProjection(rawSelect);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atscript/moost-db",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.64",
|
|
4
4
|
"description": "Generic database controller for Moost with Atscript.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"annotations",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"@wooksjs/event-core": "^0.7.10",
|
|
59
59
|
"@wooksjs/http-body": "^0.7.10",
|
|
60
60
|
"moost": "^0.6.8",
|
|
61
|
-
"@atscript/db": "^0.1.
|
|
61
|
+
"@atscript/db": "^0.1.64"
|
|
62
62
|
},
|
|
63
63
|
"scripts": {
|
|
64
64
|
"postinstall": "asc -f dts",
|