@antelopejs/interface-data-api 0.4.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 (121) hide show
  1. package/dist/components.d.ts +15 -7
  2. package/dist/components.js +98 -645
  3. package/dist/components.js.map +1 -1
  4. package/dist/index.d.ts +41 -36
  5. package/dist/index.js +94 -92
  6. package/dist/index.js.map +1 -1
  7. package/dist/internal/computed.d.ts +6 -0
  8. package/dist/internal/computed.js +43 -0
  9. package/dist/internal/computed.js.map +1 -0
  10. package/dist/internal/controller-types.d.ts +19 -0
  11. package/dist/internal/controller-types.js +4 -0
  12. package/dist/internal/controller-types.js.map +1 -0
  13. package/dist/internal/default-filter.d.ts +4 -0
  14. package/dist/internal/default-filter.js +19 -0
  15. package/dist/internal/default-filter.js.map +1 -0
  16. package/dist/internal/deferred-joined-stream.d.ts +11 -0
  17. package/dist/internal/deferred-joined-stream.js +112 -0
  18. package/dist/internal/deferred-joined-stream.js.map +1 -0
  19. package/dist/internal/field-types.d.ts +183 -0
  20. package/dist/internal/field-types.js +4 -0
  21. package/dist/internal/field-types.js.map +1 -0
  22. package/dist/internal/filters.d.ts +4 -0
  23. package/dist/internal/filters.js +15 -0
  24. package/dist/internal/filters.js.map +1 -0
  25. package/dist/internal/foreign.d.ts +6 -0
  26. package/dist/internal/foreign.js +116 -0
  27. package/dist/internal/foreign.js.map +1 -0
  28. package/dist/internal/guards.d.ts +20 -0
  29. package/dist/internal/guards.js +74 -0
  30. package/dist/internal/guards.js.map +1 -0
  31. package/dist/internal/joined.d.ts +28 -0
  32. package/dist/internal/joined.js +127 -0
  33. package/dist/internal/joined.js.map +1 -0
  34. package/dist/internal/list.d.ts +16 -0
  35. package/dist/internal/list.js +178 -0
  36. package/dist/internal/list.js.map +1 -0
  37. package/dist/internal/meta-records.d.ts +17 -0
  38. package/dist/internal/meta-records.js +73 -0
  39. package/dist/internal/meta-records.js.map +1 -0
  40. package/dist/internal/properties.d.ts +4 -0
  41. package/dist/internal/properties.js +43 -0
  42. package/dist/internal/properties.js.map +1 -0
  43. package/dist/internal/table-reference.d.ts +11 -0
  44. package/dist/internal/table-reference.js +46 -0
  45. package/dist/internal/table-reference.js.map +1 -0
  46. package/dist/internal/tables.d.ts +18 -0
  47. package/dist/internal/tables.js +37 -0
  48. package/dist/internal/tables.js.map +1 -0
  49. package/dist/internal/validation.d.ts +11 -0
  50. package/dist/internal/validation.js +88 -0
  51. package/dist/internal/validation.js.map +1 -0
  52. package/dist/metadata.d.ts +27 -228
  53. package/dist/metadata.js +61 -188
  54. package/dist/metadata.js.map +1 -1
  55. package/dist/tests/components/access_control.test.js +20 -20
  56. package/dist/tests/components/access_control.test.js.map +1 -1
  57. package/dist/tests/components/computed.test.js +28 -30
  58. package/dist/tests/components/computed.test.js.map +1 -1
  59. package/dist/tests/components/cross_instance.test.js +8 -6
  60. package/dist/tests/components/cross_instance.test.js.map +1 -1
  61. package/dist/tests/components/deferred_joined.test.js +56 -53
  62. package/dist/tests/components/deferred_joined.test.js.map +1 -1
  63. package/dist/tests/components/filter_options.test.js +8 -7
  64. package/dist/tests/components/filter_options.test.js.map +1 -1
  65. package/dist/tests/components/foreign_computed.test.js +14 -16
  66. package/dist/tests/components/foreign_computed.test.js.map +1 -1
  67. package/dist/tests/components/foreign_joined.test.js +19 -27
  68. package/dist/tests/components/foreign_joined.test.js.map +1 -1
  69. package/dist/tests/components/joined.test.js +41 -45
  70. package/dist/tests/components/joined.test.js.map +1 -1
  71. package/dist/tests/components/listable.test.js +59 -58
  72. package/dist/tests/components/listable.test.js.map +1 -1
  73. package/dist/tests/components/localized_sort.test.js +10 -11
  74. package/dist/tests/components/localized_sort.test.js.map +1 -1
  75. package/dist/tests/components/mandatory.test.js +25 -25
  76. package/dist/tests/components/mandatory.test.js.map +1 -1
  77. package/dist/tests/components/modifier_key.test.js +19 -18
  78. package/dist/tests/components/modifier_key.test.js.map +1 -1
  79. package/dist/tests/components/parse_body.test.d.ts +1 -0
  80. package/dist/tests/components/parse_body.test.js +75 -0
  81. package/dist/tests/components/parse_body.test.js.map +1 -0
  82. package/dist/tests/components/parsed_value.test.js +23 -21
  83. package/dist/tests/components/parsed_value.test.js.map +1 -1
  84. package/dist/tests/components/partial_edit.test.js +33 -32
  85. package/dist/tests/components/partial_edit.test.js.map +1 -1
  86. package/dist/tests/components/per_action_access.test.js +12 -12
  87. package/dist/tests/components/per_action_access.test.js.map +1 -1
  88. package/dist/tests/components/validator.test.js +45 -37
  89. package/dist/tests/components/validator.test.js.map +1 -1
  90. package/dist/tests/helpers/other-instance.d.ts +11 -1
  91. package/dist/tests/helpers/other-instance.js +27 -5
  92. package/dist/tests/helpers/other-instance.js.map +1 -1
  93. package/dist/tests/index/get_guard.test.js +6 -6
  94. package/dist/tests/index/get_guard.test.js.map +1 -1
  95. package/dist/tests/index/inheritance.test.js +10 -9
  96. package/dist/tests/index/inheritance.test.js.map +1 -1
  97. package/dist/tests/index/routes.test.js +40 -44
  98. package/dist/tests/index/routes.test.js.map +1 -1
  99. package/dist/tests/integration.test.d.ts +1 -1
  100. package/dist/tests/integration.test.js +53 -729
  101. package/dist/tests/integration.test.js.map +1 -1
  102. package/dist/tests/internal/http.d.ts +23 -0
  103. package/dist/tests/internal/http.js +35 -0
  104. package/dist/tests/internal/http.js.map +1 -0
  105. package/dist/tests/internal/shop-api.d.ts +1 -0
  106. package/dist/tests/internal/shop-api.js +331 -0
  107. package/dist/tests/internal/shop-api.js.map +1 -0
  108. package/dist/tests/internal/shop-schema.d.ts +154 -0
  109. package/dist/tests/internal/shop-schema.js +387 -0
  110. package/dist/tests/internal/shop-schema.js.map +1 -0
  111. package/dist/tests/utils.d.ts +5 -5
  112. package/dist/tests/utils.js +1 -3
  113. package/dist/tests/utils.js.map +1 -1
  114. package/dist/tests/utils.test.js +10 -11
  115. package/dist/tests/utils.test.js.map +1 -1
  116. package/docs/3.routes.md +8 -3
  117. package/docs/5.validators.md +12 -3
  118. package/docs/8.filters.md +7 -3
  119. package/package.json +14 -3
  120. package/skills/data-api-interface/SKILL.md +4 -0
  121. 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,45 +219,13 @@ 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
- /** Where a list sorted on a modifier-held field keeps the value it sorts on. */
603
- const MODIFIED_SORT_KEY = "__dataApiSortValue";
604
- /**
605
- * Whether a field is held by a modifier the controller keys (a localized
606
- * field read in the request's locale): stored under the modifier, it sorts
607
- * on the value the request reads, not on the stored field.
608
- */
609
- function isHeldByKeyedModifier(meta, field) {
610
- const keyed = new Set(Array.from(meta.modifierKeys.keys(), (modifier) => modifier.name));
611
- return ((0, common_1.getMetadata)(meta.tableClass, common_2.ModifiersStaticMetadata).fields[field]?.some(({ id }) => keyed.has(id)) ?? false);
612
- }
613
229
  /**
614
230
  * Builds the filtered and sorted stream of a list request along with its
615
231
  * total count. Joined fields that no filter or sort references are not
@@ -620,93 +236,15 @@ var Query;
620
236
  * since no lookup can be built without one — the raw stream is returned
621
237
  * and the third element names the joined fields left for the caller.
622
238
  */
623
- function List(obj, meta, request, reqCtx, sorting, filters, db, options) {
624
- const filterList = Object.entries(meta.filters).filter(([name]) => filters && name in filters);
625
- // The index serves one equality; a field filtered several times is
626
- // filtered row by row, each of its filters in turn.
627
- const indexFilter = filterList.find(([name]) => {
628
- const values = filters ? (0, metadata_1.FilterValuesOf)(filters[name]) : [];
629
- return (values.length === 1 &&
630
- values[0][1] === "eq" &&
631
- meta.fields[name]?.indexable);
632
- })?.[0];
633
- const index = indexFilter
634
- ? meta.fields[indexFilter].dbName || indexFilter
635
- : undefined;
636
- const indexedFilter = indexFilter
637
- ? (0, metadata_1.FilterValuesOf)(filters?.[indexFilter] ?? [])[0]
638
- : undefined;
639
- let tmpRequest = index
640
- ? request.getAll(indexedFilter?.[0] ?? "", index)
641
- : request;
642
- const joinedSplit = splitJoinedFields(meta, sorting, filters);
643
- if (db && joinedSplit.filter.size > 0) {
644
- tmpRequest = Joined(db, meta, tmpRequest, joinedSplit.filter);
645
- }
646
- const filteredComputed = new Set(filterList
647
- .map(([name]) => name)
648
- .filter((name) => meta.fields[name]?.computed));
649
- if (db && filteredComputed.size > 0) {
650
- tmpRequest = Computed(db, meta, tmpRequest, filteredComputed);
651
- }
652
- const sortField = sorting?.[0];
653
- const shouldSort = sortField ? meta.fields[sortField]?.sortable : undefined;
654
- const isModifiedSort = !!shouldSort && !!sortField && isHeldByKeyedModifier(meta, sortField);
655
- if (shouldSort?.indexed && sortField && !isModifiedSort) {
656
- tmpRequest = tmpRequest.orderBy(sortField, sorting?.[1] ?? "asc");
657
- }
658
- if (filterList.length > 0) {
659
- // TODO: rework for modifier-affected fields (unlockrequest)
660
- tmpRequest = filterList.reduce((req, [name, filter]) => {
661
- const fieldFilter = filters?.[name];
662
- if (!fieldFilter || name === indexFilter)
663
- return req;
664
- 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);
665
- }, tmpRequest);
666
- }
667
- const total = tmpRequest.count();
668
- if (db && joinedSplit.sort.size > 0) {
669
- tmpRequest = Joined(db, meta, tmpRequest, joinedSplit.sort);
670
- }
671
- if (db &&
672
- sortField &&
673
- meta.fields[sortField]?.computed &&
674
- !filteredComputed.has(sortField)) {
675
- tmpRequest = Computed(db, meta, tmpRequest, new Set([sortField]));
676
- }
677
- if (isModifiedSort && sortField) {
678
- // Sorts on the value in the request's locale, else its fallback.
679
- tmpRequest = tmpRequest
680
- .map((row) => row.merge({
681
- [MODIFIED_SORT_KEY]: Validation.UnlockRequest(obj, meta, row, sortField),
682
- }))
683
- .orderBy(MODIFIED_SORT_KEY, sorting?.[1] ?? "asc")
684
- .without(MODIFIED_SORT_KEY);
685
- }
686
- else if (shouldSort && !shouldSort.indexed && sortField) {
687
- tmpRequest = tmpRequest.orderBy(sortField, sorting?.[1] ?? "asc");
688
- }
689
- 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
+ });
690
246
  }
691
247
  Query.List = List;
692
- function pageComputedFields(meta, options) {
693
- const merged = new Set(Object.keys(options.filters ?? {}).filter((name) => name in meta.filters));
694
- if (options.sortKey)
695
- merged.add(options.sortKey);
696
- const names = Object.entries(meta.fields)
697
- .filter(([name, field]) => field.computed &&
698
- !merged.has(name) &&
699
- (!options.pluck || options.pluck.has(name)))
700
- .map(([name]) => name);
701
- return new Set(names);
702
- }
703
- function pageJoinedFields(options, materializeAll) {
704
- const deferred = options.deferredJoined ?? new Set();
705
- if (materializeAll || !options.pluck)
706
- return deferred;
707
- const pluck = options.pluck;
708
- return new Set(Array.from(deferred).filter((name) => pluck.has(name)));
709
- }
710
248
  /**
711
249
  * One page of the stream {@link List} built, with the fields it answers:
712
250
  * the deferred joined fields, then the computed fields no filter or sort
@@ -716,19 +254,7 @@ var Query;
716
254
  * it.
717
255
  */
718
256
  function ListPage(db, meta, query, options) {
719
- let page = query.slice(options.offset, options.limit);
720
- const computed = pageComputedFields(meta, options);
721
- // A computed expression may read any joined field, so when one is
722
- // materialized every deferred joined group is merged first, whatever
723
- // the pluck; the final pluck strips what the page did not ask for.
724
- const joined = pageJoinedFields(options, computed.size > 0);
725
- if (joined.size > 0)
726
- page = Joined(db, meta, page, joined);
727
- if (computed.size > 0)
728
- page = Computed(db, meta, page, computed);
729
- if (!options.noForeign)
730
- page = Foreign(db, meta, page, options.pluck);
731
- return options.pluck ? page.pluck("_internal", ...options.pluck) : page;
257
+ return list.ListPage(db, meta, query, options);
732
258
  }
733
259
  Query.ListPage = ListPage;
734
260
  function Delete(table, id) {
@@ -743,8 +269,8 @@ var Validation;
743
269
  function isClearingValue(value) {
744
270
  if (value === null || value === undefined)
745
271
  return true;
746
- if (typeof value === "string")
747
- return value.length === 0;
272
+ if (value === "")
273
+ return true;
748
274
  return Array.isArray(value) && value.length === 0;
749
275
  }
750
276
  const isAbsentFromBody = (body, name) => !(name in body);
@@ -777,7 +303,7 @@ var Validation;
777
303
  }
778
304
  Validation.MandatoryFields = MandatoryFields;
779
305
  function toParseResult(result, value) {
780
- if (typeof result !== "boolean")
306
+ if (result !== true && result !== false)
781
307
  return result;
782
308
  return result ? { success: true, data: value } : { success: false };
783
309
  }
@@ -810,17 +336,7 @@ var Validation;
810
336
  * body already parsed is checked, not parsed again.
811
337
  */
812
338
  function ParseBody(body) {
813
- let data = body;
814
- if (Buffer.isBuffer(body)) {
815
- try {
816
- data = JSON.parse(body.toString());
817
- }
818
- catch {
819
- (0, interface_api_util_1.assert)(false, 400, "Invalid JSON body.");
820
- }
821
- }
822
- (0, interface_api_util_1.assert)(typeof data === "object" && data !== null && !Array.isArray(data), 400, "The request body must be a JSON object.");
823
- return data;
339
+ return validation.ParseBody(body);
824
340
  }
825
341
  Validation.ParseBody = ParseBody;
826
342
  /**
@@ -838,78 +354,15 @@ var Validation;
838
354
  }
839
355
  Validation.Lock = Lock;
840
356
  function Unlock(obj, meta, dbData) {
841
- for (const [name, field] of Object.entries(meta.fields)) {
842
- if (field.foreign &&
843
- dbData[name] &&
844
- typeof dbData[name] === "object" &&
845
- field.foreign[1]) {
846
- const foreignSchema = field.foreign[1];
847
- dbData[name] = Array.isArray(dbData[name])
848
- ? dbData[name].map((entry) => (0, common_2.fromDatabase)(entry, foreignSchema))
849
- : (0, common_2.fromDatabase)(dbData[name], field.foreign[1]);
850
- }
851
- }
852
- for (const [modifier, field] of meta.modifierKeys.entries()) {
853
- const key = obj[field];
854
- (0, common_2.unlock)(dbData, modifier, undefined, key);
855
- for (const [foreign, fieldData] of Object.entries(meta.fields)) {
856
- if (fieldData.foreign &&
857
- dbData[foreign] &&
858
- typeof dbData[foreign] === "object") {
859
- (0, common_2.unlock)(dbData[foreign], modifier, undefined, key);
860
- }
861
- }
862
- }
357
+ validation.Unlock(obj, meta, dbData);
863
358
  }
864
359
  Validation.Unlock = Unlock;
865
360
  function UnlockRequest(obj, meta, row, field) {
866
- const modifiers = Array.from(meta.modifierKeys.entries()).map(([modifier, field]) => ({
867
- modifier,
868
- args: [obj[field]],
869
- }));
870
- return (0, common_2.unlockrequest)(meta.tableClass, row, field, modifiers);
361
+ return validation.UnlockRequest(obj, meta, row, field);
871
362
  }
872
363
  Validation.UnlockRequest = UnlockRequest;
873
364
  function ClearInternal(meta, obj) {
874
- const results = Array.isArray(obj) ? obj : [obj];
875
- const foreignFields = Object.entries(meta.fields).filter(([, field]) => field.foreign);
876
- for (const entry of results) {
877
- delete entry._internal;
878
- for (const [name, { foreign }] of foreignFields) {
879
- if (foreign &&
880
- entry[name] &&
881
- typeof entry[name] === "object" &&
882
- (!Array.isArray(entry[name]) || typeof entry[name][0] === "object")) {
883
- const processForeign = (data) => {
884
- const plain = (0, common_2.toPlainData)(data);
885
- if (!foreign?.[4]) {
886
- return plain;
887
- }
888
- // An orphan foreign key whose target exposes @Joined fields produces
889
- // an object with only those joined fields (all null) and none of the
890
- // physical pluck fields. Normalize it back to a null relation, so it
891
- // matches the get path and non-joined foreigns (which already null).
892
- const targetJoined = foreign[6];
893
- if (targetJoined?.length) {
894
- const joinedNames = new Set(targetJoined.map((g) => g.name));
895
- const physicalKeys = foreign[4].filter((k) => !joinedNames.has(k));
896
- if (physicalKeys.length > 0 &&
897
- physicalKeys.every((k) => plain[k] === null || plain[k] === undefined)) {
898
- return null;
899
- }
900
- }
901
- const plainPlucked = {};
902
- for (const key of foreign[4]) {
903
- plainPlucked[key] = plain[key];
904
- }
905
- return plainPlucked;
906
- };
907
- entry[name] = Array.isArray(entry[name])
908
- ? entry[name].map(processForeign)
909
- : processForeign(entry[name]);
910
- }
911
- }
912
- }
365
+ validation.ClearInternal(meta, obj);
913
366
  }
914
367
  Validation.ClearInternal = ClearInternal;
915
368
  })(Validation || (exports.Validation = Validation = {}));