@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
|
@@ -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
|
+
}
|