@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,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;