@appweaver/core 1.1.5 → 1.2.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 (43) hide show
  1. package/cache/eviction/lfu-eviction-index.js +3 -0
  2. package/export/export-service.d.ts +4 -2
  3. package/export/export-service.js +27 -17
  4. package/factory/create-model.js +72 -36
  5. package/factory/create-service.js +1 -1
  6. package/memory/in-memory.js +10 -6
  7. package/package.json +3 -3
  8. package/resource/index.d.ts +1 -0
  9. package/resource/index.js +1 -0
  10. package/resource/resource-schema.d.ts +0 -5
  11. package/resource/resource-schema.js +25 -10
  12. package/resource/resource-service.d.ts +214 -36
  13. package/resource/resource-service.js +247 -402
  14. package/resource/schemas/index.d.ts +3 -0
  15. package/resource/schemas/index.js +19 -0
  16. package/resource/schemas/resource-aggregate-schema.d.ts +54 -0
  17. package/resource/schemas/resource-aggregate-schema.js +131 -0
  18. package/resource/schemas/resource-filter-schema.d.ts +39 -0
  19. package/resource/schemas/resource-filter-schema.js +153 -0
  20. package/resource/schemas/resource-sort-schema.d.ts +37 -0
  21. package/resource/schemas/resource-sort-schema.js +114 -0
  22. package/resource/utils/aggregate-util.d.ts +113 -0
  23. package/resource/utils/aggregate-util.js +323 -0
  24. package/resource/utils/filter-util.d.ts +24 -0
  25. package/resource/utils/filter-util.js +283 -0
  26. package/resource/utils/index.d.ts +4 -0
  27. package/resource/utils/index.js +20 -0
  28. package/resource/utils/relation-util.d.ts +84 -0
  29. package/resource/utils/relation-util.js +344 -0
  30. package/resource/utils/sort-util.d.ts +20 -0
  31. package/resource/utils/sort-util.js +218 -0
  32. package/security/create-auth-resources.js +2 -2
  33. package/security/helper.js +1 -1
  34. package/security/resources/api-key/model.js +1 -0
  35. package/security/resources/api-key/service.d.ts +1 -1
  36. package/security/resources/role/model.js +1 -1
  37. package/server/create-server.js +6 -3
  38. package/server/register-route.js +1 -0
  39. package/server/swagger.js +87 -21
  40. package/server/virtual-projection.js +10 -8
  41. package/types/generated.d.ts +110 -4
  42. package/utils/schema-util.js +1 -1
  43. package/utils/virtual-util.js +1 -1
@@ -14,6 +14,9 @@ class LfuEvictionIndex {
14
14
  this._entries.set(key, meta);
15
15
  }
16
16
  evictionCandidates(count, now, gracePeriod) {
17
+ if (count <= 0) {
18
+ return [];
19
+ }
17
20
  // Build a min-heap of size `count` to find the k entries with the lowest
18
21
  // frequency score (usedCount / age), avoiding a full sort.
19
22
  // Time complexity: O(n log k) where k = count (typically 1).
@@ -1,4 +1,5 @@
1
1
  import { Readable } from 'node:stream';
2
+ import { QuerySort } from '@appweaver/common';
2
3
  export type ExportStream = {
3
4
  stream: Readable;
4
5
  fileName: string;
@@ -11,9 +12,10 @@ export declare class ExportService {
11
12
  *
12
13
  * @param {string} modelName - The resource name for which to export data.
13
14
  * @param {Object} [filter={}] - The filter conditions to apply when retrieving the data. Default is an empty object.
14
- * @param {string} [sort='-createdAt,id'] - The sorting criteria for the data. Default is `-createdAt, id`.
15
+ * @param {QuerySort} [sort='-createdAt,id'] - The sorting criteria for the data, given either as a comma-separated
16
+ * field list or as an object of field directions. Default is `-createdAt, id`.
15
17
  * @return {Promise<ExportStream>} A promise resolving to the export stream object, which includes the readable
16
18
  * stream, MIME type, and file name for the CSV.
17
19
  */
18
- exportCsv(modelName: string, filter?: any, sort?: string): Promise<ExportStream>;
20
+ exportCsv(modelName: string, filter?: any, sort?: QuerySort): Promise<ExportStream>;
19
21
  }
@@ -13,7 +13,8 @@ class ExportService {
13
13
  *
14
14
  * @param {string} modelName - The resource name for which to export data.
15
15
  * @param {Object} [filter={}] - The filter conditions to apply when retrieving the data. Default is an empty object.
16
- * @param {string} [sort='-createdAt,id'] - The sorting criteria for the data. Default is `-createdAt, id`.
16
+ * @param {QuerySort} [sort='-createdAt,id'] - The sorting criteria for the data, given either as a comma-separated
17
+ * field list or as an object of field directions. Default is `-createdAt, id`.
17
18
  * @return {Promise<ExportStream>} A promise resolving to the export stream object, which includes the readable
18
19
  * stream, MIME type, and file name for the CSV.
19
20
  */
@@ -100,29 +101,38 @@ class ExportService {
100
101
  ? `${parentKey}.${exportField.headerName}`
101
102
  : exportField.headerName;
102
103
  }
103
- if (exportField.mapValue) {
104
- const mappedValues = [];
105
- // Transform value items using mapValue configuration.
106
- const subItems = isArrayValue ? value : [value];
107
- for (const subItem of subItems) {
108
- if ((0, common_1.isFunction)(exportField.mapValue)) {
109
- try {
110
- mappedValues.push(exportField.mapValue(subItem));
104
+ const mapValue = exportField.mapValue;
105
+ if (mapValue) {
106
+ // A scalar value has no properties to read the configured field name
107
+ // off, so it is resolved against the exported record itself.
108
+ if ((0, common_1.isString)(mapValue) && !relationSchema && !fileSchema) {
109
+ value = item?.[mapValue];
110
+ }
111
+ else {
112
+ const mappedValues = [];
113
+ // Transform value items using mapValue configuration.
114
+ const subItems = isArrayValue ? value : [value];
115
+ for (const subItem of subItems) {
116
+ if ((0, common_1.isFunction)(mapValue)) {
117
+ try {
118
+ mappedValues.push(mapValue(subItem));
119
+ }
120
+ catch (e) {
121
+ mappedValues.push('');
122
+ common_1.logger.error(e, 'Export value mapping error.');
123
+ }
111
124
  }
112
- catch (e) {
113
- mappedValues.push('');
114
- common_1.logger.error(e, 'Export value mapping error.');
125
+ else if ((0, common_1.isString)(mapValue)) {
126
+ mappedValues.push(subItem?.[mapValue]);
115
127
  }
116
128
  }
117
- else if (!(0, common_1.isObject)(exportField.mapValue)) {
118
- mappedValues.push(subItem?.[exportField.mapValue]);
119
- }
129
+ value = mappedValues.join(',');
120
130
  }
121
- value = mappedValues.join(',');
122
131
  }
123
132
  }
124
133
  // Recursively map relation and file fields if the value is an object.
125
- if ((value && (0, common_1.isObject)(value)) || (isArrayValue && (0, common_1.isObject)(value[0]))) {
134
+ // Arrays of plain values are written as a single column below.
135
+ if ((0, common_1.isPlainObject)(value) || (isArrayValue && (0, common_1.isPlainObject)(value[0]))) {
126
136
  const relationName = (0, common_1.extractResourceName)(relationSchema ?? fileSchema);
127
137
  if (!relationName) {
128
138
  continue;
@@ -38,7 +38,7 @@ function createModel(config, override = false) {
38
38
  const createModel = omitOrPickScalars(resolveDefaultScalars(typebox_1.Type.Composite([scalarsSchema, virtualSchema]), config), config.create);
39
39
  const updateModel = omitOrPickScalars(resolveDefaultScalars(typebox_1.Type.Partial(typebox_1.Type.Composite([scalarsSchema, virtualSchema])), config), config.update);
40
40
  const { readOneModel, readManyModel } = buildOutputModels(baseReadModel, relationsModel, filesModel, config);
41
- const { createOneModel, updateOneModel } = buildInputModels(createModel, updateModel, relationsModel, config);
41
+ const { createOneModel, updateOneModel, relationCreateModel, relationUpdateModel, relationInputModel } = buildInputModels(createModel, updateModel, relationsModel, idSchema, config);
42
42
  const { fileUploadModel, fileDeleteModel } = buildFileInputModels(config);
43
43
  const resourceModel = {
44
44
  name,
@@ -53,11 +53,14 @@ function createModel(config, override = false) {
53
53
  readManyModel,
54
54
  createOneModel,
55
55
  updateOneModel,
56
+ relationCreateModel,
57
+ relationUpdateModel,
58
+ relationInputModel,
56
59
  fileUploadModel,
57
60
  fileDeleteModel
58
61
  };
59
62
  for (const value of Object.values(resourceModel)) {
60
- if ((0, common_1.isObject)(value)) {
63
+ if ((0, common_1.isPlainObject)(value)) {
61
64
  value[common_1.RESOURCE_NAME] = name;
62
65
  }
63
66
  }
@@ -185,7 +188,7 @@ function buildRelationSchema(relation) {
185
188
  const modelName = (0, common_1.capitalize)(relation.model);
186
189
  const modelRefName = `${modelName}Single`;
187
190
  let relationType = typebox_1.Type.Ref(modelRefName);
188
- relationType = relation.array
191
+ relationType = (0, common_1.isRelationArray)(relation)
189
192
  ? typebox_1.Type.Array(relationType, (0, common_1.pickProperties)(relation, ['minItems']))
190
193
  : relationType;
191
194
  return relation.required === false
@@ -209,15 +212,36 @@ function buildOutputModels(readModel, relationsModel, filesModel, config) {
209
212
  ], { $id: `${config.name}Multiple` });
210
213
  return { readOneModel, readManyModel };
211
214
  }
212
- function buildInputModels(createModel, updateModel, relationsModel, config) {
215
+ function buildInputModels(createModel, updateModel, relationsModel, idSchema, config) {
213
216
  const virtualConfig = config.virtual;
214
217
  const relationsConfig = config.relations;
215
218
  const adjustedCreateModel = resolveInputVirtualFields(removeHiddenFields(createModel), virtualConfig, 'create');
216
219
  const adjustedUpdateModel = resolveInputVirtualFields(removeHiddenFields(updateModel), virtualConfig, 'update');
220
+ // Data models accepted when this model is written inline through another
221
+ // model's relation input. Relations and files are excluded, so nested
222
+ // writes stay limited to the model's own columns, enforced by the service.
223
+ const relationCreateModel = typebox_1.Type.Composite([adjustedCreateModel], {
224
+ $id: `${config.name}RelationCreate`
225
+ });
226
+ const relationUpdateModel = typebox_1.Type.Composite([idSchema, adjustedUpdateModel], {
227
+ $id: `${config.name}RelationUpdate`
228
+ });
229
+ // Wire model for nested relation writes, holding the id together with the
230
+ // fields of both shapes above. It stays permissive because the server
231
+ // strips properties the matched schema does not declare, so a union of the
232
+ // narrower shapes would drop the fields of the ones it rejects.
233
+ const relationInputModel = typebox_1.Type.Object({
234
+ id: typebox_1.Type.Optional(idSchema.properties.id),
235
+ ...optionalProperties(adjustedCreateModel),
236
+ ...optionalProperties(adjustedUpdateModel)
237
+ }, { $id: `${config.name}RelationInput` });
217
238
  if (Object.keys(relationsConfig ?? {}).length === 0) {
218
239
  return {
219
240
  createOneModel: adjustedCreateModel,
220
- updateOneModel: adjustedUpdateModel
241
+ updateOneModel: adjustedUpdateModel,
242
+ relationCreateModel,
243
+ relationUpdateModel,
244
+ relationInputModel
221
245
  };
222
246
  }
223
247
  const relationCreateInputs = relationInputProperties(relationsModel, relationsConfig, 'create');
@@ -226,7 +250,20 @@ function buildInputModels(createModel, updateModel, relationsModel, config) {
226
250
  });
227
251
  const relationUpdateInputs = relationInputProperties(relationsModel, relationsConfig, 'update');
228
252
  const updateOneModel = typebox_1.Type.Composite([adjustedUpdateModel, typebox_1.Type.Partial(relationUpdateInputs)], { $id: `${config.name}Update` });
229
- return { createOneModel, updateOneModel };
253
+ return {
254
+ createOneModel,
255
+ updateOneModel,
256
+ relationCreateModel,
257
+ relationUpdateModel,
258
+ relationInputModel
259
+ };
260
+ }
261
+ function optionalProperties(schema) {
262
+ const properties = {};
263
+ for (const [name, field] of Object.entries(schema.properties)) {
264
+ properties[name] = typebox_1.Type.Optional(field);
265
+ }
266
+ return properties;
230
267
  }
231
268
  function buildFileInputModels(config) {
232
269
  const fileConfig = config.files ?? {};
@@ -249,38 +286,34 @@ function buildFileInputModels(config) {
249
286
  function relationInputProperties(object, relationConfig, inputType) {
250
287
  const relationInputType = (key) => {
251
288
  const { type, ...options } = object.properties[key];
252
- const uniqueIdObject = resource_1.Id;
253
- const uniqueIdType = resource_1.Id.properties.id;
254
- let fullInputType = undefined;
255
289
  const config = relationConfig?.[key];
256
- if (config) {
257
- if (shouldSkipInputField(config.input?.type, inputType)) {
258
- return undefined;
259
- }
260
- if (config.input?.fullModel) {
261
- fullInputType = typebox_1.Type.Ref(`${config.model}Create`);
262
- }
290
+ if (config && shouldSkipInputField(config.input?.type, inputType)) {
291
+ return undefined;
263
292
  }
264
- const isArray = type === 'array';
265
- const isOptional = config?.required === false;
266
- const idObjectSchema = isArray
267
- ? typebox_1.Type.Array(uniqueIdObject, options)
268
- : uniqueIdObject;
269
- const idTypeSchema = isArray
270
- ? typebox_1.Type.Array(uniqueIdType, options)
271
- : uniqueIdType;
272
- const inputTypeSchemas = [
273
- isOptional ? (0, common_1.Nullable)(idObjectSchema) : idObjectSchema,
274
- isOptional ? (0, common_1.Nullable)(idTypeSchema) : idTypeSchema
293
+ // Inline updates carry the related record id, so only parent update
294
+ // requests accept them
295
+ const acceptsInlineWrite = config?.input?.allowCreate ||
296
+ (config?.input?.allowUpdate && inputType === 'update');
297
+ // Existing records are connected by an id object or a bare id value.
298
+ // Relations accepting inline writes take the permissive input model
299
+ // instead, which also covers a lone id. Only one object schema may join
300
+ // the union, since the server strips undeclared properties.
301
+ const itemSchemas = [
302
+ acceptsInlineWrite ? typebox_1.Type.Ref(`${config.model}RelationInput`) : resource_1.Id,
303
+ resource_1.Id.properties.id
275
304
  ];
276
- if (fullInputType) {
277
- const fullInputTypeSchema = isArray
278
- ? typebox_1.Type.Array(fullInputType, options)
279
- : fullInputType;
280
- inputTypeSchemas.push(isOptional ? (0, common_1.Nullable)(fullInputTypeSchema) : fullInputTypeSchema);
305
+ const isArrayType = type === 'array';
306
+ const isOptional = config?.required === false;
307
+ let inputSchema;
308
+ if (isArrayType) {
309
+ // One array of union items, so connect, create, and update inputs mix
310
+ const arraySchema = typebox_1.Type.Array(typebox_1.Type.Union(itemSchemas), options);
311
+ inputSchema = isOptional ? (0, common_1.Nullable)(arraySchema) : arraySchema;
281
312
  }
282
- const unionSchema = typebox_1.Type.Union(inputTypeSchemas);
283
- return isOptional ? typebox_1.Type.Optional(unionSchema) : unionSchema;
313
+ else {
314
+ inputSchema = typebox_1.Type.Union(itemSchemas.map((itemSchema) => isOptional ? (0, common_1.Nullable)(itemSchema) : itemSchema));
315
+ }
316
+ return isOptional ? typebox_1.Type.Optional(inputSchema) : inputSchema;
284
317
  };
285
318
  return typebox_1.Type.Object({
286
319
  ...Object.keys(object.properties).reduce((acc, key) => {
@@ -313,15 +346,18 @@ function relationOutputProperties(object, relationConfig, outputType) {
313
346
  }
314
347
  return typebox_1.Type.Integer({ minimum: 0 });
315
348
  };
349
+ // Relation, file, and count fields are optional in the output models: their
350
+ // presence depends on the inclusion depth of the query. The same schema
351
+ // describes a record nested in another model's output, without relations.
316
352
  return typebox_1.Type.Object({
317
353
  ...Object.keys(object.properties).reduce((acc, key) => {
318
354
  const type = relationOutputType(key);
319
355
  if (type) {
320
- acc[key] = type;
356
+ acc[key] = typebox_1.Type.Optional(type);
321
357
  }
322
358
  const countType = relationCountType(key);
323
359
  if (countType) {
324
- acc[(0, common_1.countFieldName)(key)] = countType;
360
+ acc[(0, common_1.countFieldName)(key)] = typebox_1.Type.Optional(countType);
325
361
  }
326
362
  return acc;
327
363
  }, {})
@@ -58,7 +58,7 @@ function createService(config, override = false) {
58
58
  if ((0, common_1.isFunction)(config.textSearch)) {
59
59
  return config.textSearch(searchText);
60
60
  }
61
- if ((0, common_1.isObject)(config.textSearch)) {
61
+ if ((0, common_1.isPlainObject)(config.textSearch)) {
62
62
  return config.textSearch;
63
63
  }
64
64
  return super.textSearchQuery(searchText);
@@ -53,7 +53,11 @@ class InMemory extends common_1.Memory {
53
53
  // is below the configured value
54
54
  if (this._maxSizeBytes) {
55
55
  while (this._approximatedSize > this._maxSizeBytes) {
56
- await this.removeValue(this._storage.keys()[0]);
56
+ const oldestKey = this._storage.keys().next().value;
57
+ if (oldestKey === undefined) {
58
+ break;
59
+ }
60
+ await this.removeValue(oldestKey);
57
61
  }
58
62
  }
59
63
  return true;
@@ -62,12 +66,12 @@ class InMemory extends common_1.Memory {
62
66
  return this._storage.has(key);
63
67
  }
64
68
  async removeValue(key) {
65
- const jsonValue = await this.getValue(key);
66
- if (!jsonValue) {
69
+ const entry = this._storage.get(key);
70
+ if (!entry) {
67
71
  return false;
68
72
  }
69
73
  const deleted = this._storage.delete(key);
70
- this._approximatedSize -= Buffer.byteLength(jsonValue, 'utf8');
74
+ this._approximatedSize -= Buffer.byteLength(entry.value, 'utf8');
71
75
  if (this._approximatedSize < 0) {
72
76
  this._approximatedSize = 0;
73
77
  }
@@ -146,14 +150,14 @@ class InMemory extends common_1.Memory {
146
150
  async cleanupExpired() {
147
151
  const now = Date.now();
148
152
  // Clean up expired storage entries
149
- for (const key in this._storage.keys()) {
153
+ for (const key of Array.from(this._storage.keys())) {
150
154
  const entry = this._storage.get(key);
151
155
  if (entry && entry.expiresAt && entry.expiresAt < now) {
152
156
  await this.removeValue(key);
153
157
  }
154
158
  }
155
159
  // Clean up expired locks
156
- for (const key in this._locks.keys()) {
160
+ for (const key of Array.from(this._locks.keys())) {
157
161
  const lock = this._locks.get(key);
158
162
  if (lock && lock.expiresAt < now) {
159
163
  this._locks.delete(key);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appweaver/core",
3
- "version": "1.1.5",
3
+ "version": "1.2.0",
4
4
  "description": "Appweaver - the backend framework for AI-first development (@core)",
5
5
  "author": "Luka Matosevic",
6
6
  "license": "MIT",
@@ -38,9 +38,9 @@
38
38
  "@fastify/oauth2": "8.2.0",
39
39
  "@fastify/rate-limit": "11.0.0",
40
40
  "@fastify/request-context": "7.0.0",
41
- "@fastify/static": "9.1.3",
41
+ "@fastify/static": "10.1.2",
42
42
  "@fastify/swagger": "9.7.0",
43
- "@fastify/swagger-ui": "6.0.0",
43
+ "@fastify/swagger-ui": "6.1.1",
44
44
  "@fastify/type-provider-typebox": "6.1.0",
45
45
  "@sinclair/typebox": "0.34.49",
46
46
  "bcrypt": "6.0.0",
@@ -2,3 +2,4 @@ export * from './resource-loader';
2
2
  export * from './resource-routes';
3
3
  export * from './resource-schema';
4
4
  export * from './resource-service';
5
+ export * from './schemas';
package/resource/index.js CHANGED
@@ -18,3 +18,4 @@ __exportStar(require("./resource-loader"), exports);
18
18
  __exportStar(require("./resource-routes"), exports);
19
19
  __exportStar(require("./resource-schema"), exports);
20
20
  __exportStar(require("./resource-service"), exports);
21
+ __exportStar(require("./schemas"), exports);
@@ -11,19 +11,14 @@ export declare const AuditData: import("@sinclair/typebox").TObject<{
11
11
  createdById: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnsafe<number | null>>;
12
12
  }>;
13
13
  export declare const QueryRequestData: import("@sinclair/typebox").TObject<{
14
- filter: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TAny>;
15
14
  page: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
16
15
  size: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
17
- sort: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
18
16
  }>;
19
17
  export declare const QueryResponseData: import("@sinclair/typebox").TObject<{
20
18
  resultCount: import("@sinclair/typebox").TNumber;
21
19
  totalCount: import("@sinclair/typebox").TNumber;
22
20
  }>;
23
21
  export declare const AggregateRequestData: import("@sinclair/typebox").TObject<{
24
- filter: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TAny>;
25
- select: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TAny>;
26
- dateField: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
27
22
  from: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnsafe<Date>>;
28
23
  to: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnsafe<Date>>;
29
24
  step: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TInteger>;
@@ -8,6 +8,7 @@ const context_1 = require("../context");
8
8
  const security_1 = require("../security");
9
9
  const errors_1 = require("../errors");
10
10
  const utils_1 = require("../utils");
11
+ const schemas_1 = require("./schemas");
11
12
  exports.Id = typebox_1.Type.Object({
12
13
  id: typebox_1.Type.Integer({ minimum: 1 })
13
14
  });
@@ -19,20 +20,19 @@ exports.AuditData = typebox_1.Type.Object({
19
20
  createdAt: (0, common_1.StringDate)(),
20
21
  createdById: (0, common_1.Nullable)(typebox_1.Type.Integer({ minimum: 1, example: 1 }))
21
22
  });
23
+ // The sort property is declared per model instead, since its object form
24
+ // references the sortable fields of the queried resource
22
25
  exports.QueryRequestData = typebox_1.Type.Object({
23
- filter: typebox_1.Type.Optional((0, common_1.AnyJson)({ example: { field: 'value' } })),
24
26
  page: typebox_1.Type.Optional(typebox_1.Type.Number({ minimum: 1, example: 1 })),
25
- size: typebox_1.Type.Optional(typebox_1.Type.Number({ minimum: 0, maximum: 1000, example: 50 })),
26
- sort: typebox_1.Type.Optional(typebox_1.Type.String({ example: '-createdAt,id' }))
27
+ size: typebox_1.Type.Optional(typebox_1.Type.Number({ minimum: 0, maximum: 1000, example: 50 }))
27
28
  });
28
29
  exports.QueryResponseData = typebox_1.Type.Object({
29
30
  resultCount: typebox_1.Type.Number({ example: 10 }),
30
31
  totalCount: typebox_1.Type.Number({ example: 100 })
31
32
  });
33
+ // The select and dateField properties are declared per model instead, since
34
+ // they reference the aggregatable fields of the aggregated resource
32
35
  exports.AggregateRequestData = typebox_1.Type.Object({
33
- filter: typebox_1.Type.Optional((0, common_1.AnyJson)({ example: { field: 'value' } })),
34
- select: typebox_1.Type.Optional((0, common_1.AnyJson)({ example: { field: 'value' } })),
35
- dateField: typebox_1.Type.Optional(typebox_1.Type.String({ example: 'createdAt' })),
36
36
  from: typebox_1.Type.Optional((0, common_1.StringDate)()),
37
37
  to: typebox_1.Type.Optional((0, common_1.StringDate)()),
38
38
  step: typebox_1.Type.Optional(typebox_1.Type.Integer({ minimum: 1, example: 3600 })),
@@ -43,7 +43,18 @@ function createSchema(name, routeAuthTypes, routeRecaptcha) {
43
43
  const resourceModel = (0, context_1.injectModel)(name);
44
44
  const resourceName = (0, common_1.camelToSnakeCase)(name, ' ');
45
45
  const tag = (0, common_1.plural)(name);
46
- const queryRequest = typebox_1.Type.Composite([exports.QueryRequestData], {
46
+ // Register the recursive query filter and sort schemas of all loaded models,
47
+ // referenced by the query, aggregate, and export request bodies
48
+ (0, schemas_1.registerQueryFilterSchemas)();
49
+ (0, schemas_1.registerQuerySortSchemas)();
50
+ (0, schemas_1.registerAggregateSelectSchemas)();
51
+ const filterData = typebox_1.Type.Object({
52
+ filter: typebox_1.Type.Optional(typebox_1.Type.Ref((0, schemas_1.queryFilterName)(name)))
53
+ });
54
+ const sortData = typebox_1.Type.Object({
55
+ sort: (0, schemas_1.querySortSchema)(name)
56
+ });
57
+ const queryRequest = typebox_1.Type.Composite([filterData, sortData, exports.QueryRequestData], {
47
58
  $id: `${name}QueryRequest`
48
59
  });
49
60
  const queryResponse = typebox_1.Type.Composite([
@@ -52,13 +63,17 @@ function createSchema(name, routeAuthTypes, routeRecaptcha) {
52
63
  items: typebox_1.Type.Array(resourceModel.readManyModel)
53
64
  })
54
65
  ], { $id: `${name}QueryResponse` });
55
- const aggregateRequest = typebox_1.Type.Composite([exports.AggregateRequestData], {
56
- $id: `${name}AggregateRequest`
66
+ const selectData = typebox_1.Type.Object({
67
+ select: typebox_1.Type.Ref((0, schemas_1.aggregateSelectName)(name)),
68
+ dateField: (0, schemas_1.aggregateDateFieldSchema)(resourceModel)
57
69
  });
70
+ const aggregateRequest = typebox_1.Type.Composite([filterData, selectData, exports.AggregateRequestData], { $id: `${name}AggregateRequest` });
58
71
  const aggregateResponse = typebox_1.Type.Composite([exports.AggregateResponseData], {
59
72
  $id: `${name}AggregateResponse`
60
73
  });
61
- const exportRequest = typebox_1.Type.Composite([typebox_1.Type.Pick(exports.QueryRequestData, ['filter', 'sort'])], { $id: `${name}ExportRequest` });
74
+ const exportRequest = typebox_1.Type.Composite([filterData, sortData], {
75
+ $id: `${name}ExportRequest`
76
+ });
62
77
  const resourceSchemaConfig = {
63
78
  findSchema: {
64
79
  tags: [tag],