@atscript/moost-db 0.1.67 → 0.1.69

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 CHANGED
@@ -23,7 +23,7 @@ function transformValidationError(error, reply) {
23
23
  }));
24
24
  }
25
25
  }
26
- const validationErrorTransform = () => (0, moost.defineInterceptor)({ error: transformValidationError }, moost.TInterceptorPriority.CATCH_ERROR);
26
+ const validationErrorTransform = () => (0, moost.defineInterceptor)({ error: transformValidationError }, moost.TInterceptorPriority.BEFORE_ALL);
27
27
  const UseValidationErrorTransform = () => (0, moost.Intercept)(validationErrorTransform());
28
28
  //#endregion
29
29
  //#region src/dto/controls.dto.as
@@ -991,15 +991,24 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
991
991
  _overlayIsNoOp;
992
992
  /** path → sibling-ref path for `@db.amount.currency.ref` / `@db.unit.ref`. */
993
993
  _quantityRefByPath;
994
+ /** Paths the adapter vetoes for filtering (e.g. JSON storage on SQL). Symmetric with `/meta` `filterable: false`. */
995
+ _adapterNonFilterable;
994
996
  constructor(readable, app) {
995
997
  super(readable.type, readable.tableName, app, readable.isView ? "view" : "table");
996
998
  this.readable = readable;
999
+ this._adapterNonFilterable = this._collectAdapterNonFilterable();
997
1000
  this._gates = this._buildGates();
998
1001
  this._preferredIdSet = new Set(readable.preferredId ?? []);
999
1002
  this._quantityRefByPath = this._collectQuantityRefs();
1000
1003
  const defaultOverlay = AsReadableController.prototype.applyMetaOverlay;
1001
1004
  this._overlayIsNoOp = this.applyMetaOverlay === defaultOverlay;
1002
1005
  }
1006
+ _collectAdapterNonFilterable() {
1007
+ const out = /* @__PURE__ */ new Set();
1008
+ if (!this.readable.fieldDescriptors || typeof this.readable.canFilterField !== "function") return out;
1009
+ for (const fd of this.readable.fieldDescriptors) if (!fd.ignored && !this.readable.canFilterField(fd)) out.add(fd.path);
1010
+ return out;
1011
+ }
1003
1012
  _collectQuantityRefs() {
1004
1013
  const out = /* @__PURE__ */ new Map();
1005
1014
  if (!this.readable.flatMap) return out;
@@ -1037,6 +1046,20 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
1037
1046
  hasField(path) {
1038
1047
  return this.readable.flatMap.has(path);
1039
1048
  }
1049
+ /**
1050
+ * Adds an adapter-capability veto on top of the base gate. Distinct from the
1051
+ * `@db.column.filterable` rejection because the message must reference the
1052
+ * adapter, not an annotation the user could add to bypass it. Sort uses
1053
+ * adapter capability differently and is enforced at the SQL builder layer.
1054
+ */
1055
+ checkGates(filter, controls, gates) {
1056
+ const baseError = super.checkGates(filter, controls, gates);
1057
+ if (baseError) return baseError;
1058
+ if (this._adapterNonFilterable.size === 0) return void 0;
1059
+ const offender = findFilterOffender(filter, (f) => !this._adapterNonFilterable.has(f));
1060
+ if (!offender) return void 0;
1061
+ return new _moostjs_event_http.HttpError(400, `Filtering on field "${offender}" is not permitted — adapter cannot filter on this storage type.`);
1062
+ }
1040
1063
  /** Validates $with relations against the readable. */
1041
1064
  validateParsed(parsed, type) {
1042
1065
  const baseError = super.validateParsed(parsed, type);
@@ -1070,6 +1093,15 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
1070
1093
  return filter;
1071
1094
  }
1072
1095
  /**
1096
+ * Transform filter for the `/one/:id` and `/one?...` endpoints. Defaults to
1097
+ * {@link transformFilter} so any row-level read overlay applied to `/query` /
1098
+ * `/pages` also gates id-based reads (existence is not leaked through
1099
+ * `findById`). Override to scope `/one` differently.
1100
+ */
1101
+ transformOne(filter) {
1102
+ return this.transformFilter(filter);
1103
+ }
1104
+ /**
1073
1105
  * Transform projection before querying.
1074
1106
  * May return a Promise for async lookups.
1075
1107
  */
@@ -1385,6 +1417,8 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
1385
1417
  }
1386
1418
  /**
1387
1419
  * **GET /one/:id** — retrieves a single record by ID or unique property.
1420
+ * The id-filter is AND-combined with {@link transformOne} so row-level
1421
+ * read overlays gate `/one` symmetrically with `/query` / `/pages`.
1388
1422
  */
1389
1423
  async getOne(id, url) {
1390
1424
  const { parsed, hasNonControl } = this.parseControlsOnlyFromUrl(url);
@@ -1399,7 +1433,8 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
1399
1433
  }
1400
1434
  /**
1401
1435
  * **GET /one?field1=val1&field2=val2** — retrieves a single record by composite key
1402
- * (composite primary key or compound unique index).
1436
+ * (composite primary key or compound unique index). Same `transformOne`
1437
+ * gating as {@link getOne}.
1403
1438
  */
1404
1439
  async getOneComposite(query, url) {
1405
1440
  const idObj = this.extractIdShape(query);
@@ -1418,7 +1453,16 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
1418
1453
  ...parsedControls,
1419
1454
  $select: initialSelect
1420
1455
  };
1421
- const row = await this.readable.findById(id, { controls });
1456
+ const idFilter = this.readable.resolveIdFilter(id);
1457
+ let row = null;
1458
+ if (idFilter) {
1459
+ const overlay = await this.transformOne({});
1460
+ const filter = overlay && Object.keys(overlay).length > 0 ? { $and: [idFilter, overlay] } : idFilter;
1461
+ row = await this.readable.findOne({
1462
+ filter,
1463
+ controls
1464
+ });
1465
+ }
1422
1466
  const item = await this.returnOne(Promise.resolve(row));
1423
1467
  if (item instanceof _moostjs_event_http.HttpError) return item;
1424
1468
  if (!prep) return item;
package/dist/index.d.cts CHANGED
@@ -182,11 +182,21 @@ declare class AsDbReadableController<T extends TAtscriptAnnotatedType = TAtscrip
182
182
  private readonly _overlayIsNoOp;
183
183
  /** path → sibling-ref path for `@db.amount.currency.ref` / `@db.unit.ref`. */
184
184
  private readonly _quantityRefByPath;
185
+ /** Paths the adapter vetoes for filtering (e.g. JSON storage on SQL). Symmetric with `/meta` `filterable: false`. */
186
+ private readonly _adapterNonFilterable;
185
187
  constructor(readable: AtscriptDbReadable<T>, app: Moost);
188
+ private _collectAdapterNonFilterable;
186
189
  private _collectQuantityRefs;
187
190
  private _buildGates;
188
191
  private _collectAnnotated;
189
192
  protected hasField(path: string): boolean;
193
+ /**
194
+ * Adds an adapter-capability veto on top of the base gate. Distinct from the
195
+ * `@db.column.filterable` rejection because the message must reference the
196
+ * adapter, not an annotation the user could add to bypass it. Sort uses
197
+ * adapter capability differently and is enforced at the SQL builder layer.
198
+ */
199
+ protected checkGates(filter: FilterExpr | undefined, controls: Record<string, unknown>, gates: ReadableGates): HttpError | undefined;
190
200
  /** Validates $with relations against the readable. */
191
201
  protected validateParsed(parsed: Uniquery, type: "query" | "pages" | "getOne"): HttpError | undefined;
192
202
  /**
@@ -200,6 +210,13 @@ declare class AsDbReadableController<T extends TAtscriptAnnotatedType = TAtscrip
200
210
  * May return a Promise for async lookups (session, permissions).
201
211
  */
202
212
  protected transformFilter(filter: FilterExpr): FilterExpr | Promise<FilterExpr>;
213
+ /**
214
+ * Transform filter for the `/one/:id` and `/one?...` endpoints. Defaults to
215
+ * {@link transformFilter} so any row-level read overlay applied to `/query` /
216
+ * `/pages` also gates id-based reads (existence is not leaked through
217
+ * `findById`). Override to scope `/one` differently.
218
+ */
219
+ protected transformOne(filter: FilterExpr): FilterExpr | Promise<FilterExpr>;
203
220
  /**
204
221
  * Transform projection before querying.
205
222
  * May return a Promise for async lookups.
@@ -252,11 +269,14 @@ declare class AsDbReadableController<T extends TAtscriptAnnotatedType = TAtscrip
252
269
  } | HttpError>;
253
270
  /**
254
271
  * **GET /one/:id** — retrieves a single record by ID or unique property.
272
+ * The id-filter is AND-combined with {@link transformOne} so row-level
273
+ * read overlays gate `/one` symmetrically with `/query` / `/pages`.
255
274
  */
256
275
  getOne(id: string, url: string): Promise<DataType | HttpError>;
257
276
  /**
258
277
  * **GET /one?field1=val1&field2=val2** — retrieves a single record by composite key
259
- * (composite primary key or compound unique index).
278
+ * (composite primary key or compound unique index). Same `transformOne`
279
+ * gating as {@link getOne}.
260
280
  */
261
281
  getOneComposite(query: Record<string, string>, url: string): Promise<DataType | HttpError>;
262
282
  private _findByIdAndAugment;
package/dist/index.d.mts CHANGED
@@ -182,11 +182,21 @@ declare class AsDbReadableController<T extends TAtscriptAnnotatedType = TAtscrip
182
182
  private readonly _overlayIsNoOp;
183
183
  /** path → sibling-ref path for `@db.amount.currency.ref` / `@db.unit.ref`. */
184
184
  private readonly _quantityRefByPath;
185
+ /** Paths the adapter vetoes for filtering (e.g. JSON storage on SQL). Symmetric with `/meta` `filterable: false`. */
186
+ private readonly _adapterNonFilterable;
185
187
  constructor(readable: AtscriptDbReadable<T>, app: Moost);
188
+ private _collectAdapterNonFilterable;
186
189
  private _collectQuantityRefs;
187
190
  private _buildGates;
188
191
  private _collectAnnotated;
189
192
  protected hasField(path: string): boolean;
193
+ /**
194
+ * Adds an adapter-capability veto on top of the base gate. Distinct from the
195
+ * `@db.column.filterable` rejection because the message must reference the
196
+ * adapter, not an annotation the user could add to bypass it. Sort uses
197
+ * adapter capability differently and is enforced at the SQL builder layer.
198
+ */
199
+ protected checkGates(filter: FilterExpr | undefined, controls: Record<string, unknown>, gates: ReadableGates): HttpError | undefined;
190
200
  /** Validates $with relations against the readable. */
191
201
  protected validateParsed(parsed: Uniquery, type: "query" | "pages" | "getOne"): HttpError | undefined;
192
202
  /**
@@ -200,6 +210,13 @@ declare class AsDbReadableController<T extends TAtscriptAnnotatedType = TAtscrip
200
210
  * May return a Promise for async lookups (session, permissions).
201
211
  */
202
212
  protected transformFilter(filter: FilterExpr): FilterExpr | Promise<FilterExpr>;
213
+ /**
214
+ * Transform filter for the `/one/:id` and `/one?...` endpoints. Defaults to
215
+ * {@link transformFilter} so any row-level read overlay applied to `/query` /
216
+ * `/pages` also gates id-based reads (existence is not leaked through
217
+ * `findById`). Override to scope `/one` differently.
218
+ */
219
+ protected transformOne(filter: FilterExpr): FilterExpr | Promise<FilterExpr>;
203
220
  /**
204
221
  * Transform projection before querying.
205
222
  * May return a Promise for async lookups.
@@ -252,11 +269,14 @@ declare class AsDbReadableController<T extends TAtscriptAnnotatedType = TAtscrip
252
269
  } | HttpError>;
253
270
  /**
254
271
  * **GET /one/:id** — retrieves a single record by ID or unique property.
272
+ * The id-filter is AND-combined with {@link transformOne} so row-level
273
+ * read overlays gate `/one` symmetrically with `/query` / `/pages`.
255
274
  */
256
275
  getOne(id: string, url: string): Promise<DataType | HttpError>;
257
276
  /**
258
277
  * **GET /one?field1=val1&field2=val2** — retrieves a single record by composite key
259
- * (composite primary key or compound unique index).
278
+ * (composite primary key or compound unique index). Same `transformOne`
279
+ * gating as {@link getOne}.
260
280
  */
261
281
  getOneComposite(query: Record<string, string>, url: string): Promise<DataType | HttpError>;
262
282
  private _findByIdAndAugment;
package/dist/index.mjs CHANGED
@@ -22,7 +22,7 @@ function transformValidationError(error, reply) {
22
22
  }));
23
23
  }
24
24
  }
25
- const validationErrorTransform = () => defineInterceptor({ error: transformValidationError }, TInterceptorPriority.CATCH_ERROR);
25
+ const validationErrorTransform = () => defineInterceptor({ error: transformValidationError }, TInterceptorPriority.BEFORE_ALL);
26
26
  const UseValidationErrorTransform = () => Intercept(validationErrorTransform());
27
27
  //#endregion
28
28
  //#region src/dto/controls.dto.as
@@ -990,15 +990,24 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
990
990
  _overlayIsNoOp;
991
991
  /** path → sibling-ref path for `@db.amount.currency.ref` / `@db.unit.ref`. */
992
992
  _quantityRefByPath;
993
+ /** Paths the adapter vetoes for filtering (e.g. JSON storage on SQL). Symmetric with `/meta` `filterable: false`. */
994
+ _adapterNonFilterable;
993
995
  constructor(readable, app) {
994
996
  super(readable.type, readable.tableName, app, readable.isView ? "view" : "table");
995
997
  this.readable = readable;
998
+ this._adapterNonFilterable = this._collectAdapterNonFilterable();
996
999
  this._gates = this._buildGates();
997
1000
  this._preferredIdSet = new Set(readable.preferredId ?? []);
998
1001
  this._quantityRefByPath = this._collectQuantityRefs();
999
1002
  const defaultOverlay = AsReadableController.prototype.applyMetaOverlay;
1000
1003
  this._overlayIsNoOp = this.applyMetaOverlay === defaultOverlay;
1001
1004
  }
1005
+ _collectAdapterNonFilterable() {
1006
+ const out = /* @__PURE__ */ new Set();
1007
+ if (!this.readable.fieldDescriptors || typeof this.readable.canFilterField !== "function") return out;
1008
+ for (const fd of this.readable.fieldDescriptors) if (!fd.ignored && !this.readable.canFilterField(fd)) out.add(fd.path);
1009
+ return out;
1010
+ }
1002
1011
  _collectQuantityRefs() {
1003
1012
  const out = /* @__PURE__ */ new Map();
1004
1013
  if (!this.readable.flatMap) return out;
@@ -1036,6 +1045,20 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
1036
1045
  hasField(path) {
1037
1046
  return this.readable.flatMap.has(path);
1038
1047
  }
1048
+ /**
1049
+ * Adds an adapter-capability veto on top of the base gate. Distinct from the
1050
+ * `@db.column.filterable` rejection because the message must reference the
1051
+ * adapter, not an annotation the user could add to bypass it. Sort uses
1052
+ * adapter capability differently and is enforced at the SQL builder layer.
1053
+ */
1054
+ checkGates(filter, controls, gates) {
1055
+ const baseError = super.checkGates(filter, controls, gates);
1056
+ if (baseError) return baseError;
1057
+ if (this._adapterNonFilterable.size === 0) return void 0;
1058
+ const offender = findFilterOffender(filter, (f) => !this._adapterNonFilterable.has(f));
1059
+ if (!offender) return void 0;
1060
+ return new HttpError(400, `Filtering on field "${offender}" is not permitted — adapter cannot filter on this storage type.`);
1061
+ }
1039
1062
  /** Validates $with relations against the readable. */
1040
1063
  validateParsed(parsed, type) {
1041
1064
  const baseError = super.validateParsed(parsed, type);
@@ -1069,6 +1092,15 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
1069
1092
  return filter;
1070
1093
  }
1071
1094
  /**
1095
+ * Transform filter for the `/one/:id` and `/one?...` endpoints. Defaults to
1096
+ * {@link transformFilter} so any row-level read overlay applied to `/query` /
1097
+ * `/pages` also gates id-based reads (existence is not leaked through
1098
+ * `findById`). Override to scope `/one` differently.
1099
+ */
1100
+ transformOne(filter) {
1101
+ return this.transformFilter(filter);
1102
+ }
1103
+ /**
1072
1104
  * Transform projection before querying.
1073
1105
  * May return a Promise for async lookups.
1074
1106
  */
@@ -1384,6 +1416,8 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
1384
1416
  }
1385
1417
  /**
1386
1418
  * **GET /one/:id** — retrieves a single record by ID or unique property.
1419
+ * The id-filter is AND-combined with {@link transformOne} so row-level
1420
+ * read overlays gate `/one` symmetrically with `/query` / `/pages`.
1387
1421
  */
1388
1422
  async getOne(id, url) {
1389
1423
  const { parsed, hasNonControl } = this.parseControlsOnlyFromUrl(url);
@@ -1398,7 +1432,8 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
1398
1432
  }
1399
1433
  /**
1400
1434
  * **GET /one?field1=val1&field2=val2** — retrieves a single record by composite key
1401
- * (composite primary key or compound unique index).
1435
+ * (composite primary key or compound unique index). Same `transformOne`
1436
+ * gating as {@link getOne}.
1402
1437
  */
1403
1438
  async getOneComposite(query, url) {
1404
1439
  const idObj = this.extractIdShape(query);
@@ -1417,7 +1452,16 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
1417
1452
  ...parsedControls,
1418
1453
  $select: initialSelect
1419
1454
  };
1420
- const row = await this.readable.findById(id, { controls });
1455
+ const idFilter = this.readable.resolveIdFilter(id);
1456
+ let row = null;
1457
+ if (idFilter) {
1458
+ const overlay = await this.transformOne({});
1459
+ const filter = overlay && Object.keys(overlay).length > 0 ? { $and: [idFilter, overlay] } : idFilter;
1460
+ row = await this.readable.findOne({
1461
+ filter,
1462
+ controls
1463
+ });
1464
+ }
1421
1465
  const item = await this.returnOne(Promise.resolve(row));
1422
1466
  if (item instanceof HttpError) return item;
1423
1467
  if (!prep) return item;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atscript/moost-db",
3
- "version": "0.1.67",
3
+ "version": "0.1.69",
4
4
  "description": "Generic database controller for Moost with Atscript.",
5
5
  "keywords": [
6
6
  "annotations",
@@ -41,24 +41,24 @@
41
41
  "@uniqu/url": "^0.1.6"
42
42
  },
43
43
  "devDependencies": {
44
- "@atscript/core": "^0.1.50",
45
- "@atscript/typescript": "^0.1.50",
46
- "@moostjs/event-http": "^0.6.8",
44
+ "@atscript/core": "^0.1.51",
45
+ "@atscript/typescript": "^0.1.51",
46
+ "@moostjs/event-http": "^0.6.9",
47
47
  "@uniqu/core": "^0.1.6",
48
- "@wooksjs/event-core": "^0.7.10",
49
- "@wooksjs/event-http": "^0.7.10",
50
- "@wooksjs/http-body": "^0.7.10",
51
- "moost": "^0.6.8",
52
- "unplugin-atscript": "^0.1.50"
48
+ "@wooksjs/event-core": "^0.7.11",
49
+ "@wooksjs/event-http": "^0.7.11",
50
+ "@wooksjs/http-body": "^0.7.11",
51
+ "moost": "^0.6.9",
52
+ "unplugin-atscript": "^0.1.51"
53
53
  },
54
54
  "peerDependencies": {
55
- "@atscript/typescript": "^0.1.50",
56
- "@moostjs/event-http": "^0.6.8",
55
+ "@atscript/typescript": "^0.1.51",
56
+ "@moostjs/event-http": "^0.6.9",
57
57
  "@uniqu/core": "^0.1.6",
58
- "@wooksjs/event-core": "^0.7.10",
59
- "@wooksjs/http-body": "^0.7.10",
60
- "moost": "^0.6.8",
61
- "@atscript/db": "^0.1.67"
58
+ "@wooksjs/event-core": "^0.7.11",
59
+ "@wooksjs/http-body": "^0.7.11",
60
+ "moost": "^0.6.9",
61
+ "@atscript/db": "^0.1.69"
62
62
  },
63
63
  "scripts": {
64
64
  "postinstall": "asc -f dts",