@appweaver/core 1.1.6 → 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 (42) 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/package.json +1 -1
  7. package/resource/index.d.ts +1 -0
  8. package/resource/index.js +1 -0
  9. package/resource/resource-schema.d.ts +0 -5
  10. package/resource/resource-schema.js +25 -10
  11. package/resource/resource-service.d.ts +214 -36
  12. package/resource/resource-service.js +247 -404
  13. package/resource/schemas/index.d.ts +3 -0
  14. package/resource/schemas/index.js +19 -0
  15. package/resource/schemas/resource-aggregate-schema.d.ts +54 -0
  16. package/resource/schemas/resource-aggregate-schema.js +131 -0
  17. package/resource/schemas/resource-filter-schema.d.ts +39 -0
  18. package/resource/schemas/resource-filter-schema.js +153 -0
  19. package/resource/schemas/resource-sort-schema.d.ts +37 -0
  20. package/resource/schemas/resource-sort-schema.js +114 -0
  21. package/resource/utils/aggregate-util.d.ts +113 -0
  22. package/resource/utils/aggregate-util.js +323 -0
  23. package/resource/utils/filter-util.d.ts +24 -0
  24. package/resource/utils/filter-util.js +283 -0
  25. package/resource/utils/index.d.ts +4 -0
  26. package/resource/utils/index.js +20 -0
  27. package/resource/utils/relation-util.d.ts +84 -0
  28. package/resource/utils/relation-util.js +344 -0
  29. package/resource/utils/sort-util.d.ts +20 -0
  30. package/resource/utils/sort-util.js +218 -0
  31. package/security/create-auth-resources.js +2 -2
  32. package/security/helper.js +1 -1
  33. package/security/resources/api-key/model.js +1 -0
  34. package/security/resources/api-key/service.d.ts +1 -1
  35. package/security/resources/role/model.js +1 -1
  36. package/server/create-server.js +4 -1
  37. package/server/register-route.js +1 -0
  38. package/server/swagger.js +87 -21
  39. package/server/virtual-projection.js +10 -8
  40. package/types/generated.d.ts +110 -4
  41. package/utils/schema-util.js +1 -1
  42. package/utils/virtual-util.js +1 -1
@@ -0,0 +1,3 @@
1
+ export * from './resource-aggregate-schema';
2
+ export * from './resource-filter-schema';
3
+ export * from './resource-sort-schema';
@@ -0,0 +1,19 @@
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 __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./resource-aggregate-schema"), exports);
18
+ __exportStar(require("./resource-filter-schema"), exports);
19
+ __exportStar(require("./resource-sort-schema"), exports);
@@ -0,0 +1,54 @@
1
+ import { TObject, TSchema } from '@sinclair/typebox';
2
+ import { ResourceModel } from '@appweaver/common';
3
+ /** The schema name the numeric field operator object is registered under. */
4
+ export declare const AGGREGATE_NUMERIC_SCHEMA_NAME = "AggregateNumericOperators";
5
+ /** The schema name the date field operator object is registered under. */
6
+ export declare const AGGREGATE_DATE_SCHEMA_NAME = "AggregateDateOperators";
7
+ /**
8
+ * Resolves the schema name the aggregate selection of a model is registered
9
+ * under.
10
+ *
11
+ * @param {string} modelName The resource model name (i.e. `User`, `Post`).
12
+ * @returns {string} The aggregate selection schema name (i.e. `UserAggregateSelect`).
13
+ */
14
+ export declare const aggregateSelectName: (modelName: string) => string;
15
+ /**
16
+ * The operators applicable to a numeric field. Every operator selected on the
17
+ * same field is applied to it, and the field is left out of the response of the
18
+ * operators that were not selected.
19
+ */
20
+ export declare const AggregateNumericSchema: TObject;
21
+ /**
22
+ * The operators applicable to a date field. Dates cannot be summed or averaged,
23
+ * so only the counting and the ordering operators are offered.
24
+ */
25
+ export declare const AggregateDateSchema: TObject;
26
+ /**
27
+ * Builds the aggregate selection schema of a resource model. Only the fields
28
+ * the database can aggregate are declared, which are the numeric and the date
29
+ * scalars of the model together with its numeric id and audit fields, so the
30
+ * request validation keeps the known fields and strips everything else
31
+ * (including hidden fields, array scalars, and the virtual fields that have no
32
+ * column of their own).
33
+ *
34
+ * @param {ResourceModel} model The resource model to build the selection schema
35
+ * for.
36
+ * @returns {TObject} The aggregate selection schema of the model.
37
+ */
38
+ export declare function buildAggregateSelectSchema(model: ResourceModel): TObject;
39
+ /**
40
+ * Builds the schema of the date field property of a model, listing the date
41
+ * fields the aggregated range can be applied on.
42
+ *
43
+ * @param {ResourceModel} model The resource model the property belongs to.
44
+ * @returns {TSchema} The optional date field property schema, restricted to the
45
+ * date fields of the model when it declares any.
46
+ */
47
+ export declare function aggregateDateFieldSchema(model: ResourceModel): TSchema;
48
+ /**
49
+ * Registers the shared operator schemas and the aggregate selection schema of
50
+ * every loaded resource model on the server, so route schemas can reference them
51
+ * by name (i.e. `UserAggregateSelect`). Schemas already registered under the
52
+ * same name are skipped, which makes repeated calls cheap.
53
+ */
54
+ export declare function registerAggregateSelectSchemas(): void;
@@ -0,0 +1,131 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AggregateDateSchema = exports.AggregateNumericSchema = exports.aggregateSelectName = exports.AGGREGATE_DATE_SCHEMA_NAME = exports.AGGREGATE_NUMERIC_SCHEMA_NAME = void 0;
4
+ exports.buildAggregateSelectSchema = buildAggregateSelectSchema;
5
+ exports.aggregateDateFieldSchema = aggregateDateFieldSchema;
6
+ exports.registerAggregateSelectSchemas = registerAggregateSelectSchemas;
7
+ const typebox_1 = require("@sinclair/typebox");
8
+ const context_1 = require("../../context");
9
+ const utils_1 = require("../../utils");
10
+ const utils_2 = require("../utils");
11
+ /** The schema name the numeric field operator object is registered under. */
12
+ exports.AGGREGATE_NUMERIC_SCHEMA_NAME = 'AggregateNumericOperators';
13
+ /** The schema name the date field operator object is registered under. */
14
+ exports.AGGREGATE_DATE_SCHEMA_NAME = 'AggregateDateOperators';
15
+ /**
16
+ * Resolves the schema name the aggregate selection of a model is registered
17
+ * under.
18
+ *
19
+ * @param {string} modelName The resource model name (i.e. `User`, `Post`).
20
+ * @returns {string} The aggregate selection schema name (i.e. `UserAggregateSelect`).
21
+ */
22
+ const aggregateSelectName = (modelName) => `${modelName}AggregateSelect`;
23
+ exports.aggregateSelectName = aggregateSelectName;
24
+ const Operator = (description) => typebox_1.Type.Optional(typebox_1.Type.Boolean({ description, example: true }));
25
+ // The boundary operators read the record itself instead of aggregating the
26
+ // range, so their value is the one the record holds, null included
27
+ const FIRST_DESCRIPTION = 'Takes the value held by the earliest record of the range, ordered by the ' +
28
+ 'aggregated date field';
29
+ const LAST_DESCRIPTION = 'Takes the value held by the latest record of the range, ordered by the ' +
30
+ 'aggregated date field';
31
+ /**
32
+ * The operators applicable to a numeric field. Every operator selected on the
33
+ * same field is applied to it, and the field is left out of the response of the
34
+ * operators that were not selected.
35
+ */
36
+ exports.AggregateNumericSchema = typebox_1.Type.Object({
37
+ count: Operator('Counts the records with a non-null value'),
38
+ sum: Operator('Sums the values'),
39
+ avg: Operator('Averages the values'),
40
+ min: Operator('Takes the lowest value'),
41
+ max: Operator('Takes the highest value'),
42
+ first: Operator(FIRST_DESCRIPTION),
43
+ last: Operator(LAST_DESCRIPTION)
44
+ }, {
45
+ description: 'Aggregation operators applicable to a numeric field'
46
+ });
47
+ /**
48
+ * The operators applicable to a date field. Dates cannot be summed or averaged,
49
+ * so only the counting and the ordering operators are offered.
50
+ */
51
+ exports.AggregateDateSchema = typebox_1.Type.Object({
52
+ count: Operator('Counts the records with a non-null value'),
53
+ min: Operator('Takes the earliest date'),
54
+ max: Operator('Takes the latest date'),
55
+ first: Operator(FIRST_DESCRIPTION),
56
+ last: Operator(LAST_DESCRIPTION)
57
+ }, {
58
+ description: 'Aggregation operators applicable to a date field'
59
+ });
60
+ /**
61
+ * Builds the aggregate selection schema of a resource model. Only the fields
62
+ * the database can aggregate are declared, which are the numeric and the date
63
+ * scalars of the model together with its numeric id and audit fields, so the
64
+ * request validation keeps the known fields and strips everything else
65
+ * (including hidden fields, array scalars, and the virtual fields that have no
66
+ * column of their own).
67
+ *
68
+ * @param {ResourceModel} model The resource model to build the selection schema
69
+ * for.
70
+ * @returns {TObject} The aggregate selection schema of the model.
71
+ */
72
+ function buildAggregateSelectSchema(model) {
73
+ const properties = {};
74
+ for (const [fieldName, type] of (0, utils_2.aggregateFields)(model)) {
75
+ properties[fieldName] = typebox_1.Type.Optional(typebox_1.Type.Ref(type === 'numeric'
76
+ ? exports.AGGREGATE_NUMERIC_SCHEMA_NAME
77
+ : exports.AGGREGATE_DATE_SCHEMA_NAME));
78
+ }
79
+ return typebox_1.Type.Object(properties, {
80
+ description: `Aggregate selection for the ${model.name} resource, holding the ` +
81
+ 'operators to apply per aggregated field'
82
+ });
83
+ }
84
+ /**
85
+ * Builds the schema of the date field property of a model, listing the date
86
+ * fields the aggregated range can be applied on.
87
+ *
88
+ * @param {ResourceModel} model The resource model the property belongs to.
89
+ * @returns {TSchema} The optional date field property schema, restricted to the
90
+ * date fields of the model when it declares any.
91
+ */
92
+ function aggregateDateFieldSchema(model) {
93
+ const dateFields = (0, utils_2.aggregateFields)(model)
94
+ .filter(([, type]) => type === 'date')
95
+ .map(([fieldName]) => fieldName);
96
+ const description = 'The date field the aggregated range is applied on';
97
+ // A model without a single date field has no range to aggregate over, so the
98
+ // property is left unrestricted instead of declaring an empty enum that
99
+ // nothing can satisfy
100
+ return typebox_1.Type.Optional(dateFields.length > 0
101
+ ? typebox_1.Type.Unsafe({
102
+ type: 'string',
103
+ enum: dateFields,
104
+ // The default date field of the aggregate action, when the model
105
+ // audits it, so the example matches what an omitted value resolves to
106
+ example: dateFields.includes('createdAt')
107
+ ? 'createdAt'
108
+ : dateFields[0],
109
+ description
110
+ })
111
+ : typebox_1.Type.String({ example: 'createdAt', description }));
112
+ }
113
+ /**
114
+ * Registers the shared operator schemas and the aggregate selection schema of
115
+ * every loaded resource model on the server, so route schemas can reference them
116
+ * by name (i.e. `UserAggregateSelect`). Schemas already registered under the
117
+ * same name are skipped, which makes repeated calls cheap.
118
+ */
119
+ function registerAggregateSelectSchemas() {
120
+ (0, utils_1.createSchemaModel)(exports.AggregateNumericSchema, {
121
+ name: exports.AGGREGATE_NUMERIC_SCHEMA_NAME
122
+ });
123
+ (0, utils_1.createSchemaModel)(exports.AggregateDateSchema, {
124
+ name: exports.AGGREGATE_DATE_SCHEMA_NAME
125
+ });
126
+ for (const model of context_1.context.resource.models.values()) {
127
+ (0, utils_1.createSchemaModel)(buildAggregateSelectSchema(model), {
128
+ name: (0, exports.aggregateSelectName)(model.name)
129
+ });
130
+ }
131
+ }
@@ -0,0 +1,39 @@
1
+ import { TObject } from '@sinclair/typebox';
2
+ import { ResourceModel } from '@appweaver/common';
3
+ /** The schema name the shared field condition object is registered under. */
4
+ export declare const QUERY_CONDITION_SCHEMA_NAME = "QueryCondition";
5
+ /**
6
+ * Resolves the schema name the query filter of a model is registered under.
7
+ *
8
+ * @param {string} modelName The resource model name (i.e. `User`, `Post`).
9
+ * @returns {string} The query filter schema name (i.e. `UserQueryFilter`).
10
+ */
11
+ export declare const queryFilterName: (modelName: string) => string;
12
+ /**
13
+ * The shared schema of a field condition object, holding the comparison
14
+ * operators applicable to a single field. Operators combined inside the same
15
+ * object are merged into a single database condition, and unknown properties
16
+ * are stripped by the request validation.
17
+ */
18
+ export declare const QueryConditionSchema: TObject;
19
+ /**
20
+ * Builds the query filter schema of a resource model, mirroring the WHERE part
21
+ * of a database query. Every filterable field of the model is declared
22
+ * explicitly, so the request validation keeps the known fields and strips
23
+ * everything else (including hidden fields). Scalar fields accept a plain
24
+ * value, a list of values, or a condition object, relation and file fields
25
+ * additionally accept an id shorthand or the filter of the related model, and
26
+ * the `_`-prefixed logical operators combine nested conditions recursively.
27
+ *
28
+ * @param {ResourceModel} model The resource model to build the filter schema
29
+ * for.
30
+ * @returns {TObject} The query filter schema of the model.
31
+ */
32
+ export declare function buildQueryFilterSchema(model: ResourceModel): TObject;
33
+ /**
34
+ * Registers the shared field condition schema and the query filter schema of
35
+ * every loaded resource model on the server, so route schemas can reference
36
+ * them by name (i.e. `UserQueryFilter`). Schemas already registered under the
37
+ * same name are skipped, which makes repeated calls cheap.
38
+ */
39
+ export declare function registerQueryFilterSchemas(): void;
@@ -0,0 +1,153 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.QueryConditionSchema = exports.queryFilterName = exports.QUERY_CONDITION_SCHEMA_NAME = void 0;
4
+ exports.buildQueryFilterSchema = buildQueryFilterSchema;
5
+ exports.registerQueryFilterSchemas = registerQueryFilterSchemas;
6
+ const typebox_1 = require("@sinclair/typebox");
7
+ const common_1 = require("@appweaver/common");
8
+ const context_1 = require("../../context");
9
+ const utils_1 = require("../../utils");
10
+ /** The schema name the shared field condition object is registered under. */
11
+ exports.QUERY_CONDITION_SCHEMA_NAME = 'QueryCondition';
12
+ /**
13
+ * Resolves the schema name the query filter of a model is registered under.
14
+ *
15
+ * @param {string} modelName The resource model name (i.e. `User`, `Post`).
16
+ * @returns {string} The query filter schema name (i.e. `UserQueryFilter`).
17
+ */
18
+ const queryFilterName = (modelName) => `${modelName}QueryFilter`;
19
+ exports.queryFilterName = queryFilterName;
20
+ // The primitive types a plain filter value can take. They are declared as a
21
+ // single JSON Schema type list rather than a union of typed branches, because
22
+ // the request validator coerces a value into the first branch of a typed
23
+ // union that accepts it (turning 10 into '10'), while a type list leaves a
24
+ // value that already matches one of its types untouched. The exact type of
25
+ // each field is enforced by the database layer instead.
26
+ const PRIMITIVE_TYPES = ['string', 'number', 'boolean', 'null'];
27
+ const PlainValue = () => typebox_1.Type.Unsafe({ type: PRIMITIVE_TYPES });
28
+ const PlainValueList = (options = {}) => typebox_1.Type.Unsafe({
29
+ type: 'array',
30
+ items: { type: PRIMITIVE_TYPES },
31
+ ...options
32
+ });
33
+ const Operator = (schema, description) => typebox_1.Type.Optional({ ...schema, description });
34
+ /**
35
+ * The shared schema of a field condition object, holding the comparison
36
+ * operators applicable to a single field. Operators combined inside the same
37
+ * object are merged into a single database condition, and unknown properties
38
+ * are stripped by the request validation.
39
+ */
40
+ exports.QueryConditionSchema = typebox_1.Type.Object({
41
+ _eq: Operator(PlainValue(), 'Matches values equal to the given value'),
42
+ _ne: Operator(PlainValue(), 'Matches values not equal to the given value'),
43
+ _gt: Operator(PlainValue(), 'Matches values greater than the given value'),
44
+ _gte: Operator(PlainValue(), 'Matches values greater than or equal to the given value'),
45
+ _lt: Operator(PlainValue(), 'Matches values lower than the given value'),
46
+ _lte: Operator(PlainValue(), 'Matches values lower than or equal to the given value'),
47
+ _in: Operator(PlainValueList(), 'Matches values included in the given list'),
48
+ _nin: Operator(PlainValueList(), 'Matches values not included in the given list'),
49
+ _between: Operator(PlainValueList({ minItems: 2, maxItems: 2 }), 'Matches values inside the inclusive [min, max] range'),
50
+ _like: Operator(typebox_1.Type.String(), 'Matches strings against an SQL LIKE pattern with % wildcards'),
51
+ _ilike: Operator(typebox_1.Type.String(), 'Case-insensitive variant of the _like operator'),
52
+ _starts: Operator(typebox_1.Type.String(), 'Matches strings starting with the given value'),
53
+ _ends: Operator(typebox_1.Type.String(), 'Matches strings ending with the given value'),
54
+ _contains: Operator(typebox_1.Type.String(), 'Matches strings containing the given value'),
55
+ _exists: Operator(typebox_1.Type.Boolean(), 'Matches values that are not null (true) or null (false)'),
56
+ _has: Operator(PlainValue(), 'Matches lists containing the given value'),
57
+ _hasSome: Operator(PlainValueList(), 'Matches lists containing at least one of the given values'),
58
+ _hasEvery: Operator(PlainValueList(), 'Matches lists containing all the given values'),
59
+ _isEmpty: Operator(typebox_1.Type.Boolean(), 'Matches empty (true) or non-empty (false) lists'),
60
+ _not: Operator(typebox_1.Type.Union([PlainValue(), typebox_1.Type.Ref(exports.QUERY_CONDITION_SCHEMA_NAME)]), 'Negates the nested condition or plain value')
61
+ }, {
62
+ description: 'Field condition object combining comparison operators on a single field'
63
+ });
64
+ /**
65
+ * Builds the query filter schema of a resource model, mirroring the WHERE part
66
+ * of a database query. Every filterable field of the model is declared
67
+ * explicitly, so the request validation keeps the known fields and strips
68
+ * everything else (including hidden fields). Scalar fields accept a plain
69
+ * value, a list of values, or a condition object, relation and file fields
70
+ * additionally accept an id shorthand or the filter of the related model, and
71
+ * the `_`-prefixed logical operators combine nested conditions recursively.
72
+ *
73
+ * @param {ResourceModel} model The resource model to build the filter schema
74
+ * for.
75
+ * @returns {TObject} The query filter schema of the model.
76
+ */
77
+ function buildQueryFilterSchema(model) {
78
+ const name = (0, exports.queryFilterName)(model.name);
79
+ const self = () => typebox_1.Type.Ref(name);
80
+ const logical = (description) => typebox_1.Type.Optional(typebox_1.Type.Union([self(), typebox_1.Type.Array(self())], { description }));
81
+ const scalarValue = (description) => typebox_1.Type.Optional(typebox_1.Type.Union([PlainValue(), PlainValueList(), typebox_1.Type.Ref(exports.QUERY_CONDITION_SCHEMA_NAME)], { description }));
82
+ // A list of related filters is declared as its own branch, so the validator
83
+ // still strips the unknown properties of the filters nested inside it
84
+ const relationValue = (refName, description) => typebox_1.Type.Optional(typebox_1.Type.Union([
85
+ PlainValue(),
86
+ PlainValueList(),
87
+ typebox_1.Type.Ref(refName),
88
+ typebox_1.Type.Array(typebox_1.Type.Ref(refName))
89
+ ], { description }));
90
+ const properties = {
91
+ id: scalarValue('Filter by the record id')
92
+ };
93
+ const auditFields = {
94
+ updatedAt: true,
95
+ createdAt: true,
96
+ createdById: true,
97
+ ...(model.config.audit ?? {})
98
+ };
99
+ for (const [fieldName, included] of Object.entries(auditFields)) {
100
+ if (included) {
101
+ properties[fieldName] = scalarValue(`Filter by the ${fieldName} field`);
102
+ }
103
+ }
104
+ for (const [fieldName, scalar] of Object.entries(model.config.scalars ?? {})) {
105
+ if (scalar.hidden) {
106
+ continue;
107
+ }
108
+ // JSON fields hold arbitrary nested structures, so their conditions are
109
+ // passed through without validation
110
+ properties[fieldName] =
111
+ scalar.type === 'json'
112
+ ? typebox_1.Type.Optional(typebox_1.Type.Any({ description: `Filter by the ${fieldName} field` }))
113
+ : scalarValue(`Filter by the ${fieldName} field`);
114
+ }
115
+ for (const [fieldName, relation] of Object.entries(model.config.relations ?? {})) {
116
+ properties[fieldName] = relationValue((0, exports.queryFilterName)((0, common_1.capitalize)(relation.model)), `Filter by the ${fieldName} relation, matching an id, a list of ids, ` +
117
+ `or a nested ${(0, common_1.capitalize)(relation.model)} filter`);
118
+ }
119
+ for (const fieldName of Object.keys(model.config.files ?? {})) {
120
+ properties[fieldName] = relationValue((0, exports.queryFilterName)('File'), `Filter by the ${fieldName} file field, matching an id, a list of ids, ` +
121
+ 'or a nested File filter');
122
+ }
123
+ return typebox_1.Type.Object({
124
+ _and: logical('Matches records satisfying all the nested conditions'),
125
+ _or: logical('Matches records satisfying at least one nested condition'),
126
+ _not: logical('Matches records satisfying none of the nested conditions'),
127
+ _nor: logical('Matches records satisfying none of the nested conditions (alias of _not)'),
128
+ _some: Operator(self(), 'Matches records where at least one related record matches the filter'),
129
+ _every: Operator(self(), 'Matches records where every related record matches the filter'),
130
+ _none: Operator(self(), 'Matches records where no related record matches the filter'),
131
+ _exists: Operator(typebox_1.Type.Boolean(), 'Matches records with a related record (true) or without one (false)'),
132
+ searchText: Operator(typebox_1.Type.String(), 'Full-text search input resolved by the resource service'),
133
+ ...properties
134
+ }, {
135
+ description: `Query filter for the ${model.name} resource`
136
+ });
137
+ }
138
+ /**
139
+ * Registers the shared field condition schema and the query filter schema of
140
+ * every loaded resource model on the server, so route schemas can reference
141
+ * them by name (i.e. `UserQueryFilter`). Schemas already registered under the
142
+ * same name are skipped, which makes repeated calls cheap.
143
+ */
144
+ function registerQueryFilterSchemas() {
145
+ (0, utils_1.createSchemaModel)(exports.QueryConditionSchema, {
146
+ name: exports.QUERY_CONDITION_SCHEMA_NAME
147
+ });
148
+ for (const model of context_1.context.resource.models.values()) {
149
+ (0, utils_1.createSchemaModel)(buildQueryFilterSchema(model), {
150
+ name: (0, exports.queryFilterName)(model.name)
151
+ });
152
+ }
153
+ }
@@ -0,0 +1,37 @@
1
+ import { TObject, TSchema } from '@sinclair/typebox';
2
+ import { ResourceModel } from '@appweaver/common';
3
+ /**
4
+ * Resolves the schema name the query sort object of a model is registered under.
5
+ *
6
+ * @param {string} modelName The resource model name (i.e. `User`, `Post`).
7
+ * @returns {string} The query sort schema name (i.e. `UserQuerySort`).
8
+ */
9
+ export declare const querySortName: (modelName: string) => string;
10
+ /**
11
+ * Builds the query sort schema of a resource model, mirroring the ORDER BY part
12
+ * of a database query. Every sortable field of the model is declared explicitly,
13
+ * so the request validation keeps the known fields and strips everything else.
14
+ * Scalar fields accept a sort direction, to-one relations accept the sort object
15
+ * of the related model, and to-many relations accept a direction applied to
16
+ * their related record count, both under the relation name and under its count
17
+ * field name.
18
+ *
19
+ * @param {ResourceModel} model The resource model to build the sort schema for.
20
+ * @returns {TObject} The query sort schema of the model.
21
+ */
22
+ export declare function buildQuerySortSchema(model: ResourceModel): TObject;
23
+ /**
24
+ * Registers the query sort schema of every loaded resource model on the server,
25
+ * so route schemas can reference them by name (i.e. `UserQuerySort`). Schemas
26
+ * already registered under the same name are skipped, which makes repeated calls
27
+ * cheap.
28
+ */
29
+ export declare function registerQuerySortSchemas(): void;
30
+ /**
31
+ * Builds the schema of the sort property of a model, accepting either a
32
+ * comma-separated field list or the query sort object of the model.
33
+ *
34
+ * @param {string} modelName The resource model name the sort property belongs to.
35
+ * @returns {TSchema} The optional sort property schema.
36
+ */
37
+ export declare function querySortSchema(modelName: string): TSchema;
@@ -0,0 +1,114 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.querySortName = void 0;
4
+ exports.buildQuerySortSchema = buildQuerySortSchema;
5
+ exports.registerQuerySortSchemas = registerQuerySortSchemas;
6
+ exports.querySortSchema = querySortSchema;
7
+ const typebox_1 = require("@sinclair/typebox");
8
+ const common_1 = require("@appweaver/common");
9
+ const context_1 = require("../../context");
10
+ const utils_1 = require("../../utils");
11
+ /**
12
+ * Resolves the schema name the query sort object of a model is registered under.
13
+ *
14
+ * @param {string} modelName The resource model name (i.e. `User`, `Post`).
15
+ * @returns {string} The query sort schema name (i.e. `UserQuerySort`).
16
+ */
17
+ const querySortName = (modelName) => `${modelName}QuerySort`;
18
+ exports.querySortName = querySortName;
19
+ /** The accepted sort directions of a single field. */
20
+ const SORT_DIRECTIONS = ['asc', 'desc'];
21
+ const SortDirection = () => typebox_1.Type.Optional((0, common_1.StringEnum)(SORT_DIRECTIONS, { example: 'desc' }));
22
+ /**
23
+ * Builds the query sort schema of a resource model, mirroring the ORDER BY part
24
+ * of a database query. Every sortable field of the model is declared explicitly,
25
+ * so the request validation keeps the known fields and strips everything else.
26
+ * Scalar fields accept a sort direction, to-one relations accept the sort object
27
+ * of the related model, and to-many relations accept a direction applied to
28
+ * their related record count, both under the relation name and under its count
29
+ * field name.
30
+ *
31
+ * @param {ResourceModel} model The resource model to build the sort schema for.
32
+ * @returns {TObject} The query sort schema of the model.
33
+ */
34
+ function buildQuerySortSchema(model) {
35
+ const properties = {
36
+ id: SortDirection()
37
+ };
38
+ const auditFields = {
39
+ updatedAt: true,
40
+ createdAt: true,
41
+ createdById: true,
42
+ ...(model.config.audit ?? {})
43
+ };
44
+ for (const [fieldName, included] of Object.entries(auditFields)) {
45
+ if (included) {
46
+ properties[fieldName] = SortDirection();
47
+ }
48
+ }
49
+ // Array scalars hold no single value to order by, and virtual fields have no
50
+ // column of their own, so neither of them is sortable
51
+ for (const [fieldName, scalar] of Object.entries(model.config.scalars ?? {})) {
52
+ if (scalar.hidden || scalar.array) {
53
+ continue;
54
+ }
55
+ properties[fieldName] = SortDirection();
56
+ }
57
+ // A relation holding a list of records is ordered by their count, under the
58
+ // relation name and under its count field name alike
59
+ for (const [fieldName, relation] of Object.entries(model.config.relations ?? {})) {
60
+ if ((0, common_1.isRelationArray)(relation)) {
61
+ properties[fieldName] = SortDirection();
62
+ properties[(0, common_1.countFieldName)(fieldName)] = SortDirection();
63
+ continue;
64
+ }
65
+ properties[fieldName] = typebox_1.Type.Optional(typebox_1.Type.Ref((0, exports.querySortName)((0, common_1.capitalize)(relation.model))));
66
+ }
67
+ for (const [fieldName, file] of Object.entries(model.config.files ?? {})) {
68
+ if (file.array) {
69
+ properties[fieldName] = SortDirection();
70
+ properties[(0, common_1.countFieldName)(fieldName)] = SortDirection();
71
+ continue;
72
+ }
73
+ properties[fieldName] = typebox_1.Type.Optional(typebox_1.Type.Ref((0, exports.querySortName)('File')));
74
+ }
75
+ return typebox_1.Type.Object(properties, {
76
+ description: `Query sort for the ${model.name} resource, applying its fields in the ` +
77
+ 'order they are declared. A nested object sorts by a field of a relation, ' +
78
+ 'which has to be included in the response.'
79
+ });
80
+ }
81
+ /**
82
+ * Registers the query sort schema of every loaded resource model on the server,
83
+ * so route schemas can reference them by name (i.e. `UserQuerySort`). Schemas
84
+ * already registered under the same name are skipped, which makes repeated calls
85
+ * cheap.
86
+ */
87
+ function registerQuerySortSchemas() {
88
+ for (const model of context_1.context.resource.models.values()) {
89
+ (0, utils_1.createSchemaModel)(buildQuerySortSchema(model), {
90
+ name: (0, exports.querySortName)(model.name)
91
+ });
92
+ }
93
+ }
94
+ /**
95
+ * Builds the schema of the sort property of a model, accepting either a
96
+ * comma-separated field list or the query sort object of the model.
97
+ *
98
+ * @param {string} modelName The resource model name the sort property belongs to.
99
+ * @returns {TSchema} The optional sort property schema.
100
+ */
101
+ function querySortSchema(modelName) {
102
+ return typebox_1.Type.Optional(typebox_1.Type.Union([
103
+ typebox_1.Type.String({
104
+ example: '-createdAt,id',
105
+ description: 'Comma-separated list of fields to sort by, where a field prefixed ' +
106
+ 'with `-` is sorted in descending order and a dot notation path ' +
107
+ 'targets a field of an included relation (i.e. `-author.createdAt,id`)'
108
+ }),
109
+ typebox_1.Type.Ref((0, exports.querySortName)(modelName))
110
+ ], {
111
+ description: 'Fields to sort the results by, given as a comma-separated field list ' +
112
+ 'or as an object of field directions'
113
+ }));
114
+ }