@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,400 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.mapRelationInclusions = mapRelationInclusions;
|
|
4
|
+
exports.mapRelationActions = mapRelationActions;
|
|
5
|
+
exports.unknownRelationFields = unknownRelationFields;
|
|
6
|
+
exports.missingRelationFields = missingRelationFields;
|
|
7
|
+
exports.createdByConnect = createdByConnect;
|
|
8
|
+
const common_1 = require("@appweaver/common");
|
|
9
|
+
const core_1 = require("@appweaver/core");
|
|
10
|
+
const delete_util_1 = require("./delete-util");
|
|
11
|
+
/** Levels of a self referencing relation read when the relation configures no
|
|
12
|
+
* `maxDepth`, i.e. the relation itself and nothing below it. */
|
|
13
|
+
const DEFAULT_RELATION_MAX_DEPTH = 1;
|
|
14
|
+
/**
|
|
15
|
+
* Builds the Prisma `include` clause for the relation and file fields of a resource model. A field is included when
|
|
16
|
+
* its configured output type allows it for the given action, or always when no action is specified, and the relations
|
|
17
|
+
* configured with an output count contribute to the `_count` selection instead.
|
|
18
|
+
*
|
|
19
|
+
* @param {string} resourceName - The name of the model whose relations are included.
|
|
20
|
+
* @param {ActionType} [action] - The action the inclusions are built for, matched against the configured output type
|
|
21
|
+
* of every field. When omitted, all fields whose output type is not `none` are included.
|
|
22
|
+
* @return {Object} The `include` clause mapping each included field to `true` or to its own nested `include` clause,
|
|
23
|
+
* extended with a `_count` selection for the relations configured with an output count.
|
|
24
|
+
*/
|
|
25
|
+
function mapRelationInclusions(resourceName, action) {
|
|
26
|
+
const inclusion = {};
|
|
27
|
+
const resourceModel = (0, core_1.injectModel)(resourceName);
|
|
28
|
+
const relationConfig = resourceModel.config.relations;
|
|
29
|
+
const fileConfig = resourceModel.config.files;
|
|
30
|
+
const relationModelProps = (0, common_1.extractSchemaProperties)(resourceModel.relationsModel);
|
|
31
|
+
const fileModelProps = (0, common_1.extractSchemaProperties)(resourceModel.filesModel);
|
|
32
|
+
// Add relation and file fields to the inclusion map if the include type is
|
|
33
|
+
// satisfied or the requested action is not specified. Also, add the count
|
|
34
|
+
// aggregation actions for relations if configured.
|
|
35
|
+
for (const key of Object.keys({
|
|
36
|
+
...relationModelProps,
|
|
37
|
+
...fileModelProps
|
|
38
|
+
})) {
|
|
39
|
+
const relationField = relationConfig?.[key] || fileConfig?.[key];
|
|
40
|
+
if (relationField?.output?.count) {
|
|
41
|
+
inclusion._count = inclusion._count ?? { select: {} };
|
|
42
|
+
// Soft deleted related records are not counted
|
|
43
|
+
inclusion._count.select[key] =
|
|
44
|
+
(0, delete_util_1.liveInclusionFilter)(relationField.model) ?? true;
|
|
45
|
+
}
|
|
46
|
+
// Check if the relation should be included based on the output type
|
|
47
|
+
if (shouldIncludeRelation(relationField?.output?.type, action)) {
|
|
48
|
+
inclusion[key] = buildNestedInclusion(relationField, key, action);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return inclusion;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Maps the relation and file fields of a write payload to the Prisma nested write actions. Bare key values and arrays
|
|
55
|
+
* of them are normalized to objects first, then every item is classified: items with an id and additional data become
|
|
56
|
+
* inline updates when the parent action is an update and inline updates are enabled for the relation, items with only
|
|
57
|
+
* an id are connected, and items without an id are created inline or matched through a connect-or-create when
|
|
58
|
+
* `input.uniqueKey` is configured. On an update action, the items of the current record that are absent from the new
|
|
59
|
+
* value, as well as the relations set to null, are disconnected, or deleted when `orphanRemoval` is configured.
|
|
60
|
+
* Relations that were not loaded on the current record are left untouched.
|
|
61
|
+
*
|
|
62
|
+
* @param {string} resourceName - The name of the model the write payload belongs to.
|
|
63
|
+
* @param {'create'|'update'} action - The write action the relation actions are mapped for. Inline updates and the
|
|
64
|
+
* removal of the relations absent from the new value are only applied on an update action.
|
|
65
|
+
* @param {Object} data - The sanitized write payload whose relation and file fields are mapped. The non-relation
|
|
66
|
+
* fields are skipped, except for the null values of the array scalar fields, which are mapped to an empty array on an
|
|
67
|
+
* update action and to undefined on a create action.
|
|
68
|
+
* @param {Object} [currentData] - The currently stored record with its relations loaded, required on an update action
|
|
69
|
+
* to determine which relations to disconnect or delete.
|
|
70
|
+
* @return {RelationActions} The nested write clause per relation field, mapping each one to its `connect`, `create`,
|
|
71
|
+
* `update`, `connectOrCreate`, `disconnect` and `delete` actions, or to undefined for the relations no action can be
|
|
72
|
+
* applied to.
|
|
73
|
+
* @throws {ResourceError} `RESOURCE_INVALID_RELATION` if an inline payload holds fields the related model does not
|
|
74
|
+
* accept for its action, an inline create payload is missing required fields, a record with an id carries data the
|
|
75
|
+
* relation cannot update inline, or the relation accepts no new records and an id was not provided.
|
|
76
|
+
*/
|
|
77
|
+
function mapRelationActions(resourceName, action, data, currentData) {
|
|
78
|
+
const relations = {};
|
|
79
|
+
const resourceModel = (0, core_1.injectModel)(resourceName);
|
|
80
|
+
const readModel = resourceModel.readModel;
|
|
81
|
+
const relationsModel = resourceModel.relationsModel;
|
|
82
|
+
const relationsConfig = resourceModel.config.relations;
|
|
83
|
+
for (const key in data) {
|
|
84
|
+
let value = data[key];
|
|
85
|
+
const relationSchema = (0, common_1.extractSchemaProperties)(relationsModel, key);
|
|
86
|
+
if (!relationSchema) {
|
|
87
|
+
// Set empty array or undefined value for null array type fields
|
|
88
|
+
if (value === null) {
|
|
89
|
+
if ((0, common_1.extractSchemaProperties)(readModel, key)?.type === 'array') {
|
|
90
|
+
relations[key] = action === 'update' ? [] : undefined;
|
|
91
|
+
}
|
|
92
|
+
else if (action === 'create') {
|
|
93
|
+
relations[key] = undefined;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// Skip mapping for non-relation fields
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const config = relationsConfig?.[key];
|
|
100
|
+
// The unique key is only used to match existing records for the
|
|
101
|
+
// connect-or-create action; connect and inline update always use the id
|
|
102
|
+
const uniqueKey = config?.input?.allowCreate
|
|
103
|
+
? config?.input?.uniqueKey || 'id'
|
|
104
|
+
: 'id';
|
|
105
|
+
const isArrayType = relationSchema.type === 'array';
|
|
106
|
+
// Normalize array values to single values if a value type is not an array
|
|
107
|
+
if (!isArrayType && (0, common_1.isArray)(value)) {
|
|
108
|
+
value = value[0];
|
|
109
|
+
}
|
|
110
|
+
// Set null values to undefined value for create actions. On update action
|
|
111
|
+
// they will be returned as disconnected relations.
|
|
112
|
+
if (action === 'create' && value === null) {
|
|
113
|
+
relations[key] = undefined;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
// Normalize plain key values or arrays to object values
|
|
117
|
+
if (isArrayType) {
|
|
118
|
+
if ((0, common_1.isArray)(value) && !(0, common_1.isPlainObject)(value[0])) {
|
|
119
|
+
value = value.map((v) => ({
|
|
120
|
+
[uniqueKey]: v
|
|
121
|
+
}));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
else if (!(0, common_1.isObject)(value)) {
|
|
125
|
+
// Only bare key values are wrapped. The loose object check keeps
|
|
126
|
+
// null values untouched, so they still map to a disconnect action.
|
|
127
|
+
value = { [uniqueKey]: value };
|
|
128
|
+
}
|
|
129
|
+
// Classify every relation input item into its relation write action:
|
|
130
|
+
// items with an id and additional data become inline updates (on parent
|
|
131
|
+
// update requests with inline updates enabled), items with only an id are
|
|
132
|
+
// connected, and items without an id are created inline or matched with
|
|
133
|
+
// connect-or-create, both of which require `allowCreate`
|
|
134
|
+
if (value) {
|
|
135
|
+
const items = isArrayType && (0, common_1.isArray)(value) ? value : [value];
|
|
136
|
+
const createdBy = createdByConnect(config?.model ?? resourceName);
|
|
137
|
+
const actions = {
|
|
138
|
+
connect: [],
|
|
139
|
+
update: [],
|
|
140
|
+
create: [],
|
|
141
|
+
connectOrCreate: []
|
|
142
|
+
};
|
|
143
|
+
for (const item of items) {
|
|
144
|
+
if (!(0, common_1.isPlainObject)(item)) {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (item.id !== undefined && item.id !== null) {
|
|
148
|
+
const { id, ...updateData } = item;
|
|
149
|
+
const fields = Object.keys(updateData);
|
|
150
|
+
if (fields.length === 0) {
|
|
151
|
+
actions.connect.push({ id });
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
// The database updates relations only within an update action
|
|
155
|
+
if (action !== 'update' || !config?.input?.allowUpdate) {
|
|
156
|
+
throw new common_1.ResourceError(common_1.ErrorCode.ResourceInvalidRelation, `${resourceName} relation '${key}' does not accept inline updates, an existing record is connected by its id only`, { model: resourceName, relation: key, unknownFields: fields });
|
|
157
|
+
}
|
|
158
|
+
assertKnownFields(resourceName, key, unknownRelationFields(config?.model, 'update', updateData));
|
|
159
|
+
actions.update.push({ where: { id }, data: updateData });
|
|
160
|
+
}
|
|
161
|
+
else if (!config?.input?.allowCreate) {
|
|
162
|
+
// Without inline creation the related record must already exist,
|
|
163
|
+
// so it is created through its own endpoint first
|
|
164
|
+
throw new common_1.ResourceError(common_1.ErrorCode.ResourceInvalidRelation, `${resourceName} relation '${key}' does not accept new records, an id is required`, { model: resourceName, relation: key });
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
// A null id marks no existing record, it is not part of the data
|
|
168
|
+
const { id: _id, ...createData } = item;
|
|
169
|
+
assertKnownFields(resourceName, key, unknownRelationFields(config?.model, 'create', createData));
|
|
170
|
+
const missingFields = missingRelationFields(config?.model, createData);
|
|
171
|
+
if (missingFields.length > 0) {
|
|
172
|
+
throw new common_1.ResourceError(common_1.ErrorCode.ResourceInvalidRelation, `${resourceName} relation '${key}' is missing required fields: ${missingFields.join(', ')}`, { model: resourceName, relation: key, missingFields });
|
|
173
|
+
}
|
|
174
|
+
// A unique key matches an existing record before a new one is created
|
|
175
|
+
if (config?.input?.uniqueKey) {
|
|
176
|
+
actions.connectOrCreate.push({
|
|
177
|
+
where: { [uniqueKey]: item[uniqueKey] },
|
|
178
|
+
create: { ...createData, createdBy }
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
actions.create.push({ ...createData, createdBy });
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const relationActions = {};
|
|
187
|
+
for (const [actionName, actionItems] of Object.entries(actions)) {
|
|
188
|
+
if (actionItems.length > 0) {
|
|
189
|
+
relationActions[actionName] = isArrayType
|
|
190
|
+
? actionItems
|
|
191
|
+
: actionItems[0];
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
// An empty list maps to no action, rather than leaving the raw list in
|
|
195
|
+
// the write data, where the database rejects it
|
|
196
|
+
relations[key] =
|
|
197
|
+
Object.keys(relationActions).length > 0 ? relationActions : undefined;
|
|
198
|
+
}
|
|
199
|
+
// Map relation disconnects if keys are no longer present or the new
|
|
200
|
+
// value is null. Delete relations if orphanRemoval is set to true.
|
|
201
|
+
// Relations that are not set on the current resource are left untouched,
|
|
202
|
+
// so no relation action is applied for them.
|
|
203
|
+
if (action === 'update') {
|
|
204
|
+
const currentValue = currentData[key];
|
|
205
|
+
const removalMethod = config?.orphanRemoval ? 'delete' : 'disconnect';
|
|
206
|
+
if (currentValue && isArrayType) {
|
|
207
|
+
const newValueKeys = value?.map((v) => v[uniqueKey]) ?? [];
|
|
208
|
+
const currentValueKeys = currentValue
|
|
209
|
+
.filter((v) => newValueKeys.indexOf(v[uniqueKey]) === -1)
|
|
210
|
+
.map((v) => ({
|
|
211
|
+
[uniqueKey]: v[uniqueKey]
|
|
212
|
+
}));
|
|
213
|
+
if (currentValueKeys.length > 0) {
|
|
214
|
+
relations[key] = {
|
|
215
|
+
[removalMethod]: currentValueKeys,
|
|
216
|
+
...(relations[key] ?? {})
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
else if (currentValue && value === null) {
|
|
221
|
+
relations[key] = {
|
|
222
|
+
[removalMethod]: { [uniqueKey]: currentValue[uniqueKey] },
|
|
223
|
+
...(relations[key] ?? {})
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
else if (currentValue === undefined) {
|
|
227
|
+
// The relation was not loaded on the current record, so no relation
|
|
228
|
+
// action can be applied safely
|
|
229
|
+
relations[key] = undefined;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return relations;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Lists the fields of an inline relation payload the related model does not accept for the given action, so a payload
|
|
237
|
+
* written through a service call is rejected the same way the request validation rejects it.
|
|
238
|
+
*
|
|
239
|
+
* @param {string} [resourceName] - The name of the related model whose field restrictions are applied. When omitted,
|
|
240
|
+
* no fields are reported.
|
|
241
|
+
* @param {'create'|'update'} action - The write action the payload is checked for, selecting either the relation
|
|
242
|
+
* create or the relation update model of the related resource.
|
|
243
|
+
* @param {Object} data - The inline relation payload to check, without its `id` field.
|
|
244
|
+
* @return {string[]} The names of the payload fields the related model does not accept for the action, or an empty
|
|
245
|
+
* list if the related model declares no fields for it.
|
|
246
|
+
*/
|
|
247
|
+
function unknownRelationFields(resourceName, action, data) {
|
|
248
|
+
const resourceModel = resourceName
|
|
249
|
+
? (0, core_1.injectModel)(resourceName, false)
|
|
250
|
+
: undefined;
|
|
251
|
+
const writeModel = action === 'create'
|
|
252
|
+
? resourceModel?.relationCreateModel
|
|
253
|
+
: resourceModel?.relationUpdateModel;
|
|
254
|
+
const properties = (0, common_1.extractSchemaProperties)(writeModel);
|
|
255
|
+
if (!properties) {
|
|
256
|
+
return [];
|
|
257
|
+
}
|
|
258
|
+
return Object.keys(data).filter((name) => !(name in properties));
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Lists the fields the related model requires on creation that the given inline relation payload does not provide.
|
|
262
|
+
*
|
|
263
|
+
* @param {string} [resourceName] - The name of the related model whose required fields are checked. When omitted, no
|
|
264
|
+
* fields are reported as missing.
|
|
265
|
+
* @param {Object} data - The inline relation payload to check.
|
|
266
|
+
* @return {string[]} The names of the fields the related model requires on creation that the payload leaves
|
|
267
|
+
* undefined, or an empty list if the related model declares no create schema.
|
|
268
|
+
*/
|
|
269
|
+
function missingRelationFields(resourceName, data) {
|
|
270
|
+
const resourceModel = resourceName
|
|
271
|
+
? (0, core_1.injectModel)(resourceName, false)
|
|
272
|
+
: undefined;
|
|
273
|
+
const createModel = resourceModel?.relationCreateModel;
|
|
274
|
+
if (!createModel) {
|
|
275
|
+
return [];
|
|
276
|
+
}
|
|
277
|
+
const schema = createModel['$ref']
|
|
278
|
+
? createModel['$defs']?.[createModel['$ref']]
|
|
279
|
+
: createModel;
|
|
280
|
+
return (schema?.required ?? []).filter((name) => data[name] === undefined);
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Builds the connect action for the `createdBy` audit relation of a resource, pointing at the currently authenticated
|
|
284
|
+
* user. Returns undefined when the model does not audit the `createdById` field or when no user is authenticated.
|
|
285
|
+
*
|
|
286
|
+
* @param {string} resourceName - The name of the model the audit relation is built for.
|
|
287
|
+
* @return {{connect: {id: ResourceId}}|undefined} The connect action pointing at the id of the currently authenticated
|
|
288
|
+
* user, or undefined if the model does not audit the `createdById` field or no user is authenticated.
|
|
289
|
+
*/
|
|
290
|
+
function createdByConnect(resourceName) {
|
|
291
|
+
const resourceModel = (0, core_1.injectModel)(resourceName, false);
|
|
292
|
+
if (resourceModel?.config.audit?.createdById === false) {
|
|
293
|
+
return undefined;
|
|
294
|
+
}
|
|
295
|
+
const currentUser = (0, core_1.currentAuthUser)();
|
|
296
|
+
return currentUser
|
|
297
|
+
? {
|
|
298
|
+
connect: {
|
|
299
|
+
id: currentUser.id
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
: undefined;
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Resolves the inclusion value of a single relation field, walking both the levels a self referencing relation
|
|
306
|
+
* repeats itself for and the nested includes it configures. Returns `true` when the relation reads no further than
|
|
307
|
+
* itself for the given action, or a nested `include` clause otherwise.
|
|
308
|
+
*
|
|
309
|
+
* A relation whose related model holds the same relation again points back at its own model, so it repeats itself
|
|
310
|
+
* down the tree up to its configured `output.maxDepth`, letting a category carry its ancestors without every level
|
|
311
|
+
* being spelled out. An `output.include` entry naming that same relation configures the level itself and replaces
|
|
312
|
+
* the repetition.
|
|
313
|
+
*
|
|
314
|
+
* @param {RelationField} relationField - The configuration of the relation whose inclusion value is resolved, read
|
|
315
|
+
* from its `output.include` and `output.maxDepth` properties.
|
|
316
|
+
* @param {string} key - The field name the relation is declared under, matched against the related model to detect a
|
|
317
|
+
* relation pointing back at its own model.
|
|
318
|
+
* @param {ActionType} [action] - The action the inclusions are built for, matched against the configured output type
|
|
319
|
+
* of every nested relation.
|
|
320
|
+
* @param {number} [depth=1] - The level of the relation being resolved, counting the relation itself as the first.
|
|
321
|
+
* @return {boolean|Object} True if the relation reads no further than itself, or its nested `include` clause, and
|
|
322
|
+
* for a list relation of a soft deleted model, the `where` clause skipping the deleted records.
|
|
323
|
+
*/
|
|
324
|
+
function buildNestedInclusion(relationField, key, action, depth = 1) {
|
|
325
|
+
const nestedIncludeConfig = relationField?.output?.include;
|
|
326
|
+
const nestedInclusion = {};
|
|
327
|
+
// A file field carries no related model, so it reads no further than itself
|
|
328
|
+
const relatedModel = relationField?.model
|
|
329
|
+
? (0, core_1.injectModel)((0, common_1.capitalize)(relationField.model), false)
|
|
330
|
+
: undefined;
|
|
331
|
+
const selfRelation = selfReferencingRelation(relatedModel, key);
|
|
332
|
+
const maxDepth = relationField?.output?.maxDepth ?? DEFAULT_RELATION_MAX_DEPTH;
|
|
333
|
+
if (selfRelation && depth < maxDepth && !nestedIncludeConfig?.[key]) {
|
|
334
|
+
nestedInclusion[key] = buildNestedInclusion({ ...selfRelation, output: relationField.output }, key, action, depth + 1);
|
|
335
|
+
}
|
|
336
|
+
for (const [nestedKey, nestedOutput] of Object.entries(nestedIncludeConfig ?? {})) {
|
|
337
|
+
// Check if the nested relation should be included
|
|
338
|
+
if (!shouldIncludeRelation(nestedOutput?.type, action)) {
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
// The related model carries the field the nested include names, so each
|
|
342
|
+
// level is resolved against the model it actually belongs to
|
|
343
|
+
const nestedRelation = relatedModel?.config?.relations?.[nestedKey] ??
|
|
344
|
+
relatedModel?.config?.files?.[nestedKey];
|
|
345
|
+
nestedInclusion[nestedKey] = buildNestedInclusion({ ...nestedRelation, output: nestedOutput }, nestedKey, action);
|
|
346
|
+
}
|
|
347
|
+
// A list relation reads only the related records that are not soft deleted,
|
|
348
|
+
// while a single relation is cleared of them after it is read
|
|
349
|
+
const liveFilter = relatedModel && (0, common_1.isRelationArray)(relationField)
|
|
350
|
+
? (0, delete_util_1.liveInclusionFilter)(relatedModel.name)
|
|
351
|
+
: undefined;
|
|
352
|
+
const hasNested = Object.keys(nestedInclusion).length > 0;
|
|
353
|
+
if (!hasNested && !liveFilter) {
|
|
354
|
+
return true;
|
|
355
|
+
}
|
|
356
|
+
return {
|
|
357
|
+
...(liveFilter ?? {}),
|
|
358
|
+
...(hasNested ? { include: nestedInclusion } : {})
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Reads the relation a related model holds under the given field name, when it points back at that same model. It is
|
|
363
|
+
* the relation a self referencing field repeats itself through, i.e. the `parent` of the parent of a category.
|
|
364
|
+
*
|
|
365
|
+
* @param {ResourceModel} [model] - The related model the field is looked up on.
|
|
366
|
+
* @param {string} key - The field name of the relation.
|
|
367
|
+
* @return {RelationField | undefined} The relation of the model under that name when it references the model itself,
|
|
368
|
+
* undefined otherwise.
|
|
369
|
+
*/
|
|
370
|
+
function selfReferencingRelation(model, key) {
|
|
371
|
+
const relation = key ? model?.config?.relations?.[key] : undefined;
|
|
372
|
+
return relation && (0, common_1.capitalize)(relation.model) === model?.name
|
|
373
|
+
? relation
|
|
374
|
+
: undefined;
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Decides whether a relation with the given configured output type is included for the given action. The `none` type
|
|
378
|
+
* is never included, the `single` type is excluded from the query action, and the `multiple` type is only included on
|
|
379
|
+
* the query action or when no action is specified.
|
|
380
|
+
*
|
|
381
|
+
* @param {OutputType} [outputType] - The configured output type of the relation. When omitted, the relation is
|
|
382
|
+
* included for every action.
|
|
383
|
+
* @param {ActionType} [action] - The action the relation would be included for. When omitted, every output type
|
|
384
|
+
* except `none` is included.
|
|
385
|
+
* @return {boolean} True if the relation should be included, false otherwise.
|
|
386
|
+
*/
|
|
387
|
+
function shouldIncludeRelation(outputType, action) {
|
|
388
|
+
if (outputType === 'none') {
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
391
|
+
if (outputType === 'single' && action === 'query') {
|
|
392
|
+
return false;
|
|
393
|
+
}
|
|
394
|
+
return !(outputType === 'multiple' && action && action !== 'query');
|
|
395
|
+
}
|
|
396
|
+
function assertKnownFields(resourceName, relation, unknownFields) {
|
|
397
|
+
if (unknownFields.length > 0) {
|
|
398
|
+
throw new common_1.ResourceError(common_1.ErrorCode.ResourceInvalidRelation, `${resourceName} relation '${relation}' does not accept the fields: ${unknownFields.join(', ')}`, { model: resourceName, relation, unknownFields });
|
|
399
|
+
}
|
|
400
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { ActionType, QuerySort } from '@appweaver/common';
|
|
2
|
+
/**
|
|
3
|
+
* Maps a sort input to the ordered list of Prisma `orderBy` entries. The input is accepted either as a comma-separated
|
|
4
|
+
* field list, where a field prefixed with `-` is sorted in descending order, or as a (possibly nested) object of `asc`
|
|
5
|
+
* and `desc` field directions. Both forms support the same field paths: a scalar field of the
|
|
6
|
+
* model, a field of a to-one relation (`author.createdAt` or `{ author: { createdAt: 'desc' } }`), and the related
|
|
7
|
+
* record count of a to-many relation, given either as its count field (`tagsCount`) or as the relation itself
|
|
8
|
+
* (`tags`). The default `createdAt` sort is dropped when the model does not audit that field.
|
|
9
|
+
*
|
|
10
|
+
* @param {QuerySort} sort - The sort input to map, as a comma-separated field list or a sort object.
|
|
11
|
+
* @param {string} resourceName - The name of the model the sort is applied on.
|
|
12
|
+
* @param {ActionType} [action] - The action the sort is applied on, deciding which relations are included in the
|
|
13
|
+
* response and are therefore available to sort by. Defaults to the query action.
|
|
14
|
+
* @return {Object[]} One `orderBy` entry per field, in the order the fields were listed, each holding the single path
|
|
15
|
+
* of that field mapped to `asc` or `desc`. Two fields of the same relation get an entry each, since a database order
|
|
16
|
+
* entry accepts a single field path.
|
|
17
|
+
* @throws {ResourceError} `RESOURCE_INVALID_SORT` if a field does not exist on its model, cannot be sorted by, is given an unknown sort
|
|
18
|
+
* direction, or targets a relation that the action does not include in its response.
|
|
19
|
+
*/
|
|
20
|
+
export declare function mapSortValues(sort: QuerySort, resourceName: string, action?: ActionType): any[];
|
|
21
|
+
/**
|
|
22
|
+
* Maps a sort input the same way {@link mapSortValues} does, and terminates it with the primary key when no entry
|
|
23
|
+
* orders by it. Without a total order, records sharing every sort value can be skipped or repeated across pages.
|
|
24
|
+
*
|
|
25
|
+
* @param {QuerySort} sort - The sort input to map, as a comma-separated field list or a sort object.
|
|
26
|
+
* @param {string} resourceName - The name of the model the sort is applied on.
|
|
27
|
+
* @param {ActionType} [action] - The action the sort is applied on. Defaults to the query action.
|
|
28
|
+
* @return {Object[]} The `orderBy` entries, ending in a unique one.
|
|
29
|
+
* @throws {ResourceError} `RESOURCE_INVALID_SORT` under the same conditions as {@link mapSortValues}.
|
|
30
|
+
*/
|
|
31
|
+
export declare function mapStableSortValues(sort: QuerySort, resourceName: string, action?: ActionType): any[];
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.mapSortValues = mapSortValues;
|
|
4
|
+
exports.mapStableSortValues = mapStableSortValues;
|
|
5
|
+
const common_1 = require("@appweaver/common");
|
|
6
|
+
const core_1 = require("@appweaver/core");
|
|
7
|
+
const relation_util_1 = require("./relation-util");
|
|
8
|
+
/** The audit fields every model declares unless it opts out of them. */
|
|
9
|
+
const defaultAuditFields = {
|
|
10
|
+
updatedAt: true,
|
|
11
|
+
createdAt: true,
|
|
12
|
+
createdById: true
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Maps a sort input to the ordered list of Prisma `orderBy` entries. The input is accepted either as a comma-separated
|
|
16
|
+
* field list, where a field prefixed with `-` is sorted in descending order, or as a (possibly nested) object of `asc`
|
|
17
|
+
* and `desc` field directions. Both forms support the same field paths: a scalar field of the
|
|
18
|
+
* model, a field of a to-one relation (`author.createdAt` or `{ author: { createdAt: 'desc' } }`), and the related
|
|
19
|
+
* record count of a to-many relation, given either as its count field (`tagsCount`) or as the relation itself
|
|
20
|
+
* (`tags`). The default `createdAt` sort is dropped when the model does not audit that field.
|
|
21
|
+
*
|
|
22
|
+
* @param {QuerySort} sort - The sort input to map, as a comma-separated field list or a sort object.
|
|
23
|
+
* @param {string} resourceName - The name of the model the sort is applied on.
|
|
24
|
+
* @param {ActionType} [action] - The action the sort is applied on, deciding which relations are included in the
|
|
25
|
+
* response and are therefore available to sort by. Defaults to the query action.
|
|
26
|
+
* @return {Object[]} One `orderBy` entry per field, in the order the fields were listed, each holding the single path
|
|
27
|
+
* of that field mapped to `asc` or `desc`. Two fields of the same relation get an entry each, since a database order
|
|
28
|
+
* entry accepts a single field path.
|
|
29
|
+
* @throws {ResourceError} `RESOURCE_INVALID_SORT` if a field does not exist on its model, cannot be sorted by, is given an unknown sort
|
|
30
|
+
* direction, or targets a relation that the action does not include in its response.
|
|
31
|
+
*/
|
|
32
|
+
function mapSortValues(sort, resourceName, action = 'query') {
|
|
33
|
+
const inclusions = (0, relation_util_1.mapRelationInclusions)(resourceName, action);
|
|
34
|
+
const orderBy = [];
|
|
35
|
+
const mappedPaths = new Set();
|
|
36
|
+
for (const [path, direction] of sortEntries(sort)) {
|
|
37
|
+
const mappedPath = mapSortPath(resourceName, path, inclusions);
|
|
38
|
+
// Every field gets an entry of its own, since a database order entry holds
|
|
39
|
+
// a single field path, even when several of them share a relation. A field
|
|
40
|
+
// listed twice is already ordered by its first entry, so it is dropped
|
|
41
|
+
if (!mappedPath || mappedPaths.has(mappedPath)) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
mappedPaths.add(mappedPath);
|
|
45
|
+
orderBy.push((0, common_1.setValue)({}, mappedPath, direction));
|
|
46
|
+
}
|
|
47
|
+
return orderBy;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Maps a sort input the same way {@link mapSortValues} does, and terminates it with the primary key when no entry
|
|
51
|
+
* orders by it. Without a total order, records sharing every sort value can be skipped or repeated across pages.
|
|
52
|
+
*
|
|
53
|
+
* @param {QuerySort} sort - The sort input to map, as a comma-separated field list or a sort object.
|
|
54
|
+
* @param {string} resourceName - The name of the model the sort is applied on.
|
|
55
|
+
* @param {ActionType} [action] - The action the sort is applied on. Defaults to the query action.
|
|
56
|
+
* @return {Object[]} The `orderBy` entries, ending in a unique one.
|
|
57
|
+
* @throws {ResourceError} `RESOURCE_INVALID_SORT` under the same conditions as {@link mapSortValues}.
|
|
58
|
+
*/
|
|
59
|
+
function mapStableSortValues(sort, resourceName, action = 'query') {
|
|
60
|
+
const orderBy = mapSortValues(sort, resourceName, action);
|
|
61
|
+
if (!orderBy.some((entry) => 'id' in entry)) {
|
|
62
|
+
orderBy.push({ id: 'asc' });
|
|
63
|
+
}
|
|
64
|
+
return orderBy;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Flattens a sort input into the list of its field paths and directions, keeping the order the fields were declared
|
|
68
|
+
* in. String inputs are split on commas, with a `-` or `+` prefix selecting the direction, and object inputs are
|
|
69
|
+
* walked recursively, joining the nested keys with a dot.
|
|
70
|
+
*
|
|
71
|
+
* @param {QuerySort} sort - The sort input to flatten.
|
|
72
|
+
* @param {string} [parentPath] - The dot notation path of the object the walk descended from.
|
|
73
|
+
* @return {SortEntry[]} The flattened sort entries, each holding a dot notation field path and its direction.
|
|
74
|
+
* @throws {ResourceError} `RESOURCE_INVALID_SORT` if a sort value is neither a direction nor a nested object of directions.
|
|
75
|
+
*/
|
|
76
|
+
function sortEntries(sort, parentPath = '') {
|
|
77
|
+
if ((0, common_1.isString)(sort)) {
|
|
78
|
+
return sort
|
|
79
|
+
.split(',')
|
|
80
|
+
.map((part) => part.trim())
|
|
81
|
+
.filter((part) => part.length > 0 && part !== '-' && part !== '+')
|
|
82
|
+
.map((part) => [
|
|
83
|
+
part.replace(/^[-+]/, ''),
|
|
84
|
+
part.startsWith('-') ? 'desc' : 'asc'
|
|
85
|
+
]);
|
|
86
|
+
}
|
|
87
|
+
if (!(0, common_1.isPlainObject)(sort)) {
|
|
88
|
+
throw new common_1.ResourceError(common_1.ErrorCode.ResourceInvalidSort, `Invalid sort value${parentPath ? ` for the '${parentPath}' field` : ''}, ` +
|
|
89
|
+
'expected a field list string or a sort object', { field: parentPath || undefined });
|
|
90
|
+
}
|
|
91
|
+
const entries = [];
|
|
92
|
+
for (const [field, value] of Object.entries(sort)) {
|
|
93
|
+
if (value === undefined || value === null) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const path = parentPath ? `${parentPath}.${field}` : field;
|
|
97
|
+
if ((0, common_1.isPlainObject)(value)) {
|
|
98
|
+
entries.push(...sortEntries(value, path));
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (value !== 'asc' && value !== 'desc') {
|
|
102
|
+
throw new common_1.ResourceError(common_1.ErrorCode.ResourceInvalidSort, `Invalid sort direction '${value}' for the '${path}' field, expected 'asc' or 'desc'`, { field: path });
|
|
103
|
+
}
|
|
104
|
+
entries.push([path, value]);
|
|
105
|
+
}
|
|
106
|
+
return entries;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Resolves a single sort field path against the model it is applied on, validating every segment and rewriting the
|
|
110
|
+
* relation counts to their `_count` form. Relation segments are additionally checked against the relations the action
|
|
111
|
+
* includes in its response, since a relation that is not included cannot be sorted by.
|
|
112
|
+
*
|
|
113
|
+
* @param {string} resourceName - The name of the model the path starts at.
|
|
114
|
+
* @param {string} path - The dot notation field path to resolve.
|
|
115
|
+
* @param {Object} inclusions - The `include` clause of the action, as built by {@link mapRelationInclusions}.
|
|
116
|
+
* @return {string|undefined} The resolved dot notation path, or undefined when the field is the `createdAt` audit
|
|
117
|
+
* field of a model that does not audit it, in which case the sort entry is dropped.
|
|
118
|
+
* @throws {ResourceError} `RESOURCE_INVALID_SORT` if a segment does not exist on its model, cannot be sorted by, or targets a relation the
|
|
119
|
+
* action does not include in its response.
|
|
120
|
+
*/
|
|
121
|
+
function mapSortPath(resourceName, path, inclusions) {
|
|
122
|
+
const segments = path.split('.').filter((segment) => segment.length > 0);
|
|
123
|
+
const mappedSegments = [];
|
|
124
|
+
let modelName = resourceName;
|
|
125
|
+
let modelInclusions = inclusions;
|
|
126
|
+
for (const [index, field] of segments.entries()) {
|
|
127
|
+
const model = (0, core_1.injectModel)(modelName, false);
|
|
128
|
+
if (!model) {
|
|
129
|
+
throw new common_1.ResourceError(common_1.ErrorCode.ResourceInvalidSort, `Cannot sort by the '${path}' field, the '${modelName}' model is not loaded`, { model: modelName, field: path });
|
|
130
|
+
}
|
|
131
|
+
const isLast = index === segments.length - 1;
|
|
132
|
+
const relation = relationField(model, field);
|
|
133
|
+
// A scalar field ends the path, so it is the only segment a relation of the
|
|
134
|
+
// same name cannot shadow
|
|
135
|
+
if (!relation && isScalarField(model, field)) {
|
|
136
|
+
if (!isLast) {
|
|
137
|
+
throw new common_1.ResourceError(common_1.ErrorCode.ResourceInvalidSort, `Cannot sort by the '${path}' field, '${field}' is not a relation of the ${model.name} model`, { model: model.name, field: path });
|
|
138
|
+
}
|
|
139
|
+
// The default sort still names the createdAt field on the models that do
|
|
140
|
+
// not audit it, so the entry is dropped instead of rejected
|
|
141
|
+
if (field === 'createdAt' && model.config.audit?.createdAt === false) {
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
mappedSegments.push(field);
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
// A count field sorts by the number of related records, which the database
|
|
148
|
+
// orders by through the _count aggregation of the relation
|
|
149
|
+
if (!relation && isLast && (0, common_1.isCountField)(field)) {
|
|
150
|
+
const countedField = field.slice(0, -'Count'.length);
|
|
151
|
+
const countedRelation = relationField(model, countedField);
|
|
152
|
+
if (countedRelation && isFieldArray(countedRelation)) {
|
|
153
|
+
mappedSegments.push(countedField, '_count');
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (!relation) {
|
|
158
|
+
throw new common_1.ResourceError(common_1.ErrorCode.ResourceInvalidSort, `Cannot sort by the '${path}' field, '${field}' is not a sortable field of the ${model.name} model`, { model: model.name, field: path });
|
|
159
|
+
}
|
|
160
|
+
// A to-many relation holds no single value to sort by, so it is sorted by
|
|
161
|
+
// the number of its related records instead
|
|
162
|
+
if (isFieldArray(relation)) {
|
|
163
|
+
if (!isLast) {
|
|
164
|
+
throw new common_1.ResourceError(common_1.ErrorCode.ResourceInvalidSort, `Cannot sort by the '${path}' field, the '${field}' relation holds a list of records, ` +
|
|
165
|
+
`sort by the '${(0, common_1.countFieldName)(field)}' field instead`, { model: model.name, field: path });
|
|
166
|
+
}
|
|
167
|
+
mappedSegments.push(field, '_count');
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
if (isLast) {
|
|
171
|
+
throw new common_1.ResourceError(common_1.ErrorCode.ResourceInvalidSort, `Cannot sort by the '${path}' field, the '${field}' relation requires a nested field to sort by`, { model: model.name, field: path });
|
|
172
|
+
}
|
|
173
|
+
const inclusion = modelInclusions?.[field];
|
|
174
|
+
if (!inclusion) {
|
|
175
|
+
throw new common_1.ResourceError(common_1.ErrorCode.ResourceInvalidSort, `Cannot sort by the '${path}' field, the '${field}' relation is not included in the response`, { model: model.name, field: path });
|
|
176
|
+
}
|
|
177
|
+
mappedSegments.push(field);
|
|
178
|
+
modelName = (0, common_1.capitalize)(relatedModelName(model, field));
|
|
179
|
+
modelInclusions = (0, common_1.isPlainObject)(inclusion) ? inclusion.include : undefined;
|
|
180
|
+
}
|
|
181
|
+
return mappedSegments.join('.');
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Reads the relation or file field configuration of a model field.
|
|
185
|
+
*
|
|
186
|
+
* @param {ResourceModel} model - The model the field belongs to.
|
|
187
|
+
* @param {string} field - The name of the field to read.
|
|
188
|
+
* @return {RelationField|FileField|undefined} The relation or file configuration of the field, or undefined when the
|
|
189
|
+
* field is not a relation.
|
|
190
|
+
*/
|
|
191
|
+
function relationField(model, field) {
|
|
192
|
+
return model.config.relations?.[field] ?? model.config.files?.[field];
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Resolves the model name a relation or file field of a model points at.
|
|
196
|
+
*
|
|
197
|
+
* @param {ResourceModel} model - The model the relation belongs to.
|
|
198
|
+
* @param {string} field - The name of the relation field.
|
|
199
|
+
* @return {string} The name of the related model, which is always `File` for a file field.
|
|
200
|
+
*/
|
|
201
|
+
function relatedModelName(model, field) {
|
|
202
|
+
return model.config.relations?.[field]?.model ?? 'File';
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Determines whether a relation or file field holds a list of related records, which can only be sorted by their
|
|
206
|
+
* count.
|
|
207
|
+
*
|
|
208
|
+
* @param {RelationField|FileField} field - The relation or file field configuration.
|
|
209
|
+
* @return {boolean} True when the field holds a list of related records.
|
|
210
|
+
*/
|
|
211
|
+
function isFieldArray(field) {
|
|
212
|
+
return 'model' in field ? (0, common_1.isRelationArray)(field) : field.array === true;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Determines whether a model field is a scalar column the database can sort by. The id and the audited fields are
|
|
216
|
+
* always sortable, the configured scalars are sortable unless they are hidden or hold a list of values, and the
|
|
217
|
+
* virtual fields are never sortable, since they have no column of their own.
|
|
218
|
+
*
|
|
219
|
+
* @param {ResourceModel} model - The model the field belongs to.
|
|
220
|
+
* @param {string} field - The name of the field to check.
|
|
221
|
+
* @return {boolean} True when the field can be sorted by.
|
|
222
|
+
*/
|
|
223
|
+
function isScalarField(model, field) {
|
|
224
|
+
if (field === 'id') {
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
const auditFields = { ...defaultAuditFields, ...(model.config.audit ?? {}) };
|
|
228
|
+
if (field in auditFields) {
|
|
229
|
+
// The createdAt field of a model that does not audit it is still accepted,
|
|
230
|
+
// so the default sort value does not have to be adjusted per model
|
|
231
|
+
return auditFields[field] === true || field === 'createdAt';
|
|
232
|
+
}
|
|
233
|
+
const scalar = model.config.scalars?.[field];
|
|
234
|
+
return !!scalar && !scalar.hidden && scalar.array !== true;
|
|
235
|
+
}
|