@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.
- package/cache/eviction/lfu-eviction-index.js +3 -0
- package/export/export-service.d.ts +4 -2
- package/export/export-service.js +27 -17
- package/factory/create-model.js +72 -36
- package/factory/create-service.js +1 -1
- package/memory/in-memory.js +10 -6
- package/package.json +3 -3
- package/resource/index.d.ts +1 -0
- package/resource/index.js +1 -0
- package/resource/resource-schema.d.ts +0 -5
- package/resource/resource-schema.js +25 -10
- package/resource/resource-service.d.ts +214 -36
- package/resource/resource-service.js +247 -402
- package/resource/schemas/index.d.ts +3 -0
- package/resource/schemas/index.js +19 -0
- package/resource/schemas/resource-aggregate-schema.d.ts +54 -0
- package/resource/schemas/resource-aggregate-schema.js +131 -0
- package/resource/schemas/resource-filter-schema.d.ts +39 -0
- package/resource/schemas/resource-filter-schema.js +153 -0
- package/resource/schemas/resource-sort-schema.d.ts +37 -0
- package/resource/schemas/resource-sort-schema.js +114 -0
- package/resource/utils/aggregate-util.d.ts +113 -0
- package/resource/utils/aggregate-util.js +323 -0
- package/resource/utils/filter-util.d.ts +24 -0
- package/resource/utils/filter-util.js +283 -0
- package/resource/utils/index.d.ts +4 -0
- package/resource/utils/index.js +20 -0
- package/resource/utils/relation-util.d.ts +84 -0
- package/resource/utils/relation-util.js +344 -0
- package/resource/utils/sort-util.d.ts +20 -0
- package/resource/utils/sort-util.js +218 -0
- package/security/create-auth-resources.js +2 -2
- package/security/helper.js +1 -1
- package/security/resources/api-key/model.js +1 -0
- package/security/resources/api-key/service.d.ts +1 -1
- package/security/resources/role/model.js +1 -1
- package/server/create-server.js +6 -3
- package/server/register-route.js +1 -0
- package/server/swagger.js +87 -21
- package/server/virtual-projection.js +10 -8
- package/types/generated.d.ts +110 -4
- package/utils/schema-util.js +1 -1
- 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 {
|
|
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?:
|
|
20
|
+
exportCsv(modelName: string, filter?: any, sort?: QuerySort): Promise<ExportStream>;
|
|
19
21
|
}
|
package/export/export-service.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
//
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
package/factory/create-model.js
CHANGED
|
@@ -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.
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
265
|
-
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
-
|
|
283
|
-
|
|
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.
|
|
61
|
+
if ((0, common_1.isPlainObject)(config.textSearch)) {
|
|
62
62
|
return config.textSearch;
|
|
63
63
|
}
|
|
64
64
|
return super.textSearchQuery(searchText);
|
package/memory/in-memory.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
66
|
-
if (!
|
|
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(
|
|
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
|
|
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
|
|
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.
|
|
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": "
|
|
41
|
+
"@fastify/static": "10.1.2",
|
|
42
42
|
"@fastify/swagger": "9.7.0",
|
|
43
|
-
"@fastify/swagger-ui": "6.
|
|
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",
|
package/resource/index.d.ts
CHANGED
package/resource/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
56
|
-
|
|
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([
|
|
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],
|