@antelopejs/interface-data-api 0.3.0 → 0.5.0

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.
Files changed (124) hide show
  1. package/README.md +1 -1
  2. package/dist/components.d.ts +32 -9
  3. package/dist/components.js +113 -628
  4. package/dist/components.js.map +1 -1
  5. package/dist/index.d.ts +41 -36
  6. package/dist/index.js +94 -92
  7. package/dist/index.js.map +1 -1
  8. package/dist/internal/computed.d.ts +6 -0
  9. package/dist/internal/computed.js +43 -0
  10. package/dist/internal/computed.js.map +1 -0
  11. package/dist/internal/controller-types.d.ts +19 -0
  12. package/dist/internal/controller-types.js +4 -0
  13. package/dist/internal/controller-types.js.map +1 -0
  14. package/dist/internal/default-filter.d.ts +4 -0
  15. package/dist/internal/default-filter.js +19 -0
  16. package/dist/internal/default-filter.js.map +1 -0
  17. package/dist/internal/deferred-joined-stream.d.ts +11 -0
  18. package/dist/internal/deferred-joined-stream.js +112 -0
  19. package/dist/internal/deferred-joined-stream.js.map +1 -0
  20. package/dist/internal/field-types.d.ts +183 -0
  21. package/dist/internal/field-types.js +4 -0
  22. package/dist/internal/field-types.js.map +1 -0
  23. package/dist/internal/filters.d.ts +4 -0
  24. package/dist/internal/filters.js +15 -0
  25. package/dist/internal/filters.js.map +1 -0
  26. package/dist/internal/foreign.d.ts +6 -0
  27. package/dist/internal/foreign.js +116 -0
  28. package/dist/internal/foreign.js.map +1 -0
  29. package/dist/internal/guards.d.ts +20 -0
  30. package/dist/internal/guards.js +74 -0
  31. package/dist/internal/guards.js.map +1 -0
  32. package/dist/internal/joined.d.ts +28 -0
  33. package/dist/internal/joined.js +127 -0
  34. package/dist/internal/joined.js.map +1 -0
  35. package/dist/internal/list.d.ts +16 -0
  36. package/dist/internal/list.js +178 -0
  37. package/dist/internal/list.js.map +1 -0
  38. package/dist/internal/meta-records.d.ts +17 -0
  39. package/dist/internal/meta-records.js +73 -0
  40. package/dist/internal/meta-records.js.map +1 -0
  41. package/dist/internal/properties.d.ts +4 -0
  42. package/dist/internal/properties.js +43 -0
  43. package/dist/internal/properties.js.map +1 -0
  44. package/dist/internal/table-reference.d.ts +11 -0
  45. package/dist/internal/table-reference.js +46 -0
  46. package/dist/internal/table-reference.js.map +1 -0
  47. package/dist/internal/tables.d.ts +18 -0
  48. package/dist/internal/tables.js +37 -0
  49. package/dist/internal/tables.js.map +1 -0
  50. package/dist/internal/validation.d.ts +11 -0
  51. package/dist/internal/validation.js +88 -0
  52. package/dist/internal/validation.js.map +1 -0
  53. package/dist/metadata.d.ts +27 -228
  54. package/dist/metadata.js +61 -188
  55. package/dist/metadata.js.map +1 -1
  56. package/dist/tests/components/access_control.test.js +20 -20
  57. package/dist/tests/components/access_control.test.js.map +1 -1
  58. package/dist/tests/components/computed.test.js +28 -30
  59. package/dist/tests/components/computed.test.js.map +1 -1
  60. package/dist/tests/components/cross_instance.test.js +8 -6
  61. package/dist/tests/components/cross_instance.test.js.map +1 -1
  62. package/dist/tests/components/deferred_joined.test.js +56 -53
  63. package/dist/tests/components/deferred_joined.test.js.map +1 -1
  64. package/dist/tests/components/filter_options.test.js +8 -7
  65. package/dist/tests/components/filter_options.test.js.map +1 -1
  66. package/dist/tests/components/foreign_computed.test.js +14 -16
  67. package/dist/tests/components/foreign_computed.test.js.map +1 -1
  68. package/dist/tests/components/foreign_joined.test.js +19 -27
  69. package/dist/tests/components/foreign_joined.test.js.map +1 -1
  70. package/dist/tests/components/joined.test.js +41 -45
  71. package/dist/tests/components/joined.test.js.map +1 -1
  72. package/dist/tests/components/listable.test.js +59 -58
  73. package/dist/tests/components/listable.test.js.map +1 -1
  74. package/dist/tests/components/localized_sort.test.d.ts +1 -0
  75. package/dist/tests/components/localized_sort.test.js +131 -0
  76. package/dist/tests/components/localized_sort.test.js.map +1 -0
  77. package/dist/tests/components/mandatory.test.js +33 -28
  78. package/dist/tests/components/mandatory.test.js.map +1 -1
  79. package/dist/tests/components/modifier_key.test.js +19 -18
  80. package/dist/tests/components/modifier_key.test.js.map +1 -1
  81. package/dist/tests/components/parse_body.test.d.ts +1 -0
  82. package/dist/tests/components/parse_body.test.js +75 -0
  83. package/dist/tests/components/parse_body.test.js.map +1 -0
  84. package/dist/tests/components/parsed_value.test.js +27 -21
  85. package/dist/tests/components/parsed_value.test.js.map +1 -1
  86. package/dist/tests/components/partial_edit.test.js +33 -32
  87. package/dist/tests/components/partial_edit.test.js.map +1 -1
  88. package/dist/tests/components/per_action_access.test.js +12 -12
  89. package/dist/tests/components/per_action_access.test.js.map +1 -1
  90. package/dist/tests/components/validator.test.js +45 -37
  91. package/dist/tests/components/validator.test.js.map +1 -1
  92. package/dist/tests/helpers/other-instance.d.ts +11 -1
  93. package/dist/tests/helpers/other-instance.js +27 -5
  94. package/dist/tests/helpers/other-instance.js.map +1 -1
  95. package/dist/tests/index/get_guard.test.js +6 -6
  96. package/dist/tests/index/get_guard.test.js.map +1 -1
  97. package/dist/tests/index/inheritance.test.js +10 -9
  98. package/dist/tests/index/inheritance.test.js.map +1 -1
  99. package/dist/tests/index/routes.test.js +40 -44
  100. package/dist/tests/index/routes.test.js.map +1 -1
  101. package/dist/tests/integration.test.d.ts +1 -1
  102. package/dist/tests/integration.test.js +53 -729
  103. package/dist/tests/integration.test.js.map +1 -1
  104. package/dist/tests/internal/http.d.ts +23 -0
  105. package/dist/tests/internal/http.js +35 -0
  106. package/dist/tests/internal/http.js.map +1 -0
  107. package/dist/tests/internal/shop-api.d.ts +1 -0
  108. package/dist/tests/internal/shop-api.js +331 -0
  109. package/dist/tests/internal/shop-api.js.map +1 -0
  110. package/dist/tests/internal/shop-schema.d.ts +154 -0
  111. package/dist/tests/internal/shop-schema.js +387 -0
  112. package/dist/tests/internal/shop-schema.js.map +1 -0
  113. package/dist/tests/utils.d.ts +5 -5
  114. package/dist/tests/utils.js +1 -3
  115. package/dist/tests/utils.js.map +1 -1
  116. package/dist/tests/utils.test.js +10 -11
  117. package/dist/tests/utils.test.js.map +1 -1
  118. package/docs/3.routes.md +8 -3
  119. package/docs/5.validators.md +16 -7
  120. package/docs/6.listable.md +4 -0
  121. package/docs/8.filters.md +7 -3
  122. package/package.json +17 -6
  123. package/skills/data-api-interface/SKILL.md +4 -0
  124. package/skills/data-api-interface/cases.json +99 -0
@@ -1,29 +1,61 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  exports.Validation = exports.Query = exports.Parameters = void 0;
4
37
  const interface_api_util_1 = require("@antelopejs/interface-api-util");
5
38
  const common_1 = require("@antelopejs/interface-database-decorators/common");
6
39
  const decorators_1 = require("@antelopejs/interface-core/decorators");
7
40
  const interface_api_1 = require("@antelopejs/interface-api");
8
- const interface_database_1 = require("@antelopejs/interface-database");
41
+ require("@antelopejs/interface-database");
9
42
  const common_2 = require("@antelopejs/interface-database-decorators/modifiers/common");
10
43
  const metadata_1 = require("./metadata");
44
+ const computed = __importStar(require("./internal/computed"));
45
+ const filters_1 = require("./internal/filters");
46
+ const foreign = __importStar(require("./internal/foreign"));
47
+ const guards_1 = require("./internal/guards");
48
+ const joined = __importStar(require("./internal/joined"));
49
+ const list = __importStar(require("./internal/list"));
50
+ const properties = __importStar(require("./internal/properties"));
51
+ const tables = __importStar(require("./internal/tables"));
52
+ const validation = __importStar(require("./internal/validation"));
11
53
  var Parameters;
12
54
  (function (Parameters) {
13
55
  function GetOptionOverrides(reqCtx) {
14
56
  return reqCtx.dataAPIEntry?.options ?? {};
15
57
  }
16
58
  Parameters.GetOptionOverrides = GetOptionOverrides;
17
- function parseFilter(searchVal) {
18
- const match = searchVal.match(/([^:]+):(.*)/);
19
- return match ? [match[2], match[1]] : [searchVal, "eq"];
20
- }
21
- function copyFilter(filter) {
22
- const copies = (0, metadata_1.FilterValuesOf)(filter).map((value) => [
23
- ...value,
24
- ]);
25
- return copies.length === 1 ? copies[0] : copies;
26
- }
27
59
  /**
28
60
  * Extracts request filters and mandatory route filters, with route field
29
61
  * keys winning. A field the request filters once (`?filter_status=open`)
@@ -36,14 +68,14 @@ var Parameters;
36
68
  for (const filter of Object.keys(meta.filters)) {
37
69
  const values = reqCtx.url.searchParams
38
70
  .getAll(`filter_${filter}`)
39
- .map(parseFilter);
71
+ .map(filters_1.parseFilter);
40
72
  if (values.length > 0) {
41
73
  result[filter] = values.length === 1 ? values[0] : values;
42
74
  }
43
75
  }
44
76
  const overrides = GetOptionOverrides(reqCtx);
45
77
  for (const [key, value] of Object.entries(overrides.filters ?? {})) {
46
- result[key] = copyFilter(value);
78
+ result[key] = (0, filters_1.copyFilter)(value);
47
79
  }
48
80
  return result;
49
81
  }
@@ -54,29 +86,40 @@ var Parameters;
54
86
  bool: (val) => val !== "0",
55
87
  string: (val) => val,
56
88
  };
89
+ function isConverterKey(name) {
90
+ return Object.hasOwn(converters, name);
91
+ }
92
+ function converterOf(name) {
93
+ if (!isConverterKey(name)) {
94
+ throw new TypeError(`Unknown parameter converter "${name}"`);
95
+ }
96
+ return converters[name];
97
+ }
98
+ function extractFromSearchParams(reqCtx, key, extractor) {
99
+ if (extractor.startsWith("multi:")) {
100
+ const name = extractor.substring(6);
101
+ return reqCtx.url.searchParams
102
+ .getAll(key)
103
+ .map((searchVal) => converterOf(name)(searchVal));
104
+ }
105
+ const searchVal = reqCtx.url.searchParams.get(key);
106
+ return searchVal === null ? undefined : converterOf(extractor)(searchVal);
107
+ }
57
108
  function ExtractGeneric(reqCtx, meta, dynamic) {
58
109
  const overrides = GetOptionOverrides(reqCtx);
59
110
  const result = { ...overrides };
60
111
  for (const key of Object.keys(dynamic)) {
61
- if (!(key in result) || dynamic[key] === ExtractFilters) {
62
- const extractor = dynamic[key];
63
- if (typeof extractor === "string") {
64
- if (extractor.startsWith("multi:")) {
65
- const converter = converters[extractor.substring(6)];
66
- result[key] = reqCtx.url.searchParams
67
- .getAll(key)
68
- .map((searchVal) => converter(searchVal));
69
- }
70
- else {
71
- const searchVal = reqCtx.url.searchParams.get(key);
72
- if (searchVal !== null) {
73
- result[key] = converters[extractor](searchVal);
74
- }
75
- }
76
- }
77
- else {
78
- result[key] = extractor(reqCtx, meta);
79
- }
112
+ const extractor = dynamic[key];
113
+ if (key in result && extractor !== ExtractFilters)
114
+ continue;
115
+ if ((0, guards_1.isString)(extractor)) {
116
+ const value = extractFromSearchParams(reqCtx, key, extractor);
117
+ if (value !== undefined)
118
+ result[key] = value;
119
+ }
120
+ else {
121
+ // SAFETY: key comes from Object.keys(dynamic), typed GenericParams<Partial<T>> (a mapped type over keyof T), so for a type-checked argument it is a keyof T; false only for a stray key on a non-literal argument, which is harmless as the key is only written on result.
122
+ result[key] = extractor(reqCtx, meta);
80
123
  }
81
124
  }
82
125
  return result;
@@ -101,7 +144,7 @@ var Parameters;
101
144
  const params = ExtractGeneric(context, (0, metadata_1.GetDataControllerMeta)(this), {
102
145
  id: "string",
103
146
  });
104
- (0, interface_api_util_1.assert)(params.id && typeof params.id === "string", 400, "Missing id.");
147
+ (0, interface_api_util_1.assert)(params.id && (0, guards_1.isString)(params.id), 400, "Missing id.");
105
148
  return params;
106
149
  }));
107
150
  Parameters.New = (0, decorators_1.MakeParameterAndPropertyDecorator)((target, key, param) => (0, interface_api_1.SetParameterProvider)(target, key, param, function (context) {
@@ -112,7 +155,7 @@ var Parameters;
112
155
  const params = ExtractGeneric(context, (0, metadata_1.GetDataControllerMeta)(this), {
113
156
  id: "string",
114
157
  });
115
- (0, interface_api_util_1.assert)(params.id && typeof params.id === "string", 400, "Missing id.");
158
+ (0, interface_api_util_1.assert)(params.id && (0, guards_1.isString)(params.id), 400, "Missing id.");
116
159
  return params;
117
160
  }));
118
161
  Parameters.Delete = (0, decorators_1.MakeParameterAndPropertyDecorator)((target, key, param) => (0, interface_api_1.SetParameterProvider)(target, key, param, function (context) {
@@ -131,394 +174,15 @@ var Query;
131
174
  }
132
175
  Query.GetModel = GetModel;
133
176
  function Foreign(db, meta, query, pluck) {
134
- /**/
135
- if ((0, interface_database_1.IsStream)(query)) {
136
- for (const [name, field] of Object.entries(meta.fields)) {
137
- if (!field.foreign || (pluck && !pluck.has(name))) {
138
- continue;
139
- }
140
- const [table, tableClass, index, multi, pluckField, schemaName, targetJoined,] = field.foreign;
141
- const other = targetTable(db, { table, tableClass, schemaName });
142
- const hasTargetJoined = !multi && !!targetJoined?.length;
143
- if (pluckField) {
144
- // The join keys of the target's @Joined fields must be fetched too
145
- // (so the second lookup can resolve them), but they are NOT stored in
146
- // `foreign[4]`, so they get stripped from the response by
147
- // `ClearInternal` afterwards.
148
- const lookupPluck = hasTargetJoined
149
- ? Array.from(new Set([
150
- ...pluckField,
151
- ...targetJoined.map((g) => g.localKey),
152
- ]))
153
- : pluckField;
154
- query = query.lookup(other.pluck("_internal", ...lookupPluck), name, index || "_id");
155
- }
156
- else {
157
- query = query.lookup(other, name, index || "_id");
158
- }
159
- if (hasTargetJoined) {
160
- query = applyForeignJoinedStream(db, query, name, targetJoined);
161
- }
162
- }
163
- return query;
164
- }
165
- else {
166
- const oldForeign = (obj) => {
167
- const changedFields = {};
168
- for (const [name, field] of Object.entries(meta.fields)) {
169
- if (!field.foreign || (pluck && !pluck.has(name))) {
170
- continue;
171
- }
172
- const [table, tableClass, index, multi, pluckField, schemaName, targetJoined,] = field.foreign;
173
- const remote = targetTable(db, { table, tableClass, schemaName });
174
- if (multi) {
175
- changedFields[name] = obj.key(name)
176
- .default([])
177
- .map((val) => {
178
- let foreignObject = Get(remote, val, index);
179
- if (pluckField) {
180
- foreignObject = foreignObject.pluck("_internal", ...pluckField);
181
- }
182
- return foreignObject.default(null);
183
- });
184
- }
185
- else {
186
- let foreignObject = Get(remote, obj.key(name), index);
187
- if (targetJoined && targetJoined.length > 0) {
188
- foreignObject = applyForeignJoinedDatum(db, foreignObject, targetJoined);
189
- }
190
- changedFields[name] = foreignObject.default(null);
191
- }
192
- }
193
- return obj.merge(changedFields);
194
- };
195
- return query.do(oldForeign);
196
- }
177
+ return foreign.Foreign(db, meta, query, pluck);
197
178
  }
198
179
  Query.Foreign = Foreign;
199
- /**
200
- * Re-materializes the `@Joined` fields of a relation target on the looked-up
201
- * sub-object (Stream/list path). The base `Foreign` lookup only plucks the
202
- * target's physical columns, so a joined label would come back empty; here we
203
- * resolve each joined field with a second lookup keyed on the target's
204
- * (already plucked) local key, then merge it back into the relation object.
205
- */
206
- function applyForeignJoinedStream(db, stream, foreignName, groups) {
207
- let s = stream;
208
- for (const group of groups) {
209
- const remoteTable = targetTable(db, group).pluck("_internal", group.remoteIndex, group.remoteField);
210
- const tmpKey = `__fjoin_${foreignName}_${group.name}`;
211
- s = s
212
- .map((row) => row.merge({
213
- [tmpKey]: row.key(foreignName).key(group.localKey),
214
- }))
215
- .lookup(remoteTable, tmpKey, group.remoteIndex)
216
- .map((row) => row.merge({
217
- // For an orphan foreign key this yields `{ [joined]: null }`; it is
218
- // normalized back to a null relation in `ClearInternal` (the DSL has
219
- // no value-level conditional to do it here).
220
- [foreignName]: row.key(foreignName).merge({
221
- [group.name]: row
222
- .key(tmpKey)
223
- .default({})
224
- .key(group.remoteField)
225
- .default(null),
226
- }),
227
- }))
228
- .without(tmpKey);
229
- }
230
- return s;
231
- }
232
- /**
233
- * Datum/get counterpart of {@link applyForeignJoinedStream}: resolves each of
234
- * the target's `@Joined` fields with a secondary `Get` and merges it into the
235
- * relation object.
236
- */
237
- function applyForeignJoinedDatum(db, datum, groups) {
238
- let d = datum;
239
- for (const group of groups) {
240
- // Use the raw table here (not a plucked stream): `Get` relies on
241
- // `getAll`/`get`, which exist on Selection/Table but not on a Stream.
242
- const remoteTable = targetTable(db, group);
243
- d = d.do((row) => row.merge({
244
- [group.name]: Get(remoteTable, row.key(group.localKey), group.remoteIndex)
245
- .key(group.remoteField)
246
- .default(null),
247
- }));
248
- }
249
- return d;
250
- }
251
- function collectJoinedGroups(meta) {
252
- const groups = new Map();
253
- for (const [name, field] of Object.entries(meta.fields)) {
254
- if (!field.joined)
255
- continue;
256
- const j = field.joined;
257
- const groupKey = `${j.schemaName ?? ""}|${j.table}|${j.localKey}|${j.remoteIndex}`;
258
- let group = groups.get(groupKey);
259
- if (!group) {
260
- group = {
261
- table: j.table,
262
- tableClass: j.tableClass,
263
- localKey: j.localKey,
264
- remoteIndex: j.remoteIndex,
265
- schemaName: j.schemaName,
266
- fields: [],
267
- };
268
- groups.set(groupKey, group);
269
- }
270
- group.fields.push({ name, remoteField: j.remoteField });
271
- }
272
- return Array.from(groups.values());
273
- }
274
- function addAll(target, names) {
275
- for (const name of names) {
276
- target.add(name);
277
- }
278
- }
279
- /**
280
- * Splits the joined fields of `meta` between those {@link List} must
281
- * materialize before the filter and count (`filter`) and those it only
282
- * needs after the count (`sort`). Joined fields referenced by neither are
283
- * left out so the list route can defer their lookups to the returned page.
284
- *
285
- * Computed expressions may read joined fields, so materializing a computed
286
- * field pulls every remaining joined group in with it. Groups whose local
287
- * key is also a foreign field must resolve before `Foreign` replaces the
288
- * key with the looked-up record.
289
- */
290
- function splitJoinedFields(meta, sorting, filters) {
291
- const split = { filter: new Set(), sort: new Set() };
292
- const filterNames = Object.keys(meta.filters).filter((name) => filters && name in filters);
293
- const hasComputedFilter = filterNames.some((name) => meta.fields[name]?.computed);
294
- const sortField = sorting?.[0];
295
- const hasComputedSort = !!sortField && !!meta.fields[sortField]?.computed;
296
- for (const group of collectJoinedGroups(meta)) {
297
- const names = group.fields.map((field) => field.name);
298
- if (hasComputedFilter ||
299
- names.some((name) => filterNames.includes(name))) {
300
- addAll(split.filter, names);
301
- }
302
- else if (hasComputedSort ||
303
- (sortField && names.includes(sortField)) ||
304
- meta.fields[group.localKey]?.foreign) {
305
- addAll(split.sort, names);
306
- }
307
- }
308
- return split;
309
- }
310
- /**
311
- * Joined field names that {@link List} does not materialize for the given
312
- * split. The default list route resolves these after pagination so their
313
- * lookups only run on the returned page.
314
- */
315
- function deferredJoinedFields(meta, split) {
316
- return new Set(Object.entries(meta.fields)
317
- .filter(([name, field]) => field.joined && !split.filter.has(name) && !split.sort.has(name))
318
- .map(([name]) => name));
319
- }
320
- /**
321
- * Stream returned by {@link List} when the deferred joined fields are not
322
- * exposed to the caller: it resolves them itself, so consumers that
323
- * destructure `[query, total]` alone still observe every `@Joined` field.
324
- * The lookups are appended after `slice`/`nth`, keeping them on the
325
- * requested page only; bare `count()` calls and field aggregates on
326
- * non-joined fields skip them (left-join lookups never change the row
327
- * count nor other fields); `changes()` attaches to the raw stream, since
328
- * change feeds cannot carry the lookups. Any other operation may observe
329
- * the joined fields, so it materializes them first.
330
- */
331
- class DeferredJoinedStream extends interface_database_1.Stream {
332
- }
333
- function materializeDeferredJoin(stream) {
334
- const { db, meta, names, raw } = stream.deferredJoin;
335
- return Joined(db, meta, raw, names);
336
- }
337
- function pagedDeferredJoinOperation(name) {
338
- return function (...args) {
339
- const { db, meta, names, raw } = this.deferredJoin;
340
- const paged = raw[name](...args);
341
- return Joined(db, meta, paged, names);
342
- };
343
- }
344
- /**
345
- * Resolves the joined fields through the stream form of {@link Joined}
346
- * rather than its datum form: only the datum form has no way to drop the
347
- * temporary key it stages (neither `Datum` nor `ValueProxy` exposes
348
- * `without`), so it would leave `__joined_orig_*` on the row.
349
- */
350
- function nthDeferredJoinOperation() {
351
- return function (n) {
352
- const { db, meta, names, raw } = this.deferredJoin;
353
- return Joined(db, meta, raw.slice(n, 1), names).nth(0);
354
- };
355
- }
356
- function rawDeferredJoinOperation(name) {
357
- return function (...args) {
358
- return this.deferredJoin.raw[name](...args);
359
- };
360
- }
361
- function fieldAggregateDeferredJoinOperation(name, bareCallOnRaw) {
362
- return function (field) {
363
- const { db, meta, names, raw } = this.deferredJoin;
364
- if (!field) {
365
- const bareTarget = bareCallOnRaw ? raw : materializeDeferredJoin(this);
366
- return bareTarget[name]();
367
- }
368
- const target = names.has(field)
369
- ? Joined(db, meta, raw, new Set([field]))
370
- : raw;
371
- return target[name](field);
372
- };
373
- }
374
- function forwardingDeferredJoinOperation(name) {
375
- return function (...args) {
376
- return materializeDeferredJoin(this)[name](...args);
377
- };
378
- }
379
- const DEFERRED_JOIN_OPERATION_FACTORIES = {
380
- slice: pagedDeferredJoinOperation,
381
- nth: nthDeferredJoinOperation,
382
- changes: rawDeferredJoinOperation,
383
- count: (name) => fieldAggregateDeferredJoinOperation(name, true),
384
- sum: (name) => fieldAggregateDeferredJoinOperation(name, false),
385
- avg: (name) => fieldAggregateDeferredJoinOperation(name, false),
386
- min: (name) => fieldAggregateDeferredJoinOperation(name, false),
387
- max: (name) => fieldAggregateDeferredJoinOperation(name, false),
388
- distinct: (name) => fieldAggregateDeferredJoinOperation(name, false),
389
- };
390
- const DEFERRED_JOIN_INHERITED_OPERATIONS = new Set(["constructor", "cast"]);
391
- function wireDeferredJoinedStream() {
392
- const operationNames = [
393
- ...Object.getOwnPropertyNames(interface_database_1.Stream.prototype),
394
- "run",
395
- "cursor",
396
- "build",
397
- ].filter((name) => !DEFERRED_JOIN_INHERITED_OPERATIONS.has(name));
398
- for (const name of operationNames) {
399
- const factory = DEFERRED_JOIN_OPERATION_FACTORIES[name] ??
400
- forwardingDeferredJoinOperation;
401
- DeferredJoinedStream.prototype[name] = factory(name);
402
- }
403
- }
404
- wireDeferredJoinedStream();
405
- /**
406
- * Wraps `raw` into a {@link DeferredJoinedStream}. The wrapper keeps the
407
- * raw stages as its own (an unobservable fallback, since every operation
408
- * is overridden) while `build()` serializes the materialized pipeline, so
409
- * a wrapper embedded as an argument of another query never leaks rows
410
- * without the joined fields.
411
- */
412
- function wrapDeferredJoined(db, meta, names, raw) {
413
- const wrapped = Object.create(DeferredJoinedStream.prototype);
414
- wrapped.stages = raw.build();
415
- wrapped.deferredJoin = { db, meta, names, raw };
416
- return wrapped;
417
- }
418
- function listResultTuple(tmpRequest, total, meta, joinedSplit, db, options) {
419
- const deferredJoined = deferredJoinedFields(meta, joinedSplit);
420
- if (options?.exposeDeferredJoined || !db || deferredJoined.size === 0) {
421
- return [tmpRequest, total, deferredJoined];
422
- }
423
- return [
424
- wrapDeferredJoined(db, meta, deferredJoined, tmpRequest),
425
- total,
426
- new Set(),
427
- ];
428
- }
429
- function targetTable(sourceDb, target) {
430
- if (!target.schemaName)
431
- return sourceDb.table(target.table);
432
- (0, interface_api_util_1.assert)(target.tableClass, 500, `Table "${target.table}" of schema "${target.schemaName}" has no registered table class`);
433
- return target.tableClass.table();
434
- }
435
180
  function Joined(db, meta, query, only) {
436
- const groups = collectJoinedGroups(meta).filter((group) => !only || group.fields.some((field) => only.has(field.name)));
437
- if (groups.length === 0) {
438
- return query;
439
- }
440
- const emptyRemote = {};
441
- const tmpKeyFor = (localKey) => `__joined_orig_${localKey}`;
442
- if ((0, interface_database_1.IsStream)(query)) {
443
- let stream = query;
444
- const tmpKeys = [];
445
- for (const group of groups) {
446
- const remoteFields = group.fields.map((f) => f.remoteField);
447
- const remoteTable = targetTable(db, group).pluck("_internal", group.remoteIndex, ...remoteFields);
448
- const tmpKey = tmpKeyFor(group.localKey);
449
- tmpKeys.push(tmpKey);
450
- stream = stream
451
- .map((row) => row.merge({ [tmpKey]: row.key(group.localKey) }))
452
- .lookup(remoteTable, group.localKey, group.remoteIndex)
453
- .map((row) => {
454
- const merged = {};
455
- const remote = row.key(group.localKey).default(emptyRemote);
456
- for (const f of group.fields) {
457
- merged[f.name] = remote.key(f.remoteField).default(null);
458
- }
459
- merged[group.localKey] = row.key(tmpKey);
460
- return row.merge(merged);
461
- });
462
- }
463
- return stream.without(...tmpKeys);
464
- }
465
- let datum = query;
466
- for (const group of groups) {
467
- const remoteFields = group.fields.map((f) => f.remoteField);
468
- const remoteTable = targetTable(db, group).pluck("_internal", group.remoteIndex, ...remoteFields);
469
- const tmpKey = tmpKeyFor(group.localKey);
470
- datum = datum
471
- .do((row) => row.merge({ [tmpKey]: row.key(group.localKey) }))
472
- .lookup(remoteTable, group.localKey, group.remoteIndex)
473
- .do((row) => {
474
- const merged = {};
475
- const remote = row.key(group.localKey).default(emptyRemote);
476
- for (const f of group.fields) {
477
- merged[f.name] = remote.key(f.remoteField).default(null);
478
- }
479
- merged[group.localKey] = row.key(tmpKey);
480
- return row.merge(merged);
481
- });
482
- }
483
- return datum;
181
+ return joined.Joined(db, meta, query, only);
484
182
  }
485
183
  Query.Joined = Joined;
486
- function collectComputedEntries(meta, only) {
487
- return Object.entries(meta.fields)
488
- .filter(([name, field]) => field.computed && (!only || only.has(name)))
489
- .map(([name, field]) => [name, field.computed]);
490
- }
491
- function buildComputedMappers(db, entries) {
492
- const mergeExpressions = (row) => {
493
- const merged = {};
494
- for (const [name, computed] of entries) {
495
- merged[name] = computed.expr(row, db);
496
- }
497
- return row.merge(merged);
498
- };
499
- const defaulted = entries.filter(([, computed]) => computed.default !== undefined);
500
- if (defaulted.length === 0) {
501
- return [mergeExpressions];
502
- }
503
- const applyDefaults = (row) => {
504
- const merged = {};
505
- for (const [name, computed] of defaulted) {
506
- merged[name] = row.key(name).default(computed.default);
507
- }
508
- return row.merge(merged);
509
- };
510
- return [mergeExpressions, applyDefaults];
511
- }
512
184
  function Computed(db, meta, query, only) {
513
- const entries = collectComputedEntries(meta, only);
514
- if (entries.length === 0) {
515
- return query;
516
- }
517
- const mappers = buildComputedMappers(db, entries);
518
- if ((0, interface_database_1.IsStream)(query)) {
519
- return mappers.reduce((stream, mapper) => stream.map(mapper), query);
520
- }
521
- return mappers.reduce((datum, mapper) => datum.do(mapper), query);
185
+ return computed.Computed(db, meta, query, only);
522
186
  }
523
187
  Query.Computed = Computed;
524
188
  async function ReadProperties(obj, meta, dbData, action, onlyList) {
@@ -539,27 +203,11 @@ var Query;
539
203
  continue;
540
204
  }
541
205
  const val = field.desc?.get?.apply(instance);
542
- res[key] = await (typeof val === "function" ? val() : val);
206
+ res[key] = await ((0, guards_1.isFunction)(val) ? val() : val);
543
207
  }
544
208
  return res;
545
209
  }
546
210
  Query.ReadProperties = ReadProperties;
547
- function writableInBody(writable, bodyData) {
548
- return {
549
- props: writable.props.filter(([key]) => Object.hasOwn(bodyData, key)),
550
- setters: writable.setters.filter(([key]) => Object.hasOwn(bodyData, key)),
551
- };
552
- }
553
- function writeTargetDefaults(instance, meta, dbData) {
554
- for (const [key, value] of Object.entries(new meta.target())) {
555
- if (value !== undefined) {
556
- instance[key] = value;
557
- if (key in meta.fields) {
558
- dbData[meta.fields[key].dbName || key] = value;
559
- }
560
- }
561
- }
562
- }
563
211
  /**
564
212
  * Writes the writable fields of `bodyData` into a database row.
565
213
  *
@@ -571,32 +219,11 @@ var Query;
571
219
  * `null` clears it.
572
220
  */
573
221
  async function WriteProperties(obj, meta, bodyData, action, existingDBData) {
574
- const actionWritable = meta.writable[action ?? "_default"] ?? meta.writable._default;
575
- const writable = existingDBData
576
- ? writableInBody(actionWritable, bodyData)
577
- : actionWritable;
578
- const instance = { ...obj };
579
- const dbData = existingDBData || {};
580
- Object.setPrototypeOf(dbData, meta.tableClass.prototype);
581
- if (!existingDBData) {
582
- writeTargetDefaults(instance, meta, dbData);
583
- }
584
- for (const [key, field] of writable.props) {
585
- instance[key] = bodyData[key];
586
- dbData[field.dbName || key] = bodyData[key];
587
- }
588
- instance.table = dbData;
589
- Object.setPrototypeOf(instance, meta.target.prototype);
590
- for (const [key, field] of writable.setters) {
591
- field.desc?.set?.apply(instance, [bodyData[key]]);
592
- }
593
- return dbData;
222
+ return properties.WriteProperties(obj, meta, bodyData, action, existingDBData);
594
223
  }
595
224
  Query.WriteProperties = WriteProperties;
596
225
  function Get(table, id, index) {
597
- return index
598
- ? table.getAll(id, index).nth(0)
599
- : table.get(id);
226
+ return tables.Get(table, id, index);
600
227
  }
601
228
  Query.Get = Get;
602
229
  /**
@@ -609,83 +236,15 @@ var Query;
609
236
  * since no lookup can be built without one — the raw stream is returned
610
237
  * and the third element names the joined fields left for the caller.
611
238
  */
612
- function List(obj, meta, request, reqCtx, sorting, filters, db, options) {
613
- const filterList = Object.entries(meta.filters).filter(([name]) => filters && name in filters);
614
- // The index serves one equality; a field filtered several times is
615
- // filtered row by row, each of its filters in turn.
616
- const indexFilter = filterList.find(([name]) => {
617
- const values = filters ? (0, metadata_1.FilterValuesOf)(filters[name]) : [];
618
- return (values.length === 1 &&
619
- values[0][1] === "eq" &&
620
- meta.fields[name]?.indexable);
621
- })?.[0];
622
- const index = indexFilter
623
- ? meta.fields[indexFilter].dbName || indexFilter
624
- : undefined;
625
- const indexedFilter = indexFilter
626
- ? (0, metadata_1.FilterValuesOf)(filters?.[indexFilter] ?? [])[0]
627
- : undefined;
628
- let tmpRequest = index
629
- ? request.getAll(indexedFilter?.[0] ?? "", index)
630
- : request;
631
- const joinedSplit = splitJoinedFields(meta, sorting, filters);
632
- if (db && joinedSplit.filter.size > 0) {
633
- tmpRequest = Joined(db, meta, tmpRequest, joinedSplit.filter);
634
- }
635
- const filteredComputed = new Set(filterList
636
- .map(([name]) => name)
637
- .filter((name) => meta.fields[name]?.computed));
638
- if (db && filteredComputed.size > 0) {
639
- tmpRequest = Computed(db, meta, tmpRequest, filteredComputed);
640
- }
641
- const sortField = sorting?.[0];
642
- const shouldSort = sortField ? meta.fields[sortField]?.sortable : undefined;
643
- if (shouldSort?.indexed && sortField) {
644
- tmpRequest = tmpRequest.orderBy(sortField, sorting?.[1] ?? "asc");
645
- }
646
- if (filterList.length > 0) {
647
- // TODO: rework for modifier-affected fields (unlockrequest)
648
- tmpRequest = filterList.reduce((req, [name, filter]) => {
649
- const fieldFilter = filters?.[name];
650
- if (!fieldFilter || name === indexFilter)
651
- return req;
652
- return (0, metadata_1.FilterValuesOf)(fieldFilter).reduce((filtered, [value, mode]) => filtered.filter((row) => filter(Object.assign(reqCtx, { this: obj }), Validation.UnlockRequest(obj, meta, row, name), name, value, mode, row)), req);
653
- }, tmpRequest);
654
- }
655
- const total = tmpRequest.count();
656
- if (db && joinedSplit.sort.size > 0) {
657
- tmpRequest = Joined(db, meta, tmpRequest, joinedSplit.sort);
658
- }
659
- if (db &&
660
- sortField &&
661
- meta.fields[sortField]?.computed &&
662
- !filteredComputed.has(sortField)) {
663
- tmpRequest = Computed(db, meta, tmpRequest, new Set([sortField]));
664
- }
665
- if (shouldSort && !shouldSort.indexed && sortField) {
666
- tmpRequest = tmpRequest.orderBy(sortField, sorting?.[1] ?? "asc");
667
- }
668
- return listResultTuple(tmpRequest, total, meta, joinedSplit, db, options);
239
+ function List(obj, meta, request, reqCtx, ...[sorting, filters, db, options]) {
240
+ return list.List(obj, meta, request, reqCtx, {
241
+ sorting,
242
+ filters,
243
+ db,
244
+ options,
245
+ });
669
246
  }
670
247
  Query.List = List;
671
- function pageComputedFields(meta, options) {
672
- const merged = new Set(Object.keys(options.filters ?? {}).filter((name) => name in meta.filters));
673
- if (options.sortKey)
674
- merged.add(options.sortKey);
675
- const names = Object.entries(meta.fields)
676
- .filter(([name, field]) => field.computed &&
677
- !merged.has(name) &&
678
- (!options.pluck || options.pluck.has(name)))
679
- .map(([name]) => name);
680
- return new Set(names);
681
- }
682
- function pageJoinedFields(options, materializeAll) {
683
- const deferred = options.deferredJoined ?? new Set();
684
- if (materializeAll || !options.pluck)
685
- return deferred;
686
- const pluck = options.pluck;
687
- return new Set(Array.from(deferred).filter((name) => pluck.has(name)));
688
- }
689
248
  /**
690
249
  * One page of the stream {@link List} built, with the fields it answers:
691
250
  * the deferred joined fields, then the computed fields no filter or sort
@@ -695,19 +254,7 @@ var Query;
695
254
  * it.
696
255
  */
697
256
  function ListPage(db, meta, query, options) {
698
- let page = query.slice(options.offset, options.limit);
699
- const computed = pageComputedFields(meta, options);
700
- // A computed expression may read any joined field, so when one is
701
- // materialized every deferred joined group is merged first, whatever
702
- // the pluck; the final pluck strips what the page did not ask for.
703
- const joined = pageJoinedFields(options, computed.size > 0);
704
- if (joined.size > 0)
705
- page = Joined(db, meta, page, joined);
706
- if (computed.size > 0)
707
- page = Computed(db, meta, page, computed);
708
- if (!options.noForeign)
709
- page = Foreign(db, meta, page, options.pluck);
710
- return options.pluck ? page.pluck("_internal", ...options.pluck) : page;
257
+ return list.ListPage(db, meta, query, options);
711
258
  }
712
259
  Query.ListPage = ListPage;
713
260
  function Delete(table, id) {
@@ -722,8 +269,8 @@ var Validation;
722
269
  function isClearingValue(value) {
723
270
  if (value === null || value === undefined)
724
271
  return true;
725
- if (typeof value === "string")
726
- return value.length === 0;
272
+ if (value === "")
273
+ return true;
727
274
  return Array.isArray(value) && value.length === 0;
728
275
  }
729
276
  const isAbsentFromBody = (body, name) => !(name in body);
@@ -731,28 +278,39 @@ var Validation;
731
278
  const MISSING_MANDATORY_FIELD_CHECKS = {
732
279
  edit: isClearedByBody,
733
280
  };
281
+ /** The `code` of a 400 naming the mandatory fields a body lacks. */
282
+ Validation.MISSING_MANDATORY_FIELDS = "missing_mandatory_fields";
283
+ /** The `code` of a 400 naming the fields whose validator refused the value. */
284
+ Validation.INVALID_FIELD_TYPES = "invalid_field_types";
285
+ function rejectFields(code, fields) {
286
+ if (fields.length === 0)
287
+ return;
288
+ throw new interface_api_1.HTTPResult(400, { code, fields });
289
+ }
734
290
  /**
735
291
  * Rejects with 400 the request bodies missing a field mandatory for `type`.
736
292
  * On `edit`, a mandatory field is missing when the body clears it (`null`,
737
293
  * `""` or `[]`); a field absent from the body keeps its stored value. On any
738
- * other action, it is missing when absent from the body.
294
+ * other action, it is missing when absent from the body. The body of the
295
+ * 400 is a {@link FieldsErrorBody} with {@link MISSING_MANDATORY_FIELDS}.
739
296
  */
740
297
  function MandatoryFields(meta, obj, type) {
741
298
  const isMissing = MISSING_MANDATORY_FIELD_CHECKS[type] ?? isAbsentFromBody;
742
299
  const missing = Object.entries(meta.fields)
743
300
  .filter(([name, field]) => field.mandatory?.has(type) && isMissing(obj, name))
744
301
  .map(([name]) => name);
745
- (0, interface_api_util_1.assert)(missing.length === 0, 400, `Missing mandatory fields: ${missing.join(", ")}`);
302
+ rejectFields(Validation.MISSING_MANDATORY_FIELDS, missing);
746
303
  }
747
304
  Validation.MandatoryFields = MandatoryFields;
748
305
  function toParseResult(result, value) {
749
- if (typeof result !== "boolean")
306
+ if (result !== true && result !== false)
750
307
  return result;
751
308
  return result ? { success: true, data: value } : { success: false };
752
309
  }
753
310
  /**
754
311
  * Runs the validator of each field present in `obj` and rejects with 400
755
- * when one fails. Returns a copy of `obj` where each validated field holds
312
+ * when one fails, with a {@link FieldsErrorBody} of
313
+ * {@link INVALID_FIELD_TYPES}. Returns a copy of `obj` where each validated field holds
756
314
  * the value its validator parsed (see {@link FieldValidationResult}).
757
315
  */
758
316
  async function ValidateTypes(meta, obj) {
@@ -769,7 +327,7 @@ var Validation;
769
327
  invalid.push(name);
770
328
  }
771
329
  }
772
- (0, interface_api_util_1.assert)(invalid.length === 0, 400, `Invalid field type(s): ${invalid.join(", ")}`);
330
+ rejectFields(Validation.INVALID_FIELD_TYPES, invalid);
773
331
  return parsed;
774
332
  }
775
333
  Validation.ValidateTypes = ValidateTypes;
@@ -778,17 +336,7 @@ var Validation;
778
336
  * body already parsed is checked, not parsed again.
779
337
  */
780
338
  function ParseBody(body) {
781
- let data = body;
782
- if (Buffer.isBuffer(body)) {
783
- try {
784
- data = JSON.parse(body.toString());
785
- }
786
- catch {
787
- (0, interface_api_util_1.assert)(false, 400, "Invalid JSON body.");
788
- }
789
- }
790
- (0, interface_api_util_1.assert)(typeof data === "object" && data !== null && !Array.isArray(data), 400, "The request body must be a JSON object.");
791
- return data;
339
+ return validation.ParseBody(body);
792
340
  }
793
341
  Validation.ParseBody = ParseBody;
794
342
  /**
@@ -806,78 +354,15 @@ var Validation;
806
354
  }
807
355
  Validation.Lock = Lock;
808
356
  function Unlock(obj, meta, dbData) {
809
- for (const [name, field] of Object.entries(meta.fields)) {
810
- if (field.foreign &&
811
- dbData[name] &&
812
- typeof dbData[name] === "object" &&
813
- field.foreign[1]) {
814
- const foreignSchema = field.foreign[1];
815
- dbData[name] = Array.isArray(dbData[name])
816
- ? dbData[name].map((entry) => (0, common_2.fromDatabase)(entry, foreignSchema))
817
- : (0, common_2.fromDatabase)(dbData[name], field.foreign[1]);
818
- }
819
- }
820
- for (const [modifier, field] of meta.modifierKeys.entries()) {
821
- const key = obj[field];
822
- (0, common_2.unlock)(dbData, modifier, undefined, key);
823
- for (const [foreign, fieldData] of Object.entries(meta.fields)) {
824
- if (fieldData.foreign &&
825
- dbData[foreign] &&
826
- typeof dbData[foreign] === "object") {
827
- (0, common_2.unlock)(dbData[foreign], modifier, undefined, key);
828
- }
829
- }
830
- }
357
+ validation.Unlock(obj, meta, dbData);
831
358
  }
832
359
  Validation.Unlock = Unlock;
833
360
  function UnlockRequest(obj, meta, row, field) {
834
- const modifiers = Array.from(meta.modifierKeys.entries()).map(([modifier, field]) => ({
835
- modifier,
836
- args: [obj[field]],
837
- }));
838
- return (0, common_2.unlockrequest)(meta.tableClass, row, field, modifiers);
361
+ return validation.UnlockRequest(obj, meta, row, field);
839
362
  }
840
363
  Validation.UnlockRequest = UnlockRequest;
841
364
  function ClearInternal(meta, obj) {
842
- const results = Array.isArray(obj) ? obj : [obj];
843
- const foreignFields = Object.entries(meta.fields).filter(([, field]) => field.foreign);
844
- for (const entry of results) {
845
- delete entry._internal;
846
- for (const [name, { foreign }] of foreignFields) {
847
- if (foreign &&
848
- entry[name] &&
849
- typeof entry[name] === "object" &&
850
- (!Array.isArray(entry[name]) || typeof entry[name][0] === "object")) {
851
- const processForeign = (data) => {
852
- const plain = (0, common_2.toPlainData)(data);
853
- if (!foreign?.[4]) {
854
- return plain;
855
- }
856
- // An orphan foreign key whose target exposes @Joined fields produces
857
- // an object with only those joined fields (all null) and none of the
858
- // physical pluck fields. Normalize it back to a null relation, so it
859
- // matches the get path and non-joined foreigns (which already null).
860
- const targetJoined = foreign[6];
861
- if (targetJoined?.length) {
862
- const joinedNames = new Set(targetJoined.map((g) => g.name));
863
- const physicalKeys = foreign[4].filter((k) => !joinedNames.has(k));
864
- if (physicalKeys.length > 0 &&
865
- physicalKeys.every((k) => plain[k] === null || plain[k] === undefined)) {
866
- return null;
867
- }
868
- }
869
- const plainPlucked = {};
870
- for (const key of foreign[4]) {
871
- plainPlucked[key] = plain[key];
872
- }
873
- return plainPlucked;
874
- };
875
- entry[name] = Array.isArray(entry[name])
876
- ? entry[name].map(processForeign)
877
- : processForeign(entry[name]);
878
- }
879
- }
880
- }
365
+ validation.ClearInternal(meta, obj);
881
366
  }
882
367
  Validation.ClearInternal = ClearInternal;
883
368
  })(Validation || (exports.Validation = Validation = {}));