@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.
- 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/package.json +1 -1
- 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 -404
- 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 +4 -1
- 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,113 @@
|
|
|
1
|
+
import { AggregateSelect, AggregateValue, ResourceClient, ResourceModel } from '@appweaver/common';
|
|
2
|
+
/** The kind of value an aggregatable field holds. */
|
|
3
|
+
export type AggregateFieldType = 'numeric' | 'date';
|
|
4
|
+
/**
|
|
5
|
+
* The operations a selection is resolved by: the arguments of a single database aggregation, and the fields read off
|
|
6
|
+
* the earliest and the latest record of the aggregated range.
|
|
7
|
+
*/
|
|
8
|
+
export type AggregationOperations = {
|
|
9
|
+
/** The Prisma aggregation arguments, keyed by the prefixed operator name */
|
|
10
|
+
aggregate: Record<string, any>;
|
|
11
|
+
/** The fields whose value is read off the earliest record of the range */
|
|
12
|
+
first: string[];
|
|
13
|
+
/** The fields whose value is read off the latest record of the range */
|
|
14
|
+
last: string[];
|
|
15
|
+
};
|
|
16
|
+
/** A single aggregation period, labeled with the median date of its range. */
|
|
17
|
+
export interface AggregationRange {
|
|
18
|
+
from: Date;
|
|
19
|
+
to: Date;
|
|
20
|
+
median: Date;
|
|
21
|
+
}
|
|
22
|
+
/** The resolved aggregation date range together with its periods. */
|
|
23
|
+
export interface AggregationPeriods {
|
|
24
|
+
fromDate: Date;
|
|
25
|
+
toDate: Date;
|
|
26
|
+
ranges: AggregationRange[];
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Resolves an aggregation date range from its ISO string bounds and splits it into equally sized periods.
|
|
30
|
+
*
|
|
31
|
+
* @param {string} [from] - The ISO date string of the range start. Defaults to seven days before the range end.
|
|
32
|
+
* @param {string} [to] - The ISO date string of the range end. Defaults to the current date and time.
|
|
33
|
+
* @param {number} [step] - The size of a single period in units of the automatically selected time unit. If not
|
|
34
|
+
* provided, one unit is used and the unit is derived from the range length.
|
|
35
|
+
* @param {boolean} [safeIncrement=true] - Whether the period increments use the derived time unit and stay consistent
|
|
36
|
+
* across daylight saving time changes. When false, the step is interpreted in seconds.
|
|
37
|
+
* @return {AggregationPeriods} The resolved range bounds and one range per period, each labeled with its median date.
|
|
38
|
+
*/
|
|
39
|
+
export declare function buildAggregationPeriods(from?: string, to?: string, step?: number, safeIncrement?: boolean): AggregationPeriods;
|
|
40
|
+
/**
|
|
41
|
+
* Maps an aggregation selection to the operations that resolve it, splitting the operators the database aggregates in
|
|
42
|
+
* a single query from the `first` and `last` operators, which read the boundary records of the range instead. Every
|
|
43
|
+
* field is validated against the model it is aggregated on, and every operator against the kind of value its field
|
|
44
|
+
* holds, since only the numeric fields can be summed and averaged.
|
|
45
|
+
*
|
|
46
|
+
* @param {AggregateSelect<Object>} select - The operations to perform per field, with the operators of a field nested
|
|
47
|
+
* inside it. The operators set to false are left out.
|
|
48
|
+
* @param {string} resourceName - The name of the model the selection is aggregated on.
|
|
49
|
+
* @return {AggregationOperations} The Prisma aggregation arguments, keyed by the prefixed operator name with the
|
|
50
|
+
* selected fields nested inside, together with the fields read off the earliest and the latest record of the range.
|
|
51
|
+
* The arguments always count the records of the range when a boundary field was selected, so the boundary queries can
|
|
52
|
+
* be skipped for the empty ranges.
|
|
53
|
+
* @throws {HttpError} 400 if the selection is not an object, selects no operation at all, names a field the model
|
|
54
|
+
* cannot aggregate, or applies an operator the kind of value of its field does not support.
|
|
55
|
+
*/
|
|
56
|
+
export declare function mapAggregationSelect<T>(select: AggregateSelect<T>, resourceName: string): AggregationOperations;
|
|
57
|
+
/**
|
|
58
|
+
* Maps a Prisma aggregation result back to the response format by swapping the operator and field nesting, so a
|
|
59
|
+
* `{ _count: { views: 3 } }` result becomes the `{ views: { count: 3 } }` response value. The boundary record values
|
|
60
|
+
* are mapped the same way when they are merged into the result under their own `_first` and `_last` keys.
|
|
61
|
+
*
|
|
62
|
+
* @param {Object} result - The Prisma aggregation result, keyed by the prefixed operator name with the aggregated
|
|
63
|
+
* fields nested inside.
|
|
64
|
+
* @return {AggregateValue<Object>} The aggregated values keyed by field name, with the operator results nested inside.
|
|
65
|
+
* The record count of the range is left out, since it counts no field of its own.
|
|
66
|
+
*/
|
|
67
|
+
export declare function mapAggregationResult<T>(result: Record<string, Record<string, any>>): AggregateValue<T>;
|
|
68
|
+
/**
|
|
69
|
+
* Reads the `first` and `last` values of an aggregated range off its boundary records. The earliest and the latest
|
|
70
|
+
* record of the range are looked up by the same date field the range is sliced by, with the record id breaking the
|
|
71
|
+
* ties, and only the selected fields are read off them. The values of an empty range are resolved to null without
|
|
72
|
+
* querying for them, so a range the aggregation already counted as empty costs nothing.
|
|
73
|
+
*
|
|
74
|
+
* @param {ResourceClient} client - The model client the boundary records are read with, which is the transaction
|
|
75
|
+
* client of the aggregation.
|
|
76
|
+
* @param {Object} where - The database query of the aggregated range, applied unchanged to the boundary lookups.
|
|
77
|
+
* @param {string} dateField - The date field the range is sliced by, which the boundary records are ordered by.
|
|
78
|
+
* @param {AggregationOperations} operations - The operations holding the fields to read off each boundary record.
|
|
79
|
+
* @param {number} [recordCount] - The number of records in the range, used to skip the lookups of an empty range. When
|
|
80
|
+
* omitted, the lookups are always performed.
|
|
81
|
+
* @return {Promise<Object>} The boundary values keyed by the prefixed operator name (`_first` and `_last`) with the
|
|
82
|
+
* read fields nested inside, ready to be merged into the aggregation result of the range. Empty when no boundary field
|
|
83
|
+
* was selected.
|
|
84
|
+
*/
|
|
85
|
+
export declare function readAggregationBoundaries(client: ResourceClient, where: any, dateField: string, operations: AggregationOperations, recordCount?: number): Promise<Record<string, any>>;
|
|
86
|
+
/**
|
|
87
|
+
* Reads the record count a Prisma aggregation result holds for its range, as counted by the `_all` selection that
|
|
88
|
+
* {@link mapAggregationSelect} adds whenever a boundary field is selected.
|
|
89
|
+
*
|
|
90
|
+
* @param {Object} result - The Prisma aggregation result to read the count off.
|
|
91
|
+
* @return {number|undefined} The number of records in the range, or undefined when the result does not count them.
|
|
92
|
+
*/
|
|
93
|
+
export declare function aggregationRecordCount(result: Record<string, any>): number | undefined;
|
|
94
|
+
/**
|
|
95
|
+
* Validates the date field the aggregated range is applied on against the model it is aggregated on.
|
|
96
|
+
*
|
|
97
|
+
* @param {string} dateField - The name of the date field to validate.
|
|
98
|
+
* @param {string} resourceName - The name of the model the range is applied on.
|
|
99
|
+
* @return {string} The validated date field name.
|
|
100
|
+
* @throws {HttpError} 400 if the model has no date field under that name.
|
|
101
|
+
*/
|
|
102
|
+
export declare function checkAggregationDateField(dateField: string, resourceName: string): string;
|
|
103
|
+
/**
|
|
104
|
+
* Lists the aggregatable fields of a resource model together with the kind of value they hold. The id is aggregatable
|
|
105
|
+
* when it is numeric, the audit fields follow the audit configuration of the model, and the scalars are aggregatable
|
|
106
|
+
* when they hold a single numeric or date value and are not hidden. The virtual fields are never aggregatable, since
|
|
107
|
+
* they have no column of their own.
|
|
108
|
+
*
|
|
109
|
+
* @param {ResourceModel} model - The resource model whose fields are listed.
|
|
110
|
+
* @return {Array} The name and kind (`numeric` or `date`) of every aggregatable field, in the order the model declares
|
|
111
|
+
* them.
|
|
112
|
+
*/
|
|
113
|
+
export declare function aggregateFields(model: ResourceModel): [field: string, type: AggregateFieldType][];
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildAggregationPeriods = buildAggregationPeriods;
|
|
4
|
+
exports.mapAggregationSelect = mapAggregationSelect;
|
|
5
|
+
exports.mapAggregationResult = mapAggregationResult;
|
|
6
|
+
exports.readAggregationBoundaries = readAggregationBoundaries;
|
|
7
|
+
exports.aggregationRecordCount = aggregationRecordCount;
|
|
8
|
+
exports.checkAggregationDateField = checkAggregationDateField;
|
|
9
|
+
exports.aggregateFields = aggregateFields;
|
|
10
|
+
const date_fns_1 = require("date-fns");
|
|
11
|
+
const common_1 = require("@appweaver/common");
|
|
12
|
+
const context_1 = require("../../context");
|
|
13
|
+
const errors_1 = require("../../errors");
|
|
14
|
+
/** The scalar types holding a numeric value, which every operator applies to. */
|
|
15
|
+
const NUMERIC_TYPES = ['int', 'bigInt', 'float'];
|
|
16
|
+
/** The scalar types holding a date value, which cannot be summed or averaged. */
|
|
17
|
+
const DATE_TYPES = ['dateTime'];
|
|
18
|
+
/** The operators resolved by reading a boundary record of the range instead of aggregating it. */
|
|
19
|
+
const BOUNDARY_OPERATORS = ['first', 'last'];
|
|
20
|
+
/** The Prisma selection counting every record of a range, regardless of the fields it holds. */
|
|
21
|
+
const COUNT_ALL_FIELD = '_all';
|
|
22
|
+
/** The aggregation operators applicable to a field, per kind of value. */
|
|
23
|
+
const OPERATORS = {
|
|
24
|
+
numeric: ['count', 'sum', 'avg', 'min', 'max', ...BOUNDARY_OPERATORS],
|
|
25
|
+
date: ['count', 'min', 'max', ...BOUNDARY_OPERATORS]
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Resolves an aggregation date range from its ISO string bounds and splits it into equally sized periods.
|
|
29
|
+
*
|
|
30
|
+
* @param {string} [from] - The ISO date string of the range start. Defaults to seven days before the range end.
|
|
31
|
+
* @param {string} [to] - The ISO date string of the range end. Defaults to the current date and time.
|
|
32
|
+
* @param {number} [step] - The size of a single period in units of the automatically selected time unit. If not
|
|
33
|
+
* provided, one unit is used and the unit is derived from the range length.
|
|
34
|
+
* @param {boolean} [safeIncrement=true] - Whether the period increments use the derived time unit and stay consistent
|
|
35
|
+
* across daylight saving time changes. When false, the step is interpreted in seconds.
|
|
36
|
+
* @return {AggregationPeriods} The resolved range bounds and one range per period, each labeled with its median date.
|
|
37
|
+
*/
|
|
38
|
+
function buildAggregationPeriods(from, to, step, safeIncrement = true) {
|
|
39
|
+
const toDate = (0, date_fns_1.parseISO)(to ?? new Date().toISOString());
|
|
40
|
+
const fromDate = from ? (0, date_fns_1.parseISO)(from) : (0, date_fns_1.subDays)(toDate, 7);
|
|
41
|
+
const iterator = makeAggregationIterator(fromDate, toDate, step, safeIncrement);
|
|
42
|
+
const ranges = [];
|
|
43
|
+
let currentDate = (0, date_fns_1.parseISO)(fromDate.toISOString());
|
|
44
|
+
while (currentDate < toDate) {
|
|
45
|
+
const date = currentDate;
|
|
46
|
+
const median = iterator.addPeriod(date, (iterator.step - 1) / 2);
|
|
47
|
+
currentDate = iterator.addPeriod(currentDate, iterator.step);
|
|
48
|
+
ranges.push({ from: date, to: currentDate, median });
|
|
49
|
+
}
|
|
50
|
+
return { fromDate, toDate, ranges };
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Maps an aggregation selection to the operations that resolve it, splitting the operators the database aggregates in
|
|
54
|
+
* a single query from the `first` and `last` operators, which read the boundary records of the range instead. Every
|
|
55
|
+
* field is validated against the model it is aggregated on, and every operator against the kind of value its field
|
|
56
|
+
* holds, since only the numeric fields can be summed and averaged.
|
|
57
|
+
*
|
|
58
|
+
* @param {AggregateSelect<Object>} select - The operations to perform per field, with the operators of a field nested
|
|
59
|
+
* inside it. The operators set to false are left out.
|
|
60
|
+
* @param {string} resourceName - The name of the model the selection is aggregated on.
|
|
61
|
+
* @return {AggregationOperations} The Prisma aggregation arguments, keyed by the prefixed operator name with the
|
|
62
|
+
* selected fields nested inside, together with the fields read off the earliest and the latest record of the range.
|
|
63
|
+
* The arguments always count the records of the range when a boundary field was selected, so the boundary queries can
|
|
64
|
+
* be skipped for the empty ranges.
|
|
65
|
+
* @throws {HttpError} 400 if the selection is not an object, selects no operation at all, names a field the model
|
|
66
|
+
* cannot aggregate, or applies an operator the kind of value of its field does not support.
|
|
67
|
+
*/
|
|
68
|
+
function mapAggregationSelect(select, resourceName) {
|
|
69
|
+
if (!(0, common_1.isPlainObject)(select)) {
|
|
70
|
+
throw new errors_1.HttpError(`${resourceName} aggregate select must be an object of fields to aggregate`, 400);
|
|
71
|
+
}
|
|
72
|
+
const model = (0, context_1.injectModel)(resourceName);
|
|
73
|
+
const fieldTypes = new Map(aggregateFields(model));
|
|
74
|
+
const operations = {
|
|
75
|
+
aggregate: {},
|
|
76
|
+
first: [],
|
|
77
|
+
last: []
|
|
78
|
+
};
|
|
79
|
+
for (const [field, operators] of Object.entries(select)) {
|
|
80
|
+
const fieldType = fieldTypes.get(field);
|
|
81
|
+
if (!fieldType) {
|
|
82
|
+
throw new errors_1.HttpError(`Cannot aggregate the '${field}' field, it is not a numeric or date field of the ${model.name} model`, 400);
|
|
83
|
+
}
|
|
84
|
+
if (!(0, common_1.isPlainObject)(operators)) {
|
|
85
|
+
throw new errors_1.HttpError(`Cannot aggregate the '${field}' field, its value must be an object of aggregation operators`, 400);
|
|
86
|
+
}
|
|
87
|
+
for (const [operator, enabled] of Object.entries(operators)) {
|
|
88
|
+
if (!OPERATORS[fieldType].includes(operator)) {
|
|
89
|
+
throw new errors_1.HttpError(`Cannot apply the '${operator}' operator to the ${fieldType} field '${field}', ` +
|
|
90
|
+
`expected one of: ${OPERATORS[fieldType].join(', ')}`, 400);
|
|
91
|
+
}
|
|
92
|
+
if (!enabled) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (isBoundaryOperator(operator)) {
|
|
96
|
+
operations[operator].push(field);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
(0, common_1.setValue)(operations.aggregate, `_${operator}.${field}`, true);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const hasBoundary = operations.first.length + operations.last.length > 0;
|
|
104
|
+
if (Object.keys(operations.aggregate).length === 0 && !hasBoundary) {
|
|
105
|
+
throw new errors_1.HttpError(`${resourceName} aggregate requires at least one field with a selected aggregation operator`, 400);
|
|
106
|
+
}
|
|
107
|
+
// The record count of a range decides whether its boundary records have to be
|
|
108
|
+
// read at all, and it is also the only aggregation left to perform when the
|
|
109
|
+
// selection holds nothing but the boundary operators
|
|
110
|
+
if (hasBoundary) {
|
|
111
|
+
(0, common_1.setValue)(operations.aggregate, `_count.${COUNT_ALL_FIELD}`, true);
|
|
112
|
+
}
|
|
113
|
+
return operations;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Maps a Prisma aggregation result back to the response format by swapping the operator and field nesting, so a
|
|
117
|
+
* `{ _count: { views: 3 } }` result becomes the `{ views: { count: 3 } }` response value. The boundary record values
|
|
118
|
+
* are mapped the same way when they are merged into the result under their own `_first` and `_last` keys.
|
|
119
|
+
*
|
|
120
|
+
* @param {Object} result - The Prisma aggregation result, keyed by the prefixed operator name with the aggregated
|
|
121
|
+
* fields nested inside.
|
|
122
|
+
* @return {AggregateValue<Object>} The aggregated values keyed by field name, with the operator results nested inside.
|
|
123
|
+
* The record count of the range is left out, since it counts no field of its own.
|
|
124
|
+
*/
|
|
125
|
+
function mapAggregationResult(result) {
|
|
126
|
+
const aggregationMap = {};
|
|
127
|
+
for (const operator in result) {
|
|
128
|
+
const fields = result[operator];
|
|
129
|
+
for (const field in fields) {
|
|
130
|
+
if (field === COUNT_ALL_FIELD) {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
(0, common_1.setValue)(aggregationMap, `${field}.${operator.substring(1)}`, fields[field]);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return aggregationMap;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Reads the `first` and `last` values of an aggregated range off its boundary records. The earliest and the latest
|
|
140
|
+
* record of the range are looked up by the same date field the range is sliced by, with the record id breaking the
|
|
141
|
+
* ties, and only the selected fields are read off them. The values of an empty range are resolved to null without
|
|
142
|
+
* querying for them, so a range the aggregation already counted as empty costs nothing.
|
|
143
|
+
*
|
|
144
|
+
* @param {ResourceClient} client - The model client the boundary records are read with, which is the transaction
|
|
145
|
+
* client of the aggregation.
|
|
146
|
+
* @param {Object} where - The database query of the aggregated range, applied unchanged to the boundary lookups.
|
|
147
|
+
* @param {string} dateField - The date field the range is sliced by, which the boundary records are ordered by.
|
|
148
|
+
* @param {AggregationOperations} operations - The operations holding the fields to read off each boundary record.
|
|
149
|
+
* @param {number} [recordCount] - The number of records in the range, used to skip the lookups of an empty range. When
|
|
150
|
+
* omitted, the lookups are always performed.
|
|
151
|
+
* @return {Promise<Object>} The boundary values keyed by the prefixed operator name (`_first` and `_last`) with the
|
|
152
|
+
* read fields nested inside, ready to be merged into the aggregation result of the range. Empty when no boundary field
|
|
153
|
+
* was selected.
|
|
154
|
+
*/
|
|
155
|
+
async function readAggregationBoundaries(client, where, dateField, operations, recordCount) {
|
|
156
|
+
const boundaries = {};
|
|
157
|
+
const readBoundary = async (operator, direction) => {
|
|
158
|
+
const fields = operations[operator];
|
|
159
|
+
if (fields.length === 0) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
// An empty range holds no record to read the values off, and its fields are
|
|
163
|
+
// resolved to null the same way the database aggregations resolve theirs
|
|
164
|
+
const record = recordCount === 0
|
|
165
|
+
? undefined
|
|
166
|
+
: await client.findFirst({
|
|
167
|
+
where,
|
|
168
|
+
// The id breaks the ties of the records sharing the same date, so
|
|
169
|
+
// the boundary of a range stays the same across identical requests
|
|
170
|
+
orderBy: [{ [dateField]: direction }, { id: direction }],
|
|
171
|
+
select: Object.fromEntries(fields.map((field) => [field, true]))
|
|
172
|
+
});
|
|
173
|
+
boundaries[`_${operator}`] = Object.fromEntries(fields.map((field) => [field, record?.[field] ?? null]));
|
|
174
|
+
};
|
|
175
|
+
await Promise.all([
|
|
176
|
+
readBoundary('first', 'asc'),
|
|
177
|
+
readBoundary('last', 'desc')
|
|
178
|
+
]);
|
|
179
|
+
return boundaries;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Reads the record count a Prisma aggregation result holds for its range, as counted by the `_all` selection that
|
|
183
|
+
* {@link mapAggregationSelect} adds whenever a boundary field is selected.
|
|
184
|
+
*
|
|
185
|
+
* @param {Object} result - The Prisma aggregation result to read the count off.
|
|
186
|
+
* @return {number|undefined} The number of records in the range, or undefined when the result does not count them.
|
|
187
|
+
*/
|
|
188
|
+
function aggregationRecordCount(result) {
|
|
189
|
+
return (0, common_1.isPlainObject)(result?._count)
|
|
190
|
+
? result._count[COUNT_ALL_FIELD]
|
|
191
|
+
: undefined;
|
|
192
|
+
}
|
|
193
|
+
/** Determines whether an operator reads a boundary record instead of aggregating the range. */
|
|
194
|
+
function isBoundaryOperator(operator) {
|
|
195
|
+
return BOUNDARY_OPERATORS.includes(operator);
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Validates the date field the aggregated range is applied on against the model it is aggregated on.
|
|
199
|
+
*
|
|
200
|
+
* @param {string} dateField - The name of the date field to validate.
|
|
201
|
+
* @param {string} resourceName - The name of the model the range is applied on.
|
|
202
|
+
* @return {string} The validated date field name.
|
|
203
|
+
* @throws {HttpError} 400 if the model has no date field under that name.
|
|
204
|
+
*/
|
|
205
|
+
function checkAggregationDateField(dateField, resourceName) {
|
|
206
|
+
const model = (0, context_1.injectModel)(resourceName);
|
|
207
|
+
const dateFields = aggregateFields(model)
|
|
208
|
+
.filter(([, type]) => type === 'date')
|
|
209
|
+
.map(([field]) => field);
|
|
210
|
+
if (!dateFields.includes(dateField)) {
|
|
211
|
+
throw new errors_1.HttpError(`Cannot aggregate over the '${dateField}' field, it is not a date field of the ${model.name} model` +
|
|
212
|
+
(dateFields.length > 0
|
|
213
|
+
? `, expected one of: ${dateFields.join(', ')}`
|
|
214
|
+
: ''), 400);
|
|
215
|
+
}
|
|
216
|
+
return dateField;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Lists the aggregatable fields of a resource model together with the kind of value they hold. The id is aggregatable
|
|
220
|
+
* when it is numeric, the audit fields follow the audit configuration of the model, and the scalars are aggregatable
|
|
221
|
+
* when they hold a single numeric or date value and are not hidden. The virtual fields are never aggregatable, since
|
|
222
|
+
* they have no column of their own.
|
|
223
|
+
*
|
|
224
|
+
* @param {ResourceModel} model - The resource model whose fields are listed.
|
|
225
|
+
* @return {Array} The name and kind (`numeric` or `date`) of every aggregatable field, in the order the model declares
|
|
226
|
+
* them.
|
|
227
|
+
*/
|
|
228
|
+
function aggregateFields(model) {
|
|
229
|
+
const fields = [];
|
|
230
|
+
if ((model.config.id?.type ?? 'int') !== 'string') {
|
|
231
|
+
fields.push(['id', 'numeric']);
|
|
232
|
+
}
|
|
233
|
+
const auditFields = {
|
|
234
|
+
updatedAt: true,
|
|
235
|
+
createdAt: true,
|
|
236
|
+
createdById: true,
|
|
237
|
+
...(model.config.audit ?? {})
|
|
238
|
+
};
|
|
239
|
+
for (const [fieldName, included] of Object.entries(auditFields)) {
|
|
240
|
+
if (included) {
|
|
241
|
+
fields.push([
|
|
242
|
+
fieldName,
|
|
243
|
+
fieldName === 'createdById' ? 'numeric' : 'date'
|
|
244
|
+
]);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
for (const [fieldName, scalar] of Object.entries(model.config.scalars ?? {})) {
|
|
248
|
+
if (scalar.hidden || scalar.array) {
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
if (NUMERIC_TYPES.includes(scalar.type)) {
|
|
252
|
+
fields.push([fieldName, 'numeric']);
|
|
253
|
+
}
|
|
254
|
+
else if (DATE_TYPES.includes(scalar.type)) {
|
|
255
|
+
fields.push([fieldName, 'date']);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return fields;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Builds the period iterator used to split an aggregation date range into equally sized periods. When no step is
|
|
262
|
+
* provided, a step of one is used with the time unit derived from the range length, and the returned increment
|
|
263
|
+
* function compensates for daylight saving time offset changes so every period keeps the time zone offset of its
|
|
264
|
+
* start date.
|
|
265
|
+
*
|
|
266
|
+
* @param {Date} fromDate - The start of the aggregated date range.
|
|
267
|
+
* @param {Date} toDate - The end of the aggregated date range.
|
|
268
|
+
* @param {number} [step] - The size of a single period in units of the selected time unit. If not provided, a step of
|
|
269
|
+
* one is used with the time unit derived from the range length (seconds up to a minute, minutes up to an hour, hours
|
|
270
|
+
* up to a day, days up to a month, months up to a year, and years beyond that).
|
|
271
|
+
* @param {boolean} [safeIncrement=true] - Whether the increments use the derived time unit. When false, the step is
|
|
272
|
+
* interpreted in seconds.
|
|
273
|
+
* @return {{addPeriod: PeriodIncrementFn, step: number}} The iterator holding the resolved step amount and the
|
|
274
|
+
* `addPeriod` function that adds a number of periods to a date while preserving the time zone offset of that date.
|
|
275
|
+
*/
|
|
276
|
+
function makeAggregationIterator(fromDate, toDate, step, safeIncrement = true) {
|
|
277
|
+
let stepAmount = step;
|
|
278
|
+
let incrementFn = date_fns_1.addSeconds;
|
|
279
|
+
if (!stepAmount) {
|
|
280
|
+
const diffInSeconds = (0, date_fns_1.differenceInSeconds)(toDate, fromDate);
|
|
281
|
+
const diffInMonths = (0, date_fns_1.differenceInMonths)(toDate, fromDate);
|
|
282
|
+
const diffInYears = (0, date_fns_1.differenceInYears)(toDate, fromDate);
|
|
283
|
+
stepAmount = 1;
|
|
284
|
+
// 1-second step if the difference is less than or equal to 1 minute
|
|
285
|
+
if (diffInSeconds <= 60) {
|
|
286
|
+
incrementFn = date_fns_1.addSeconds;
|
|
287
|
+
}
|
|
288
|
+
// 1-minute step if the difference is less than or equal to 1 hour
|
|
289
|
+
else if (diffInSeconds <= 3600) {
|
|
290
|
+
incrementFn = date_fns_1.addMinutes;
|
|
291
|
+
}
|
|
292
|
+
// 1-hour step if the difference is less than or equal to 1 day
|
|
293
|
+
else if (diffInSeconds <= 86400) {
|
|
294
|
+
incrementFn = date_fns_1.addHours;
|
|
295
|
+
}
|
|
296
|
+
// 1-day step if the difference is less than or equal to 1 month
|
|
297
|
+
else if (diffInMonths <= 1) {
|
|
298
|
+
incrementFn = date_fns_1.addDays;
|
|
299
|
+
}
|
|
300
|
+
// 1-month step if the difference is less than or equal to 1 year
|
|
301
|
+
else if (diffInYears <= 1) {
|
|
302
|
+
incrementFn = date_fns_1.addMonths;
|
|
303
|
+
}
|
|
304
|
+
// 1-year step if the difference is equal to 1 year or more
|
|
305
|
+
else {
|
|
306
|
+
incrementFn = date_fns_1.addYears;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
// A higher-order function that adjusts date increments to account for
|
|
310
|
+
// changes in daylight saving time (DST). When incrementing dates in time
|
|
311
|
+
// zones that observe DST, this function ensures that the resulting date
|
|
312
|
+
// remains consistent with the original date's time zone offset.
|
|
313
|
+
const dstAgnosticFn = (fn) => {
|
|
314
|
+
return (date, amount) => {
|
|
315
|
+
const endDate = fn(date, amount);
|
|
316
|
+
return (0, date_fns_1.addMinutes)(endDate, date.getTimezoneOffset() - endDate.getTimezoneOffset());
|
|
317
|
+
};
|
|
318
|
+
};
|
|
319
|
+
return {
|
|
320
|
+
addPeriod: dstAgnosticFn(safeIncrement ? incrementFn : date_fns_1.addSeconds),
|
|
321
|
+
step: stepAmount
|
|
322
|
+
};
|
|
323
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maps a request filter to a Prisma `where` clause based on the schema of the
|
|
3
|
+
* given resource model. The logical operators (`_and`, `_or`, `_not`, `_nor`)
|
|
4
|
+
* combine their nested conditions with the matching connective, the field
|
|
5
|
+
* operator objects (`_eq`, `_gt`, `_like`, `_exists`, ...) are translated to
|
|
6
|
+
* their database conditions, and the list relation quantifiers (`_some`,
|
|
7
|
+
* `_every`, `_none`) filter the related records. Plain values keep their
|
|
8
|
+
* shorthand meaning: relation and file fields are matched by id (wrapped in a
|
|
9
|
+
* `some` condition for the array relations), array fields use the `has` and
|
|
10
|
+
* `hasSome` operators, an array value on a numeric or date field becomes an
|
|
11
|
+
* inclusive `gte`/`lte` range, other array values become an `in` inclusion,
|
|
12
|
+
* nested objects are mapped recursively against the related model, and null
|
|
13
|
+
* values are passed through to match records without a value or a related
|
|
14
|
+
* record. Values that match no known field are passed through with only their
|
|
15
|
+
* operators translated, so the Prisma operators can still be used directly.
|
|
16
|
+
*
|
|
17
|
+
* @param {Object} filter The request filter object to map.
|
|
18
|
+
* @param {string} resourceName The name of the model the filter is matched
|
|
19
|
+
* against. It is set to the related model name when recursing into a nested
|
|
20
|
+
* relation or file filter.
|
|
21
|
+
* @returns {Object} The `where` clause with every recognized field mapped to its
|
|
22
|
+
* database condition and the unrecognized values passed through unchanged.
|
|
23
|
+
*/
|
|
24
|
+
export declare function mapQueryFilter(filter: any, resourceName: string): any;
|