@appweaver/data 0.0.0-stage → 2.0.1
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/LICENSE +21 -0
- package/README.md +61 -2
- package/config.d.ts +6 -0
- package/config.js +22 -0
- package/database/create-client.d.ts +11 -0
- package/database/create-client.js +145 -0
- package/database/database-config.d.ts +14 -0
- package/database/database-config.js +40 -0
- package/database/database-error.d.ts +49 -0
- package/database/database-error.js +216 -0
- package/database/index.d.ts +3 -0
- package/database/index.js +19 -0
- package/database/prisma-database.d.ts +33 -0
- package/database/prisma-database.js +178 -0
- package/database/run-transaction.d.ts +21 -0
- package/database/run-transaction.js +15 -0
- package/export/export-config.d.ts +7 -0
- package/export/export-config.js +17 -0
- package/export/export-service.d.ts +22 -0
- package/export/export-service.js +188 -0
- package/export/index.d.ts +1 -0
- package/export/index.js +17 -0
- package/factory/create-model.d.ts +11 -0
- package/factory/create-model.js +479 -0
- package/factory/create-policy.d.ts +12 -0
- package/factory/create-policy.js +20 -0
- package/factory/create-service.d.ts +14 -0
- package/factory/create-service.js +116 -0
- package/index.d.ts +22 -0
- package/index.js +38 -0
- package/internal.d.ts +2 -0
- package/internal.js +6 -0
- package/module.d.ts +3 -0
- package/module.js +81 -0
- package/package.json +67 -3
- package/prisma/client/browser.d.ts +39 -0
- package/prisma/client/browser.js +56 -0
- package/prisma/client/client.d.ts +58 -0
- package/prisma/client/client.js +70 -0
- package/prisma/client/commonInputTypes.d.ts +345 -0
- package/prisma/client/commonInputTypes.js +11 -0
- package/prisma/client/enums.d.ts +1 -0
- package/prisma/client/enums.js +11 -0
- package/prisma/client/internal/class.d.ts +198 -0
- package/prisma/client/internal/class.js +85 -0
- package/prisma/client/internal/prismaNamespace.d.ts +1125 -0
- package/prisma/client/internal/prismaNamespace.js +204 -0
- package/prisma/client/internal/prismaNamespaceBrowser.d.ts +137 -0
- package/prisma/client/internal/prismaNamespaceBrowser.js +175 -0
- package/prisma/client/models/ApiKey.d.ts +1167 -0
- package/prisma/client/models/ApiKey.js +2 -0
- package/prisma/client/models/ConnectedAccount.d.ts +1094 -0
- package/prisma/client/models/ConnectedAccount.js +2 -0
- package/prisma/client/models/File.d.ts +1291 -0
- package/prisma/client/models/File.js +2 -0
- package/prisma/client/models/OneTimeToken.d.ts +1055 -0
- package/prisma/client/models/OneTimeToken.js +2 -0
- package/prisma/client/models/Permission.d.ts +1209 -0
- package/prisma/client/models/Permission.js +2 -0
- package/prisma/client/models/Role.d.ts +1209 -0
- package/prisma/client/models/Role.js +2 -0
- package/prisma/client/models/Seeder.d.ts +1023 -0
- package/prisma/client/models/Seeder.js +2 -0
- package/prisma/client/models.d.ts +8 -0
- package/prisma/client/models.js +2 -0
- package/resource/load-data-resources.d.ts +13 -0
- package/resource/load-data-resources.js +37 -0
- package/resource/model-schema.d.ts +34 -0
- package/resource/model-schema.js +54 -0
- package/resource/resource-config.d.ts +16 -0
- package/resource/resource-config.js +62 -0
- package/resource/resource-loader.d.ts +25 -0
- package/resource/resource-loader.js +138 -0
- package/resource/resource-service.d.ts +462 -0
- package/resource/resource-service.js +1042 -0
- package/resource/schemas/index.d.ts +3 -0
- package/resource/schemas/index.js +19 -0
- package/resource/schemas/resource-aggregate-schema.d.ts +53 -0
- package/resource/schemas/resource-aggregate-schema.js +130 -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 +38 -0
- package/resource/schemas/resource-sort-schema.js +113 -0
- package/resource/utils/aggregate-util.d.ts +113 -0
- package/resource/utils/aggregate-util.js +322 -0
- package/resource/utils/cursor-util.d.ts +60 -0
- package/resource/utils/cursor-util.js +116 -0
- package/resource/utils/date-util.d.ts +22 -0
- package/resource/utils/date-util.js +112 -0
- package/resource/utils/delete-util.d.ts +131 -0
- package/resource/utils/delete-util.js +385 -0
- package/resource/utils/filter-util.d.ts +24 -0
- package/resource/utils/filter-util.js +289 -0
- package/resource/utils/prune-util.d.ts +11 -0
- package/resource/utils/prune-util.js +56 -0
- package/resource/utils/relation-util.d.ts +75 -0
- package/resource/utils/relation-util.js +400 -0
- package/resource/utils/sort-util.d.ts +31 -0
- package/resource/utils/sort-util.js +235 -0
- package/resource/utils/text-search-util.d.ts +14 -0
- package/resource/utils/text-search-util.js +39 -0
- package/resources.d.ts +10 -0
- package/resources.js +26 -0
- package/security/auth-config.d.ts +23 -0
- package/security/auth-config.js +43 -0
- package/security/create-auth-resources.d.ts +28 -0
- package/security/create-auth-resources.js +163 -0
- package/security/helper.d.ts +109 -0
- package/security/helper.js +298 -0
- package/security/resources/api-key/model.d.ts +2 -0
- package/security/resources/api-key/model.js +67 -0
- package/security/resources/api-key/policy.d.ts +2 -0
- package/security/resources/api-key/policy.js +27 -0
- package/security/resources/api-key/service.d.ts +3 -0
- package/security/resources/api-key/service.js +34 -0
- package/security/resources/connected-account/model.d.ts +2 -0
- package/security/resources/connected-account/model.js +57 -0
- package/security/resources/connected-account/service.d.ts +2 -0
- package/security/resources/connected-account/service.js +7 -0
- package/security/resources/one-time-token/model.d.ts +2 -0
- package/security/resources/one-time-token/model.js +36 -0
- package/security/resources/permission/model.d.ts +2 -0
- package/security/resources/permission/model.js +16 -0
- package/security/resources/role/model.d.ts +2 -0
- package/security/resources/role/model.js +22 -0
- package/security/store/database-security-store.d.ts +5 -0
- package/security/store/database-security-store.js +54 -0
- package/seeder/create-seeder.d.ts +29 -0
- package/seeder/create-seeder.js +28 -0
- package/seeder/index.d.ts +2 -0
- package/seeder/index.js +18 -0
- package/seeder/resources/seeder/model.d.ts +2 -0
- package/seeder/resources/seeder/model.js +38 -0
- package/seeder/seeder.d.ts +29 -0
- package/seeder/seeder.js +197 -0
- package/storage/file-service.d.ts +133 -0
- package/storage/file-service.js +586 -0
- package/storage/image-config.d.ts +3 -0
- package/storage/image-config.js +11 -0
- package/storage/resources/file/model.d.ts +2 -0
- package/storage/resources/file/model.js +80 -0
- package/types/auth.d.ts +23 -0
- package/types/auth.js +2 -0
- package/types/generated.d.ts +366 -0
- package/types/generated.js +3 -0
- package/types/storage.d.ts +35 -0
- package/types/storage.js +2 -0
- package/utils/export-util.d.ts +8 -0
- package/utils/export-util.js +26 -0
- package/utils/file-util.d.ts +106 -0
- package/utils/file-util.js +254 -0
- package/utils/model-util.d.ts +36 -0
- package/utils/model-util.js +154 -0
- package/utils/policy-util.d.ts +18 -0
- package/utils/policy-util.js +28 -0
- package/utils/virtual-util.d.ts +11 -0
- package/utils/virtual-util.js +53 -0
|
@@ -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 {ResourceError} `RESOURCE_INVALID_AGGREGATE` 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 {ResourceError} `RESOURCE_INVALID_AGGREGATE` 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,322 @@
|
|
|
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_util_1 = require("./date-util");
|
|
11
|
+
const common_1 = require("@appweaver/common");
|
|
12
|
+
const core_1 = require("@appweaver/core");
|
|
13
|
+
/** The scalar types holding a numeric value, which every operator applies to. */
|
|
14
|
+
const NUMERIC_TYPES = ['int', 'bigInt', 'float'];
|
|
15
|
+
/** The scalar types holding a date value, which cannot be summed or averaged. */
|
|
16
|
+
const DATE_TYPES = ['dateTime'];
|
|
17
|
+
/** The operators resolved by reading a boundary record of the range instead of aggregating it. */
|
|
18
|
+
const BOUNDARY_OPERATORS = ['first', 'last'];
|
|
19
|
+
/** The Prisma selection counting every record of a range, regardless of the fields it holds. */
|
|
20
|
+
const COUNT_ALL_FIELD = '_all';
|
|
21
|
+
/** The aggregation operators applicable to a field, per kind of value. */
|
|
22
|
+
const OPERATORS = {
|
|
23
|
+
numeric: ['count', 'sum', 'avg', 'min', 'max', ...BOUNDARY_OPERATORS],
|
|
24
|
+
date: ['count', 'min', 'max', ...BOUNDARY_OPERATORS]
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Resolves an aggregation date range from its ISO string bounds and splits it into equally sized periods.
|
|
28
|
+
*
|
|
29
|
+
* @param {string} [from] - The ISO date string of the range start. Defaults to seven days before the range end.
|
|
30
|
+
* @param {string} [to] - The ISO date string of the range end. Defaults to the current date and time.
|
|
31
|
+
* @param {number} [step] - The size of a single period in units of the automatically selected time unit. If not
|
|
32
|
+
* provided, one unit is used and the unit is derived from the range length.
|
|
33
|
+
* @param {boolean} [safeIncrement=true] - Whether the period increments use the derived time unit and stay consistent
|
|
34
|
+
* across daylight saving time changes. When false, the step is interpreted in seconds.
|
|
35
|
+
* @return {AggregationPeriods} The resolved range bounds and one range per period, each labeled with its median date.
|
|
36
|
+
*/
|
|
37
|
+
function buildAggregationPeriods(from, to, step, safeIncrement = true) {
|
|
38
|
+
const toDate = (0, date_util_1.parseIsoDate)(to ?? new Date().toISOString());
|
|
39
|
+
const fromDate = from ? (0, date_util_1.parseIsoDate)(from) : (0, date_util_1.addDays)(toDate, -7);
|
|
40
|
+
const iterator = makeAggregationIterator(fromDate, toDate, step, safeIncrement);
|
|
41
|
+
const ranges = [];
|
|
42
|
+
let currentDate = new Date(fromDate.getTime());
|
|
43
|
+
while (currentDate < toDate) {
|
|
44
|
+
const date = currentDate;
|
|
45
|
+
const median = iterator.addPeriod(date, (iterator.step - 1) / 2);
|
|
46
|
+
currentDate = iterator.addPeriod(currentDate, iterator.step);
|
|
47
|
+
ranges.push({ from: date, to: currentDate, median });
|
|
48
|
+
}
|
|
49
|
+
return { fromDate, toDate, ranges };
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Maps an aggregation selection to the operations that resolve it, splitting the operators the database aggregates in
|
|
53
|
+
* a single query from the `first` and `last` operators, which read the boundary records of the range instead. Every
|
|
54
|
+
* field is validated against the model it is aggregated on, and every operator against the kind of value its field
|
|
55
|
+
* holds, since only the numeric fields can be summed and averaged.
|
|
56
|
+
*
|
|
57
|
+
* @param {AggregateSelect<Object>} select - The operations to perform per field, with the operators of a field nested
|
|
58
|
+
* inside it. The operators set to false are left out.
|
|
59
|
+
* @param {string} resourceName - The name of the model the selection is aggregated on.
|
|
60
|
+
* @return {AggregationOperations} The Prisma aggregation arguments, keyed by the prefixed operator name with the
|
|
61
|
+
* selected fields nested inside, together with the fields read off the earliest and the latest record of the range.
|
|
62
|
+
* The arguments always count the records of the range when a boundary field was selected, so the boundary queries can
|
|
63
|
+
* be skipped for the empty ranges.
|
|
64
|
+
* @throws {ResourceError} `RESOURCE_INVALID_AGGREGATE` if the selection is not an object, selects no operation at all, names a field the model
|
|
65
|
+
* cannot aggregate, or applies an operator the kind of value of its field does not support.
|
|
66
|
+
*/
|
|
67
|
+
function mapAggregationSelect(select, resourceName) {
|
|
68
|
+
if (!(0, common_1.isPlainObject)(select)) {
|
|
69
|
+
throw new common_1.ResourceError(common_1.ErrorCode.ResourceInvalidAggregate, `${resourceName} aggregate select must be an object of fields to aggregate`, { model: resourceName });
|
|
70
|
+
}
|
|
71
|
+
const model = (0, core_1.injectModel)(resourceName);
|
|
72
|
+
const fieldTypes = new Map(aggregateFields(model));
|
|
73
|
+
const operations = {
|
|
74
|
+
aggregate: {},
|
|
75
|
+
first: [],
|
|
76
|
+
last: []
|
|
77
|
+
};
|
|
78
|
+
for (const [field, operators] of Object.entries(select)) {
|
|
79
|
+
const fieldType = fieldTypes.get(field);
|
|
80
|
+
if (!fieldType) {
|
|
81
|
+
throw new common_1.ResourceError(common_1.ErrorCode.ResourceInvalidAggregate, `Cannot aggregate the '${field}' field, it is not a numeric or date field of the ${model.name} model`, { model: model.name, field });
|
|
82
|
+
}
|
|
83
|
+
if (!(0, common_1.isPlainObject)(operators)) {
|
|
84
|
+
throw new common_1.ResourceError(common_1.ErrorCode.ResourceInvalidAggregate, `Cannot aggregate the '${field}' field, its value must be an object of aggregation operators`, { model: model.name, field });
|
|
85
|
+
}
|
|
86
|
+
for (const [operator, enabled] of Object.entries(operators)) {
|
|
87
|
+
if (!OPERATORS[fieldType].includes(operator)) {
|
|
88
|
+
throw new common_1.ResourceError(common_1.ErrorCode.ResourceInvalidAggregate, `Cannot apply the '${operator}' operator to the ${fieldType} field '${field}', ` +
|
|
89
|
+
`expected one of: ${OPERATORS[fieldType].join(', ')}`, { model: model.name, field, operator });
|
|
90
|
+
}
|
|
91
|
+
if (!enabled) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (isBoundaryOperator(operator)) {
|
|
95
|
+
operations[operator].push(field);
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
(0, common_1.setValue)(operations.aggregate, `_${operator}.${field}`, true);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const hasBoundary = operations.first.length + operations.last.length > 0;
|
|
103
|
+
if (Object.keys(operations.aggregate).length === 0 && !hasBoundary) {
|
|
104
|
+
throw new common_1.ResourceError(common_1.ErrorCode.ResourceInvalidAggregate, `${resourceName} aggregate requires at least one field with a selected aggregation operator`, { model: resourceName });
|
|
105
|
+
}
|
|
106
|
+
// The record count of a range decides whether its boundary records have to be
|
|
107
|
+
// read at all, and it is also the only aggregation left to perform when the
|
|
108
|
+
// selection holds nothing but the boundary operators
|
|
109
|
+
if (hasBoundary) {
|
|
110
|
+
(0, common_1.setValue)(operations.aggregate, `_count.${COUNT_ALL_FIELD}`, true);
|
|
111
|
+
}
|
|
112
|
+
return operations;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Maps a Prisma aggregation result back to the response format by swapping the operator and field nesting, so a
|
|
116
|
+
* `{ _count: { views: 3 } }` result becomes the `{ views: { count: 3 } }` response value. The boundary record values
|
|
117
|
+
* are mapped the same way when they are merged into the result under their own `_first` and `_last` keys.
|
|
118
|
+
*
|
|
119
|
+
* @param {Object} result - The Prisma aggregation result, keyed by the prefixed operator name with the aggregated
|
|
120
|
+
* fields nested inside.
|
|
121
|
+
* @return {AggregateValue<Object>} The aggregated values keyed by field name, with the operator results nested inside.
|
|
122
|
+
* The record count of the range is left out, since it counts no field of its own.
|
|
123
|
+
*/
|
|
124
|
+
function mapAggregationResult(result) {
|
|
125
|
+
const aggregationMap = {};
|
|
126
|
+
for (const operator in result) {
|
|
127
|
+
const fields = result[operator];
|
|
128
|
+
for (const field in fields) {
|
|
129
|
+
if (field === COUNT_ALL_FIELD) {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
(0, common_1.setValue)(aggregationMap, `${field}.${operator.substring(1)}`, fields[field]);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return aggregationMap;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Reads the `first` and `last` values of an aggregated range off its boundary records. The earliest and the latest
|
|
139
|
+
* record of the range are looked up by the same date field the range is sliced by, with the record id breaking the
|
|
140
|
+
* ties, and only the selected fields are read off them. The values of an empty range are resolved to null without
|
|
141
|
+
* querying for them, so a range the aggregation already counted as empty costs nothing.
|
|
142
|
+
*
|
|
143
|
+
* @param {ResourceClient} client - The model client the boundary records are read with, which is the transaction
|
|
144
|
+
* client of the aggregation.
|
|
145
|
+
* @param {Object} where - The database query of the aggregated range, applied unchanged to the boundary lookups.
|
|
146
|
+
* @param {string} dateField - The date field the range is sliced by, which the boundary records are ordered by.
|
|
147
|
+
* @param {AggregationOperations} operations - The operations holding the fields to read off each boundary record.
|
|
148
|
+
* @param {number} [recordCount] - The number of records in the range, used to skip the lookups of an empty range. When
|
|
149
|
+
* omitted, the lookups are always performed.
|
|
150
|
+
* @return {Promise<Object>} The boundary values keyed by the prefixed operator name (`_first` and `_last`) with the
|
|
151
|
+
* read fields nested inside, ready to be merged into the aggregation result of the range. Empty when no boundary field
|
|
152
|
+
* was selected.
|
|
153
|
+
*/
|
|
154
|
+
async function readAggregationBoundaries(client, where, dateField, operations, recordCount) {
|
|
155
|
+
const boundaries = {};
|
|
156
|
+
const readBoundary = async (operator, direction) => {
|
|
157
|
+
const fields = operations[operator];
|
|
158
|
+
if (fields.length === 0) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
// An empty range holds no record to read the values off, and its fields are
|
|
162
|
+
// resolved to null the same way the database aggregations resolve theirs
|
|
163
|
+
const record = recordCount === 0
|
|
164
|
+
? undefined
|
|
165
|
+
: await client.findFirst({
|
|
166
|
+
where,
|
|
167
|
+
// The id breaks the ties of the records sharing the same date, so
|
|
168
|
+
// the boundary of a range stays the same across identical requests
|
|
169
|
+
orderBy: [{ [dateField]: direction }, { id: direction }],
|
|
170
|
+
select: Object.fromEntries(fields.map((field) => [field, true]))
|
|
171
|
+
});
|
|
172
|
+
boundaries[`_${operator}`] = Object.fromEntries(fields.map((field) => [field, record?.[field] ?? null]));
|
|
173
|
+
};
|
|
174
|
+
await Promise.all([
|
|
175
|
+
readBoundary('first', 'asc'),
|
|
176
|
+
readBoundary('last', 'desc')
|
|
177
|
+
]);
|
|
178
|
+
return boundaries;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Reads the record count a Prisma aggregation result holds for its range, as counted by the `_all` selection that
|
|
182
|
+
* {@link mapAggregationSelect} adds whenever a boundary field is selected.
|
|
183
|
+
*
|
|
184
|
+
* @param {Object} result - The Prisma aggregation result to read the count off.
|
|
185
|
+
* @return {number|undefined} The number of records in the range, or undefined when the result does not count them.
|
|
186
|
+
*/
|
|
187
|
+
function aggregationRecordCount(result) {
|
|
188
|
+
return (0, common_1.isPlainObject)(result?._count)
|
|
189
|
+
? result._count[COUNT_ALL_FIELD]
|
|
190
|
+
: undefined;
|
|
191
|
+
}
|
|
192
|
+
/** Determines whether an operator reads a boundary record instead of aggregating the range. */
|
|
193
|
+
function isBoundaryOperator(operator) {
|
|
194
|
+
return BOUNDARY_OPERATORS.includes(operator);
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Validates the date field the aggregated range is applied on against the model it is aggregated on.
|
|
198
|
+
*
|
|
199
|
+
* @param {string} dateField - The name of the date field to validate.
|
|
200
|
+
* @param {string} resourceName - The name of the model the range is applied on.
|
|
201
|
+
* @return {string} The validated date field name.
|
|
202
|
+
* @throws {ResourceError} `RESOURCE_INVALID_AGGREGATE` if the model has no date field under that name.
|
|
203
|
+
*/
|
|
204
|
+
function checkAggregationDateField(dateField, resourceName) {
|
|
205
|
+
const model = (0, core_1.injectModel)(resourceName);
|
|
206
|
+
const dateFields = aggregateFields(model)
|
|
207
|
+
.filter(([, type]) => type === 'date')
|
|
208
|
+
.map(([field]) => field);
|
|
209
|
+
if (!dateFields.includes(dateField)) {
|
|
210
|
+
throw new common_1.ResourceError(common_1.ErrorCode.ResourceInvalidAggregate, `Cannot aggregate over the '${dateField}' field, it is not a date field of the ${model.name} model` +
|
|
211
|
+
(dateFields.length > 0
|
|
212
|
+
? `, expected one of: ${dateFields.join(', ')}`
|
|
213
|
+
: ''), { model: model.name, field: dateField });
|
|
214
|
+
}
|
|
215
|
+
return dateField;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Lists the aggregatable fields of a resource model together with the kind of value they hold. The id is aggregatable
|
|
219
|
+
* when it is numeric, the audit fields follow the audit configuration of the model, and the scalars are aggregatable
|
|
220
|
+
* when they hold a single numeric or date value and are not hidden. The virtual fields are never aggregatable, since
|
|
221
|
+
* they have no column of their own.
|
|
222
|
+
*
|
|
223
|
+
* @param {ResourceModel} model - The resource model whose fields are listed.
|
|
224
|
+
* @return {Array} The name and kind (`numeric` or `date`) of every aggregatable field, in the order the model declares
|
|
225
|
+
* them.
|
|
226
|
+
*/
|
|
227
|
+
function aggregateFields(model) {
|
|
228
|
+
const fields = [];
|
|
229
|
+
if ((model.config.id?.type ?? 'int') !== 'string') {
|
|
230
|
+
fields.push(['id', 'numeric']);
|
|
231
|
+
}
|
|
232
|
+
const auditFields = {
|
|
233
|
+
updatedAt: true,
|
|
234
|
+
createdAt: true,
|
|
235
|
+
createdById: true,
|
|
236
|
+
...(model.config.audit ?? {})
|
|
237
|
+
};
|
|
238
|
+
for (const [fieldName, included] of Object.entries(auditFields)) {
|
|
239
|
+
if (included) {
|
|
240
|
+
fields.push([
|
|
241
|
+
fieldName,
|
|
242
|
+
fieldName === 'createdById' ? 'numeric' : 'date'
|
|
243
|
+
]);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
for (const [fieldName, scalar] of Object.entries(model.config.scalars ?? {})) {
|
|
247
|
+
if (scalar.hidden || scalar.array) {
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
if (NUMERIC_TYPES.includes(scalar.type)) {
|
|
251
|
+
fields.push([fieldName, 'numeric']);
|
|
252
|
+
}
|
|
253
|
+
else if (DATE_TYPES.includes(scalar.type)) {
|
|
254
|
+
fields.push([fieldName, 'date']);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return fields;
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Builds the period iterator used to split an aggregation date range into equally sized periods. When no step is
|
|
261
|
+
* provided, a step of one is used with the time unit derived from the range length, and the returned increment
|
|
262
|
+
* function compensates for daylight saving time offset changes so every period keeps the time zone offset of its
|
|
263
|
+
* start date.
|
|
264
|
+
*
|
|
265
|
+
* @param {Date} fromDate - The start of the aggregated date range.
|
|
266
|
+
* @param {Date} toDate - The end of the aggregated date range.
|
|
267
|
+
* @param {number} [step] - The size of a single period in units of the selected time unit. If not provided, a step of
|
|
268
|
+
* one is used with the time unit derived from the range length (seconds up to a minute, minutes up to an hour, hours
|
|
269
|
+
* up to a day, days up to a month, months up to a year, and years beyond that).
|
|
270
|
+
* @param {boolean} [safeIncrement=true] - Whether the increments use the derived time unit. When false, the step is
|
|
271
|
+
* interpreted in seconds.
|
|
272
|
+
* @return {{addPeriod: PeriodIncrementFn, step: number}} The iterator holding the resolved step amount and the
|
|
273
|
+
* `addPeriod` function that adds a number of periods to a date while preserving the time zone offset of that date.
|
|
274
|
+
*/
|
|
275
|
+
function makeAggregationIterator(fromDate, toDate, step, safeIncrement = true) {
|
|
276
|
+
let stepAmount = step;
|
|
277
|
+
let incrementFn = date_util_1.addSeconds;
|
|
278
|
+
if (!stepAmount) {
|
|
279
|
+
const diffInSeconds = (0, date_util_1.differenceInSeconds)(toDate, fromDate);
|
|
280
|
+
const diffInMonths = (0, date_util_1.differenceInMonths)(toDate, fromDate);
|
|
281
|
+
const diffInYears = (0, date_util_1.differenceInYears)(toDate, fromDate);
|
|
282
|
+
stepAmount = 1;
|
|
283
|
+
// 1-second step if the difference is less than or equal to 1 minute
|
|
284
|
+
if (diffInSeconds <= 60) {
|
|
285
|
+
incrementFn = date_util_1.addSeconds;
|
|
286
|
+
}
|
|
287
|
+
// 1-minute step if the difference is less than or equal to 1 hour
|
|
288
|
+
else if (diffInSeconds <= 3600) {
|
|
289
|
+
incrementFn = date_util_1.addMinutes;
|
|
290
|
+
}
|
|
291
|
+
// 1-hour step if the difference is less than or equal to 1 day
|
|
292
|
+
else if (diffInSeconds <= 86400) {
|
|
293
|
+
incrementFn = date_util_1.addHours;
|
|
294
|
+
}
|
|
295
|
+
// 1-day step if the difference is less than or equal to 1 month
|
|
296
|
+
else if (diffInMonths <= 1) {
|
|
297
|
+
incrementFn = date_util_1.addDays;
|
|
298
|
+
}
|
|
299
|
+
// 1-month step if the difference is less than or equal to 1 year
|
|
300
|
+
else if (diffInYears <= 1) {
|
|
301
|
+
incrementFn = date_util_1.addMonths;
|
|
302
|
+
}
|
|
303
|
+
// 1-year step if the difference is equal to 1 year or more
|
|
304
|
+
else {
|
|
305
|
+
incrementFn = date_util_1.addYears;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
// A higher-order function that adjusts date increments to account for
|
|
309
|
+
// changes in daylight saving time (DST). When incrementing dates in time
|
|
310
|
+
// zones that observe DST, this function ensures that the resulting date
|
|
311
|
+
// remains consistent with the original date's time zone offset.
|
|
312
|
+
const dstAgnosticFn = (fn) => {
|
|
313
|
+
return (date, amount) => {
|
|
314
|
+
const endDate = fn(date, amount);
|
|
315
|
+
return (0, date_util_1.addMinutes)(endDate, date.getTimezoneOffset() - endDate.getTimezoneOffset());
|
|
316
|
+
};
|
|
317
|
+
};
|
|
318
|
+
return {
|
|
319
|
+
addPeriod: dstAgnosticFn(safeIncrement ? incrementFn : date_util_1.addSeconds),
|
|
320
|
+
step: stepAmount
|
|
321
|
+
};
|
|
322
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { QueryResponse, ResourceId } from '@appweaver/common';
|
|
2
|
+
/** The cursors a query response carries, kept in step with the response contract. */
|
|
3
|
+
export type PageCursors = Pick<QueryResponse<unknown>, 'nextCursor' | 'prevCursor'>;
|
|
4
|
+
/** A decoded cursor, holding the record it points at and the direction to page in. */
|
|
5
|
+
export type DecodedCursor = {
|
|
6
|
+
id: ResourceId;
|
|
7
|
+
/** True when the cursor pages towards the preceding records */
|
|
8
|
+
backward: boolean;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Builds the fingerprint identifying the query a cursor belongs to. A cursor
|
|
12
|
+
* only yields the intended records while the resource, the query, and the order
|
|
13
|
+
* stay the same, so it carries the fingerprint and is rejected on mismatch.
|
|
14
|
+
*
|
|
15
|
+
* @param {string} resourceName - The name of the queried model.
|
|
16
|
+
* @param {Object} query - The mapped database query the cursor was issued for.
|
|
17
|
+
* @param {Object[]} orderBy - The mapped order entries the cursor was issued for.
|
|
18
|
+
* @return {string} The fingerprint of the query.
|
|
19
|
+
*/
|
|
20
|
+
export declare function queryFingerprint(resourceName: string, query: any, orderBy: any[]): string;
|
|
21
|
+
/**
|
|
22
|
+
* Encodes the cursor of a page adjacent to a query result. The direction belongs
|
|
23
|
+
* to the cursor rather than to the request, so a caller cannot pair one with a
|
|
24
|
+
* direction it was not issued for.
|
|
25
|
+
*
|
|
26
|
+
* @param {ResourceId} id - The primary key of the record the page continues from.
|
|
27
|
+
* @param {string} fingerprint - The fingerprint of the query, as built by
|
|
28
|
+
* {@link queryFingerprint}.
|
|
29
|
+
* @param {boolean} [backward] - Whether the cursor pages towards the preceding
|
|
30
|
+
* records.
|
|
31
|
+
* @return {string} The encoded cursor.
|
|
32
|
+
*/
|
|
33
|
+
export declare function encodeCursor(id: ResourceId, fingerprint: string, backward?: boolean): string;
|
|
34
|
+
/**
|
|
35
|
+
* Builds the cursors of the pages adjacent to a returned page, each addressing
|
|
36
|
+
* the record of this page it continues from. A page that has no neighbour in a
|
|
37
|
+
* direction, and an empty page, yield null rather than an absent cursor.
|
|
38
|
+
*
|
|
39
|
+
* @param {Object[]} resources - The records of the returned page, in the order
|
|
40
|
+
* they were queried in.
|
|
41
|
+
* @param {string} fingerprint - The fingerprint of the query the page belongs to.
|
|
42
|
+
* @param {boolean} hasNext - Whether a page follows the returned one.
|
|
43
|
+
* @param {boolean} hasPrev - Whether a page precedes the returned one.
|
|
44
|
+
* @return {PageCursors} The cursors of the existing adjacent pages.
|
|
45
|
+
*/
|
|
46
|
+
export declare function pageCursors<T>(resources: T[], fingerprint: string, hasNext: boolean, hasPrev: boolean): PageCursors;
|
|
47
|
+
/**
|
|
48
|
+
* Decodes the cursor a query request carries, resolving the record the page
|
|
49
|
+
* continues from and the direction it runs in.
|
|
50
|
+
*
|
|
51
|
+
* @param {string} [cursor] - The cursor of the request, which may be the null
|
|
52
|
+
* one of a page that does not exist.
|
|
53
|
+
* @param {string} fingerprint - The fingerprint of the query the cursor is used
|
|
54
|
+
* on, as built by {@link queryFingerprint}.
|
|
55
|
+
* @return {DecodedCursor | undefined} The decoded cursor, or `undefined` when the
|
|
56
|
+
* request carries none.
|
|
57
|
+
* @throws {ResourceError} `RESOURCE_INVALID_CURSOR` if the cursor is malformed, or was issued for another
|
|
58
|
+
* resource, filter, or sort order.
|
|
59
|
+
*/
|
|
60
|
+
export declare function decodeCursor(cursor: string | null | undefined, fingerprint: string): DecodedCursor | undefined;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.queryFingerprint = queryFingerprint;
|
|
4
|
+
exports.encodeCursor = encodeCursor;
|
|
5
|
+
exports.pageCursors = pageCursors;
|
|
6
|
+
exports.decodeCursor = decodeCursor;
|
|
7
|
+
const common_1 = require("@appweaver/common");
|
|
8
|
+
/**
|
|
9
|
+
* Builds the fingerprint identifying the query a cursor belongs to. A cursor
|
|
10
|
+
* only yields the intended records while the resource, the query, and the order
|
|
11
|
+
* stay the same, so it carries the fingerprint and is rejected on mismatch.
|
|
12
|
+
*
|
|
13
|
+
* @param {string} resourceName - The name of the queried model.
|
|
14
|
+
* @param {Object} query - The mapped database query the cursor was issued for.
|
|
15
|
+
* @param {Object[]} orderBy - The mapped order entries the cursor was issued for.
|
|
16
|
+
* @return {string} The fingerprint of the query.
|
|
17
|
+
*/
|
|
18
|
+
function queryFingerprint(resourceName, query, orderBy) {
|
|
19
|
+
const serialized = stableStringify([resourceName, query ?? {}, orderBy]);
|
|
20
|
+
return (0, common_1.makeHash)(serialized, 'sha256', 'base64url').slice(0, 16);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Encodes the cursor of a page adjacent to a query result. The direction belongs
|
|
24
|
+
* to the cursor rather than to the request, so a caller cannot pair one with a
|
|
25
|
+
* direction it was not issued for.
|
|
26
|
+
*
|
|
27
|
+
* @param {ResourceId} id - The primary key of the record the page continues from.
|
|
28
|
+
* @param {string} fingerprint - The fingerprint of the query, as built by
|
|
29
|
+
* {@link queryFingerprint}.
|
|
30
|
+
* @param {boolean} [backward] - Whether the cursor pages towards the preceding
|
|
31
|
+
* records.
|
|
32
|
+
* @return {string} The encoded cursor.
|
|
33
|
+
*/
|
|
34
|
+
function encodeCursor(id, fingerprint, backward = false) {
|
|
35
|
+
const payload = { i: id, f: fingerprint };
|
|
36
|
+
if (backward) {
|
|
37
|
+
payload.b = true;
|
|
38
|
+
}
|
|
39
|
+
return Buffer.from(JSON.stringify(payload), 'utf8').toString('base64url');
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Builds the cursors of the pages adjacent to a returned page, each addressing
|
|
43
|
+
* the record of this page it continues from. A page that has no neighbour in a
|
|
44
|
+
* direction, and an empty page, yield null rather than an absent cursor.
|
|
45
|
+
*
|
|
46
|
+
* @param {Object[]} resources - The records of the returned page, in the order
|
|
47
|
+
* they were queried in.
|
|
48
|
+
* @param {string} fingerprint - The fingerprint of the query the page belongs to.
|
|
49
|
+
* @param {boolean} hasNext - Whether a page follows the returned one.
|
|
50
|
+
* @param {boolean} hasPrev - Whether a page precedes the returned one.
|
|
51
|
+
* @return {PageCursors} The cursors of the existing adjacent pages.
|
|
52
|
+
*/
|
|
53
|
+
function pageCursors(resources, fingerprint, hasNext, hasPrev) {
|
|
54
|
+
// A queried record always carries its primary key, which a model type need
|
|
55
|
+
// not declare
|
|
56
|
+
const first = resources[0];
|
|
57
|
+
const last = resources[resources.length - 1];
|
|
58
|
+
if (!first || !last) {
|
|
59
|
+
return { nextCursor: null, prevCursor: null };
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
nextCursor: hasNext ? encodeCursor(last.id, fingerprint) : null,
|
|
63
|
+
prevCursor: hasPrev ? encodeCursor(first.id, fingerprint, true) : null
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Decodes the cursor a query request carries, resolving the record the page
|
|
68
|
+
* continues from and the direction it runs in.
|
|
69
|
+
*
|
|
70
|
+
* @param {string} [cursor] - The cursor of the request, which may be the null
|
|
71
|
+
* one of a page that does not exist.
|
|
72
|
+
* @param {string} fingerprint - The fingerprint of the query the cursor is used
|
|
73
|
+
* on, as built by {@link queryFingerprint}.
|
|
74
|
+
* @return {DecodedCursor | undefined} The decoded cursor, or `undefined` when the
|
|
75
|
+
* request carries none.
|
|
76
|
+
* @throws {ResourceError} `RESOURCE_INVALID_CURSOR` if the cursor is malformed, or was issued for another
|
|
77
|
+
* resource, filter, or sort order.
|
|
78
|
+
*/
|
|
79
|
+
function decodeCursor(cursor, fingerprint) {
|
|
80
|
+
if (!cursor) {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
let payload;
|
|
84
|
+
try {
|
|
85
|
+
payload = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8'));
|
|
86
|
+
}
|
|
87
|
+
catch (e) {
|
|
88
|
+
throw new common_1.ResourceError(common_1.ErrorCode.ResourceInvalidCursor, 'Invalid pagination cursor', {}, { cause: e });
|
|
89
|
+
}
|
|
90
|
+
if (!(0, common_1.isPlainObject)(payload) || payload.i === undefined) {
|
|
91
|
+
throw new common_1.ResourceError(common_1.ErrorCode.ResourceInvalidCursor, 'Invalid pagination cursor');
|
|
92
|
+
}
|
|
93
|
+
if (payload.f !== fingerprint) {
|
|
94
|
+
throw new common_1.ResourceError(common_1.ErrorCode.ResourceInvalidCursor, 'Pagination cursor does not match the filter and sort of this query');
|
|
95
|
+
}
|
|
96
|
+
return { id: payload.i, backward: payload.b === true };
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Serializes a value with its object keys in a stable order, so two equal
|
|
100
|
+
* queries fingerprint identically whatever order their properties arrived in.
|
|
101
|
+
*
|
|
102
|
+
* @param {any} value - The value to serialize.
|
|
103
|
+
* @return {string} The serialized value.
|
|
104
|
+
*/
|
|
105
|
+
function stableStringify(value) {
|
|
106
|
+
if ((0, common_1.isArray)(value)) {
|
|
107
|
+
return `[${value.map(stableStringify).join(',')}]`;
|
|
108
|
+
}
|
|
109
|
+
if ((0, common_1.isPlainObject)(value)) {
|
|
110
|
+
const entries = Object.keys(value)
|
|
111
|
+
.sort()
|
|
112
|
+
.map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`);
|
|
113
|
+
return `{${entries.join(',')}}`;
|
|
114
|
+
}
|
|
115
|
+
return JSON.stringify(value) ?? 'null';
|
|
116
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parses an ISO 8601 date string. A date without a time is the local midnight of that day, a date and time without an
|
|
3
|
+
* offset is local, and one with an offset is that exact instant.
|
|
4
|
+
*
|
|
5
|
+
* @param {string} value - The ISO date string.
|
|
6
|
+
* @return {Date} The parsed date, invalid when the string is not a date.
|
|
7
|
+
*/
|
|
8
|
+
export declare function parseIsoDate(value: string): Date;
|
|
9
|
+
export declare function addSeconds(date: Date, amount: number): Date;
|
|
10
|
+
export declare function addMinutes(date: Date, amount: number): Date;
|
|
11
|
+
export declare function addHours(date: Date, amount: number): Date;
|
|
12
|
+
/** Adds whole local days, keeping the time of day across daylight saving time changes. */
|
|
13
|
+
export declare function addDays(date: Date, amount: number): Date;
|
|
14
|
+
/** Adds whole local months, clamping the day to the last day of a shorter month (January 31 plus one is February 28). */
|
|
15
|
+
export declare function addMonths(date: Date, amount: number): Date;
|
|
16
|
+
export declare function addYears(date: Date, amount: number): Date;
|
|
17
|
+
/** Returns the whole seconds between two dates, truncated toward zero. */
|
|
18
|
+
export declare function differenceInSeconds(laterDate: Date, earlierDate: Date): number;
|
|
19
|
+
/** Returns the full local months between two dates, negative when the first date is the earlier one. */
|
|
20
|
+
export declare function differenceInMonths(laterDate: Date, earlierDate: Date): number;
|
|
21
|
+
/** Returns the full local years between two dates, negative when the first date is the earlier one. */
|
|
22
|
+
export declare function differenceInYears(laterDate: Date, earlierDate: Date): number;
|