@appweaver/core 1.1.6 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cache/eviction/lfu-eviction-index.js +3 -0
- package/export/export-service.d.ts +4 -2
- package/export/export-service.js +27 -17
- package/factory/create-model.js +72 -36
- package/factory/create-service.js +1 -1
- package/package.json +1 -1
- package/resource/index.d.ts +1 -0
- package/resource/index.js +1 -0
- package/resource/resource-schema.d.ts +0 -5
- package/resource/resource-schema.js +25 -10
- package/resource/resource-service.d.ts +214 -36
- package/resource/resource-service.js +247 -404
- package/resource/schemas/index.d.ts +3 -0
- package/resource/schemas/index.js +19 -0
- package/resource/schemas/resource-aggregate-schema.d.ts +54 -0
- package/resource/schemas/resource-aggregate-schema.js +131 -0
- package/resource/schemas/resource-filter-schema.d.ts +39 -0
- package/resource/schemas/resource-filter-schema.js +153 -0
- package/resource/schemas/resource-sort-schema.d.ts +37 -0
- package/resource/schemas/resource-sort-schema.js +114 -0
- package/resource/utils/aggregate-util.d.ts +113 -0
- package/resource/utils/aggregate-util.js +323 -0
- package/resource/utils/filter-util.d.ts +24 -0
- package/resource/utils/filter-util.js +283 -0
- package/resource/utils/index.d.ts +4 -0
- package/resource/utils/index.js +20 -0
- package/resource/utils/relation-util.d.ts +84 -0
- package/resource/utils/relation-util.js +344 -0
- package/resource/utils/sort-util.d.ts +20 -0
- package/resource/utils/sort-util.js +218 -0
- package/security/create-auth-resources.js +2 -2
- package/security/helper.js +1 -1
- package/security/resources/api-key/model.js +1 -0
- package/security/resources/api-key/service.d.ts +1 -1
- package/security/resources/role/model.js +1 -1
- package/server/create-server.js +4 -1
- package/server/register-route.js +1 -0
- package/server/swagger.js +87 -21
- package/server/virtual-projection.js +10 -8
- package/types/generated.d.ts +110 -4
- package/utils/schema-util.js +1 -1
- package/utils/virtual-util.js +1 -1
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ResourceService = void 0;
|
|
4
|
-
const date_fns_1 = require("date-fns");
|
|
5
4
|
const common_1 = require("@appweaver/common");
|
|
6
5
|
const context_1 = require("../context");
|
|
7
6
|
const utils_1 = require("../utils");
|
|
8
|
-
const security_1 = require("../security");
|
|
9
7
|
const cache_1 = require("../cache");
|
|
10
8
|
const errors_1 = require("../errors");
|
|
9
|
+
const utils_2 = require("./utils");
|
|
11
10
|
class ResourceService {
|
|
12
11
|
modelName;
|
|
13
12
|
/** @internal */
|
|
@@ -18,6 +17,14 @@ class ResourceService {
|
|
|
18
17
|
_cacheService = (0, context_1.inject)(cache_1.CacheService);
|
|
19
18
|
/** @internal */
|
|
20
19
|
_client;
|
|
20
|
+
/**
|
|
21
|
+
* Creates the service for the given resource model and resolves its database
|
|
22
|
+
* client from the Prisma client instance.
|
|
23
|
+
*
|
|
24
|
+
* @param {string} modelName The resource model name this service operates on,
|
|
25
|
+
* as defined by its model file (i.e. `User`, `Post`).
|
|
26
|
+
* @throws An error if no database client exists for the provided model name.
|
|
27
|
+
*/
|
|
21
28
|
constructor(modelName) {
|
|
22
29
|
this.modelName = modelName;
|
|
23
30
|
this._client = this._db.client()[(0, common_1.uncapitalize)(modelName)];
|
|
@@ -25,12 +32,32 @@ class ResourceService {
|
|
|
25
32
|
throw new Error(`ResourceService initialized with invalid model name: ${modelName}`);
|
|
26
33
|
}
|
|
27
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* The underlying Prisma model delegate for this resource, useful for
|
|
37
|
+
* executing custom database operations that the service methods do not cover.
|
|
38
|
+
*
|
|
39
|
+
* @returns {ResourceClient} The resource client of the model this service was
|
|
40
|
+
* created for.
|
|
41
|
+
*/
|
|
28
42
|
get client() {
|
|
29
43
|
return this._client;
|
|
30
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Finds a single resource by its id, applying the read restrictions and the
|
|
47
|
+
* access check of this service, and including all relation and file fields
|
|
48
|
+
* configured for output on the find action. A resource event is emitted after
|
|
49
|
+
* a successful lookup.
|
|
50
|
+
*
|
|
51
|
+
* @param {number} id The id of the resource to find.
|
|
52
|
+
* @returns {Promise<Object>} The found resource with its virtual fields and
|
|
53
|
+
* relation counts projected.
|
|
54
|
+
* @throws {@link HttpError} 404 if the resource does not exist or is filtered
|
|
55
|
+
* out by the read restrictions, 403 if the access check denies it, and 500 on
|
|
56
|
+
* a database error.
|
|
57
|
+
*/
|
|
31
58
|
async find(id) {
|
|
32
59
|
const restrictions = await this.readRestrictions('find', id);
|
|
33
|
-
const includeRelations =
|
|
60
|
+
const includeRelations = (0, utils_2.mapRelationInclusions)(this._client.name, 'find');
|
|
34
61
|
let resource;
|
|
35
62
|
try {
|
|
36
63
|
resource = await this._client.findFirst({
|
|
@@ -53,13 +80,42 @@ class ResourceService {
|
|
|
53
80
|
});
|
|
54
81
|
return this.projectResource(resource);
|
|
55
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* Queries a page of resources matching the provided filter. The filter is
|
|
85
|
+
* mapped to a database query, combined with the optional `searchText` full
|
|
86
|
+
* text search query and the read restrictions of this service, and executed
|
|
87
|
+
* together with the total count in a single transaction. A resource event is
|
|
88
|
+
* emitted after a successful query.
|
|
89
|
+
*
|
|
90
|
+
* @param {Object} [filter] The query filter object, supporting the logical
|
|
91
|
+
* (`_and`, `_or`, `_not`, `_nor`), comparison (`_eq`, `_ne`, `_gt`, `_gte`,
|
|
92
|
+
* `_lt`, `_lte`, `_in`, `_nin`, `_between`, `_like`, `_ilike`, `_starts`,
|
|
93
|
+
* `_ends`, `_contains`, `_exists`), list (`_has`, `_hasSome`, `_hasEvery`,
|
|
94
|
+
* `_isEmpty`), and relation (`_some`, `_every`, `_none`) operators, as well
|
|
95
|
+
* as plain field values. Its `searchText` property, if present, is passed to
|
|
96
|
+
* {@link ResourceService.textSearchQuery} instead of being matched as a
|
|
97
|
+
* field.
|
|
98
|
+
* @param {number} [page] The one-based page number of results to return.
|
|
99
|
+
* @param {number} [size] The maximum number of results per page.
|
|
100
|
+
* @param {QuerySort} [sort] The fields to sort by, either as a comma-separated
|
|
101
|
+
* list where a field prefixed with `-` is sorted in descending order
|
|
102
|
+
* (i.e. `-createdAt,id`), or as an object of field directions
|
|
103
|
+
* (i.e. `{ createdAt: 'desc', id: 'asc' }`). Both forms support the fields of
|
|
104
|
+
* the included to-one relations, given with a dot notation (`author.createdAt`)
|
|
105
|
+
* or as a nested object (`{ author: { createdAt: 'desc' } }`).
|
|
106
|
+
* @returns {Promise<QueryResponse<Object>>} The paged query response containing
|
|
107
|
+
* the returned resources, the count of the returned items and the total count
|
|
108
|
+
* of matching resources.
|
|
109
|
+
* @throws {@link HttpError} 400 if the sort input names a field that cannot be
|
|
110
|
+
* sorted by, and 500 on a database error.
|
|
111
|
+
*/
|
|
56
112
|
async query(filter = {}, page = 1, size = 50, sort = '-createdAt,id') {
|
|
57
113
|
const restrictions = await this.readRestrictions('query', filter);
|
|
58
114
|
const textSearch = this.extractTextSearchQuery(filter);
|
|
59
|
-
const mappedFilter =
|
|
115
|
+
const mappedFilter = (0, utils_2.mapQueryFilter)(filter, this._client.name);
|
|
60
116
|
const query = { AND: [mappedFilter, textSearch, restrictions] };
|
|
61
|
-
const includeRelations =
|
|
62
|
-
const orderBy =
|
|
117
|
+
const includeRelations = (0, utils_2.mapRelationInclusions)(this._client.name, 'query');
|
|
118
|
+
const orderBy = (0, utils_2.mapSortValues)(sort, this._client.name, 'query');
|
|
63
119
|
let resources;
|
|
64
120
|
let totalCount;
|
|
65
121
|
try {
|
|
@@ -88,60 +144,72 @@ class ResourceService {
|
|
|
88
144
|
items: resources.map((resource) => this.projectResource(resource))
|
|
89
145
|
};
|
|
90
146
|
}
|
|
147
|
+
/**
|
|
148
|
+
* Aggregates resources matching the provided filter over a date range, both
|
|
149
|
+
* as a single overall result and as a series of results for the equally sized
|
|
150
|
+
* periods the range is split into. All aggregations are executed in a single
|
|
151
|
+
* transaction.
|
|
152
|
+
*
|
|
153
|
+
* @param {Object} [filter] The query filter object, mapped the same way as in
|
|
154
|
+
* {@link ResourceService.query}.
|
|
155
|
+
* @param {AggregateSelect<Object>} select The aggregation operations to perform
|
|
156
|
+
* per field (i.e. `{ views: { count: true, sum: true } }`). Only the numeric
|
|
157
|
+
* fields of the model accept every operator, while its date fields accept
|
|
158
|
+
* `count`, `min`, `max`, `first` and `last`. The `first` and `last` operators
|
|
159
|
+
* take the value the earliest and the latest record of a period holds, which
|
|
160
|
+
* costs one additional query per period and boundary, skipped for the periods
|
|
161
|
+
* holding no record.
|
|
162
|
+
* @param {string} [dateField] The date field the range is applied on.
|
|
163
|
+
* @param {string} [from] The ISO date string of the range start. Defaults to
|
|
164
|
+
* seven days before the range end.
|
|
165
|
+
* @param {string} [to] The ISO date string of the range end. Defaults to the
|
|
166
|
+
* current date and time.
|
|
167
|
+
* @param {number} [step] The size of a single period in units of the
|
|
168
|
+
* automatically selected time unit. If not provided, one unit is used and the
|
|
169
|
+
* unit is derived from the range length (seconds up to a minute, minutes up to
|
|
170
|
+
* an hour, hours up to a day, days up to a month, months up to a year, and
|
|
171
|
+
* years beyond that).
|
|
172
|
+
* @param {boolean} [safeIncrement] Whether the period increments use the
|
|
173
|
+
* derived time unit and stay consistent across daylight saving time changes.
|
|
174
|
+
* When false, the step is interpreted in seconds.
|
|
175
|
+
* @returns {Promise<AggregateResponse<Object>>} The aggregation response with
|
|
176
|
+
* the overall total and one result per period, each labeled with the median
|
|
177
|
+
* date of its period.
|
|
178
|
+
* @throws {@link HttpError} 400 if the selection is empty or names a field or
|
|
179
|
+
* operator that cannot be aggregated, and 500 on a database error.
|
|
180
|
+
*/
|
|
91
181
|
async aggregate(filter = {}, select, dateField = 'createdAt', from, to, step, safeIncrement = true) {
|
|
92
|
-
const toDate = (0,
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
const dateRanges = [];
|
|
96
|
-
let currentDate = (0, date_fns_1.parseISO)(fromDate.toISOString());
|
|
97
|
-
while (currentDate < toDate) {
|
|
98
|
-
const date = currentDate;
|
|
99
|
-
const median = iterator.addPeriod(date, (iterator.step - 1) / 2);
|
|
100
|
-
currentDate = iterator.addPeriod(currentDate, iterator.step);
|
|
101
|
-
dateRanges.push({ from: date, to: currentDate, median });
|
|
102
|
-
}
|
|
103
|
-
const aggregateOperations = this.mapAggregationValues(select);
|
|
182
|
+
const { fromDate, toDate, ranges: dateRanges } = (0, utils_2.buildAggregationPeriods)(from, to, step, safeIncrement);
|
|
183
|
+
const operations = (0, utils_2.mapAggregationSelect)(select, this._client.name);
|
|
184
|
+
(0, utils_2.checkAggregationDateField)(dateField, this._client.name);
|
|
104
185
|
const restrictions = await this.readRestrictions('aggregate', filter);
|
|
105
186
|
const textSearch = this.extractTextSearchQuery(filter);
|
|
106
|
-
const mappedFilter =
|
|
187
|
+
const mappedFilter = (0, utils_2.mapQueryFilter)(filter, this._client.name);
|
|
107
188
|
const query = { AND: [mappedFilter, textSearch, restrictions] };
|
|
189
|
+
const rangeQuery = (rangeFrom, rangeTo) => ({
|
|
190
|
+
AND: [query, { [dateField]: { gte: rangeFrom, lt: rangeTo } }]
|
|
191
|
+
});
|
|
192
|
+
// Aggregates a single range and reads the boundary records it holds the
|
|
193
|
+
// first and last values of which the database cannot aggregate
|
|
194
|
+
const aggregateRange = async (txModel, where) => {
|
|
195
|
+
const result = await txModel.aggregate({
|
|
196
|
+
...operations.aggregate,
|
|
197
|
+
where
|
|
198
|
+
});
|
|
199
|
+
const boundaries = await (0, utils_2.readAggregationBoundaries)(txModel, where, dateField, operations, (0, utils_2.aggregationRecordCount)(result));
|
|
200
|
+
return { ...result, ...boundaries };
|
|
201
|
+
};
|
|
108
202
|
let total = {};
|
|
109
203
|
let items = [];
|
|
110
204
|
try {
|
|
111
205
|
[total, items] = await this._db.client().$transaction(async (tx) => {
|
|
112
206
|
const txModel = tx[this._client.name];
|
|
113
|
-
const overall = await txModel
|
|
114
|
-
...aggregateOperations,
|
|
115
|
-
where: {
|
|
116
|
-
AND: [
|
|
117
|
-
query,
|
|
118
|
-
{
|
|
119
|
-
[dateField]: {
|
|
120
|
-
gte: fromDate,
|
|
121
|
-
lt: toDate
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
]
|
|
125
|
-
}
|
|
126
|
-
});
|
|
207
|
+
const overall = await aggregateRange(txModel, rangeQuery(fromDate, toDate));
|
|
127
208
|
// Skip executing the same query if only one range value is generated
|
|
128
209
|
if (dateRanges.length === 1) {
|
|
129
210
|
return [overall, [overall]];
|
|
130
211
|
}
|
|
131
|
-
const ranges = await Promise.all(dateRanges.map((dateRange) => txModel.
|
|
132
|
-
...aggregateOperations,
|
|
133
|
-
where: {
|
|
134
|
-
AND: [
|
|
135
|
-
query,
|
|
136
|
-
{
|
|
137
|
-
[dateField]: {
|
|
138
|
-
gte: dateRange.from,
|
|
139
|
-
lt: dateRange.to
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
]
|
|
143
|
-
}
|
|
144
|
-
})));
|
|
212
|
+
const ranges = await Promise.all(dateRanges.map((dateRange) => aggregateRange(txModel, rangeQuery(dateRange.from, dateRange.to))));
|
|
145
213
|
return [overall, ranges];
|
|
146
214
|
});
|
|
147
215
|
}
|
|
@@ -149,15 +217,32 @@ class ResourceService {
|
|
|
149
217
|
throw new errors_1.HttpError('Error on results aggregation', 500, e);
|
|
150
218
|
}
|
|
151
219
|
return {
|
|
152
|
-
total:
|
|
220
|
+
total: (0, utils_2.mapAggregationResult)(total),
|
|
153
221
|
items: items.map((item, index) => ({
|
|
154
222
|
date: dateRanges[index].median,
|
|
155
|
-
result:
|
|
223
|
+
result: (0, utils_2.mapAggregationResult)(item)
|
|
156
224
|
}))
|
|
157
225
|
};
|
|
158
226
|
}
|
|
227
|
+
/**
|
|
228
|
+
* Creates a new resource. The provided data is merged with the write
|
|
229
|
+
* restrictions, checked for access, sanitized against the model configuration
|
|
230
|
+
* and mapped to the relation write actions (connect, create, or
|
|
231
|
+
* connect-or-create) before the record is created. The `createdBy` audit
|
|
232
|
+
* relation is connected to the currently authenticated user when configured.
|
|
233
|
+
* The resource cache is invalidated and a resource event is emitted after a
|
|
234
|
+
* successful create.
|
|
235
|
+
*
|
|
236
|
+
* @param {Object} data The data of the resource to create, including any inline
|
|
237
|
+
* relation and file payloads.
|
|
238
|
+
* @returns {Promise<Object>} The created resource with its virtual fields and
|
|
239
|
+
* relation counts projected.
|
|
240
|
+
* @throws {@link HttpError} 403 if the access check denies the action, 400 if
|
|
241
|
+
* an inline relation payload is missing required fields or the relation does
|
|
242
|
+
* not accept new records, and 500 on a database error.
|
|
243
|
+
*/
|
|
159
244
|
async create(data) {
|
|
160
|
-
const createdBy =
|
|
245
|
+
const createdBy = (0, utils_2.createdByConnect)(this._client.name);
|
|
161
246
|
const restrictions = await this.writeRestrictions('create', data);
|
|
162
247
|
const createData = (0, common_1.removeUndefined)({
|
|
163
248
|
...data,
|
|
@@ -168,8 +253,8 @@ class ResourceService {
|
|
|
168
253
|
throw new errors_1.HttpError(`${this._client.name} create action is forbidden`, 403);
|
|
169
254
|
}
|
|
170
255
|
const sanitizedData = this.sanitizeData('create', createData);
|
|
171
|
-
const connectRelations =
|
|
172
|
-
const includeRelations =
|
|
256
|
+
const connectRelations = (0, utils_2.mapRelationActions)(this._client.name, 'create', sanitizedData);
|
|
257
|
+
const includeRelations = (0, utils_2.mapRelationInclusions)(this._client.name, 'create');
|
|
173
258
|
let resource;
|
|
174
259
|
try {
|
|
175
260
|
resource = await this._client.create({
|
|
@@ -190,6 +275,25 @@ class ResourceService {
|
|
|
190
275
|
});
|
|
191
276
|
return this.projectResource(resource);
|
|
192
277
|
}
|
|
278
|
+
/**
|
|
279
|
+
* Updates an existing resource by its id. The current record is loaded with
|
|
280
|
+
* the read restrictions applied and checked for access, then updated with the
|
|
281
|
+
* data merged with the write restrictions inside a single transaction.
|
|
282
|
+
* Relations missing from the new value are disconnected, or deleted when
|
|
283
|
+
* `orphanRemoval` is configured for them. The resource cache is invalidated
|
|
284
|
+
* and a resource event carrying both the previous and the current state is
|
|
285
|
+
* emitted after a successful update.
|
|
286
|
+
*
|
|
287
|
+
* @param {number} id The id of the resource to update.
|
|
288
|
+
* @param {Object} data The partial data to update the resource with, including
|
|
289
|
+
* any inline relation and file payloads.
|
|
290
|
+
* @returns {Promise<Object>} The updated resource with its virtual fields and
|
|
291
|
+
* relation counts projected.
|
|
292
|
+
* @throws {@link HttpError} 404 if the resource does not exist or is filtered
|
|
293
|
+
* out by the read restrictions, 403 if the access check denies the action, 400
|
|
294
|
+
* if an inline relation payload is missing required fields or the relation
|
|
295
|
+
* does not accept new records, and 500 on a database error.
|
|
296
|
+
*/
|
|
193
297
|
async update(id, data) {
|
|
194
298
|
const readRestrictions = await this.readRestrictions('update', {
|
|
195
299
|
id,
|
|
@@ -204,7 +308,7 @@ class ResourceService {
|
|
|
204
308
|
...writeRestrictions
|
|
205
309
|
});
|
|
206
310
|
const sanitizedData = this.sanitizeData('update', updateData);
|
|
207
|
-
const includeRelations =
|
|
311
|
+
const includeRelations = (0, utils_2.mapRelationInclusions)(this._client.name, 'update');
|
|
208
312
|
let updateResource;
|
|
209
313
|
let resource;
|
|
210
314
|
try {
|
|
@@ -223,7 +327,7 @@ class ResourceService {
|
|
|
223
327
|
if (!access) {
|
|
224
328
|
throw new errors_1.HttpError(`${this._client.name} update action is forbidden`, 403);
|
|
225
329
|
}
|
|
226
|
-
const setRelations =
|
|
330
|
+
const setRelations = (0, utils_2.mapRelationActions)(this._client.name, 'update', sanitizedData, current);
|
|
227
331
|
const updated = await txModel.update({
|
|
228
332
|
where: { id },
|
|
229
333
|
include: includeRelations,
|
|
@@ -245,6 +349,19 @@ class ResourceService {
|
|
|
245
349
|
});
|
|
246
350
|
return this.projectResource(resource);
|
|
247
351
|
}
|
|
352
|
+
/**
|
|
353
|
+
* Deletes an existing resource by its id. The current record is loaded with
|
|
354
|
+
* the read restrictions applied and checked for access before it is deleted
|
|
355
|
+
* inside a single transaction. The resource cache is invalidated and a
|
|
356
|
+
* resource event is emitted after a successful delete.
|
|
357
|
+
*
|
|
358
|
+
* @param {number} id The id of the resource to delete.
|
|
359
|
+
* @returns {Promise<Object>} The deleted resource with its virtual fields and
|
|
360
|
+
* relation counts projected.
|
|
361
|
+
* @throws {@link HttpError} 404 if the resource does not exist or is filtered
|
|
362
|
+
* out by the read restrictions, 403 if the access check denies the action, and
|
|
363
|
+
* 500 on a database error.
|
|
364
|
+
*/
|
|
248
365
|
async delete(id) {
|
|
249
366
|
const restrictions = await this.readRestrictions('delete', id);
|
|
250
367
|
let resource;
|
|
@@ -261,7 +378,7 @@ class ResourceService {
|
|
|
261
378
|
if (!access) {
|
|
262
379
|
throw new errors_1.HttpError(`${this._client.name} delete action is forbidden`, 403);
|
|
263
380
|
}
|
|
264
|
-
const includeRelations =
|
|
381
|
+
const includeRelations = (0, utils_2.mapRelationInclusions)(this._client.name, 'delete');
|
|
265
382
|
return await txModel.delete({
|
|
266
383
|
where: { id },
|
|
267
384
|
include: includeRelations
|
|
@@ -285,21 +402,24 @@ class ResourceService {
|
|
|
285
402
|
* operations on specific data for currently logged-in user and other
|
|
286
403
|
* authorization rules. The returned object will be applied as a filter on
|
|
287
404
|
* all actions (except create action) which will prevent unwanted data access
|
|
288
|
-
* and modifications. This method can also cancel current action by
|
|
289
|
-
* an error, recommended is {@link HttpError } with appropriate HTTP
|
|
290
|
-
* code.
|
|
405
|
+
* and modifications. This method can also cancel the current action by
|
|
406
|
+
* throwing an error, recommended is {@link HttpError } with appropriate HTTP
|
|
407
|
+
* error code.
|
|
291
408
|
*
|
|
292
|
-
* @param action The called action method on this service (find,
|
|
293
|
-
* aggregate, update, or delete)
|
|
294
|
-
* @param data The passed data to the called function
|
|
295
|
-
* object. If the data is a type of number, then it represents the
|
|
296
|
-
* id, otherwise it depends on the action and can be one of the
|
|
409
|
+
* @param {ActionType} action The called action method on this service (find,
|
|
410
|
+
* query, aggregate, update, or delete)
|
|
411
|
+
* @param {Object|number} data The passed data to the called function can be
|
|
412
|
+
* number or object. If the data is a type of number, then it represents the
|
|
413
|
+
* resource id, otherwise it depends on the action and can be one of the
|
|
414
|
+
* following:
|
|
297
415
|
*
|
|
298
416
|
* - query and aggregate -> filter object
|
|
299
417
|
* - update -> combined id and the data object (i.e. { id, ...data })
|
|
300
418
|
*
|
|
301
419
|
* For other actions (find and delete) it represents the resource id.
|
|
302
|
-
* @return The
|
|
420
|
+
* @return {Promise<Object>} The database-level `where` conditions containing
|
|
421
|
+
* additional query restrictions. The returned object is applied directly to
|
|
422
|
+
* the database query, so it uses the native Prisma filter syntax.
|
|
303
423
|
*/
|
|
304
424
|
async readRestrictions(action, data) {
|
|
305
425
|
return {};
|
|
@@ -313,13 +433,15 @@ class ResourceService {
|
|
|
313
433
|
* current action by throwing an error, with the recommended type being
|
|
314
434
|
* {@link HttpError} with the appropriate HTTP error code.
|
|
315
435
|
*
|
|
316
|
-
* @param action The called action method on this service
|
|
317
|
-
*
|
|
318
|
-
*
|
|
436
|
+
* @param {'create'|'update'} action The called action method on this service
|
|
437
|
+
* (create or update)
|
|
438
|
+
* @param {Object} data The passed data to the called function, which should be
|
|
439
|
+
* an object representing the resource data to be created or updated. For
|
|
319
440
|
* update operations, this object will also include the resource ID.
|
|
320
441
|
*
|
|
321
|
-
* @return A partial object containing additional data
|
|
322
|
-
* applied, or an empty object if no restrictions are
|
|
442
|
+
* @return {Promise<Object>} A partial object containing additional data
|
|
443
|
+
* restrictions to be applied, or an empty object if no restrictions are
|
|
444
|
+
* necessary.
|
|
323
445
|
*/
|
|
324
446
|
async writeRestrictions(action, data) {
|
|
325
447
|
return {};
|
|
@@ -330,10 +452,11 @@ class ResourceService {
|
|
|
330
452
|
* a currently authenticated user or other logic, this method should return
|
|
331
453
|
* false. Otherwise, it returns true and continues with the request execution.
|
|
332
454
|
*
|
|
333
|
-
* @param action The called action method on this service (find,
|
|
334
|
-
* aggregate, create, update, or delete)
|
|
335
|
-
* @param resource The resource object that is being
|
|
336
|
-
* @returns True if the access for resource is granted, false
|
|
455
|
+
* @param {ActionType} action The called action method on this service (find,
|
|
456
|
+
* query, aggregate, create, update, or delete)
|
|
457
|
+
* @param {Object} resource The resource object that is being checked for access.
|
|
458
|
+
* @returns {Promise<boolean>} True if the access for resource is granted, false
|
|
459
|
+
* otherwise.
|
|
337
460
|
*/
|
|
338
461
|
async checkAccess(action, resource) {
|
|
339
462
|
return true;
|
|
@@ -344,14 +467,25 @@ class ResourceService {
|
|
|
344
467
|
* search text into a format suitable for text search functionality, returning
|
|
345
468
|
* a query object that can be used to filter results.
|
|
346
469
|
*
|
|
347
|
-
* @param searchText The text string used for searching resources.
|
|
348
|
-
* @returns A query object that represents the conditions for the text
|
|
349
|
-
* search operation
|
|
350
|
-
* retrieve matching resources.
|
|
470
|
+
* @param {string} searchText The text string used for searching resources.
|
|
471
|
+
* @returns {Object} A query object that represents the conditions for the text
|
|
472
|
+
* search operation, using the native Prisma filter syntax. This will be used
|
|
473
|
+
* by the database query methods to retrieve matching resources.
|
|
351
474
|
*/
|
|
352
475
|
textSearchQuery(searchText) {
|
|
353
476
|
return {};
|
|
354
477
|
}
|
|
478
|
+
/**
|
|
479
|
+
* Removes the `searchText` property from the provided filter and converts it
|
|
480
|
+
* into a text search query using
|
|
481
|
+
* {@link ResourceService.textSearchQuery}. The filter object is mutated so the
|
|
482
|
+
* search text is not matched as a regular resource field.
|
|
483
|
+
*
|
|
484
|
+
* @param {Object} filter The request filter object, possibly containing a
|
|
485
|
+
* `searchText` property. The property is deleted from this object when present.
|
|
486
|
+
* @returns {Object} The text search query for the extracted search text, or an
|
|
487
|
+
* empty object if the filter contains no search text.
|
|
488
|
+
*/
|
|
355
489
|
extractTextSearchQuery(filter) {
|
|
356
490
|
if (filter.searchText) {
|
|
357
491
|
const searchQuery = this.textSearchQuery(filter.searchText);
|
|
@@ -360,92 +494,20 @@ class ResourceService {
|
|
|
360
494
|
}
|
|
361
495
|
return {};
|
|
362
496
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
continue;
|
|
375
|
-
}
|
|
376
|
-
// Map relations count field sort order.
|
|
377
|
-
if ((0, common_1.isCountField)(path)) {
|
|
378
|
-
path = path.replace('Count', '._count');
|
|
379
|
-
}
|
|
380
|
-
(0, common_1.setValue)(sortMap, path, order);
|
|
381
|
-
}
|
|
382
|
-
return Object.entries(sortMap).map(([key, value]) => ({ [key]: value }));
|
|
383
|
-
}
|
|
384
|
-
mapAggregationValues(select, isOutput = false) {
|
|
385
|
-
const aggregationMap = {};
|
|
386
|
-
for (const field in select) {
|
|
387
|
-
const operators = select[field];
|
|
388
|
-
for (const operator in operators) {
|
|
389
|
-
const value = operators[operator];
|
|
390
|
-
const path = isOutput
|
|
391
|
-
? `${operator}.${field.substring(1)}`
|
|
392
|
-
: `_${operator}.${field}`;
|
|
393
|
-
(0, common_1.setValue)(aggregationMap, path, value);
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
return aggregationMap;
|
|
397
|
-
}
|
|
398
|
-
makeAggregationIterator(fromDate, toDate, step, safeIncrement = true) {
|
|
399
|
-
let stepAmount = step;
|
|
400
|
-
let incrementFn = date_fns_1.addSeconds;
|
|
401
|
-
if (!stepAmount) {
|
|
402
|
-
const diffInSeconds = (0, date_fns_1.differenceInSeconds)(toDate, fromDate);
|
|
403
|
-
const diffInMonths = (0, date_fns_1.differenceInMonths)(toDate, fromDate);
|
|
404
|
-
const diffInYears = (0, date_fns_1.differenceInYears)(toDate, fromDate);
|
|
405
|
-
stepAmount = 1;
|
|
406
|
-
// 1-second step if the difference is less than or equal to 1 minute
|
|
407
|
-
if (diffInSeconds <= 60) {
|
|
408
|
-
incrementFn = date_fns_1.addSeconds;
|
|
409
|
-
}
|
|
410
|
-
// 1-minute step if the difference is less than or equal to 1 hour
|
|
411
|
-
else if (diffInSeconds <= 3600) {
|
|
412
|
-
incrementFn = date_fns_1.addMinutes;
|
|
413
|
-
}
|
|
414
|
-
// 1-hour step if the difference is less than or equal to 1 day
|
|
415
|
-
else if (diffInSeconds <= 86400) {
|
|
416
|
-
incrementFn = date_fns_1.addHours;
|
|
417
|
-
}
|
|
418
|
-
// 1-day step if the difference is less than or equal to 1 month
|
|
419
|
-
else if (diffInMonths <= 1) {
|
|
420
|
-
incrementFn = date_fns_1.addDays;
|
|
421
|
-
}
|
|
422
|
-
// 1-month step if the difference is less than or equal to 1 year
|
|
423
|
-
else if (diffInYears <= 1) {
|
|
424
|
-
incrementFn = date_fns_1.addMonths;
|
|
425
|
-
}
|
|
426
|
-
// 1-year step if the difference is equal to 1 year or more
|
|
427
|
-
else {
|
|
428
|
-
incrementFn = date_fns_1.addYears;
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
// A higher-order function that adjusts date increments to account for
|
|
432
|
-
// changes in daylight saving time (DST). When incrementing dates in time
|
|
433
|
-
// zones that observe DST, this function ensures that the resulting date
|
|
434
|
-
// remains consistent with the original date's time zone offset.
|
|
435
|
-
const dstAgnosticFn = (fn) => {
|
|
436
|
-
return (date, amount) => {
|
|
437
|
-
const endDate = fn(date, amount);
|
|
438
|
-
return (0, date_fns_1.addMinutes)(endDate, date.getTimezoneOffset() - endDate.getTimezoneOffset());
|
|
439
|
-
};
|
|
440
|
-
};
|
|
441
|
-
return {
|
|
442
|
-
addPeriod: dstAgnosticFn(safeIncrement ? incrementFn : date_fns_1.addSeconds),
|
|
443
|
-
step: stepAmount
|
|
444
|
-
};
|
|
445
|
-
}
|
|
497
|
+
/**
|
|
498
|
+
* Prepares a resource for the response by resolving the virtual fields of its
|
|
499
|
+
* model and flattening the Prisma `_count` aggregation into the individual
|
|
500
|
+
* relation count fields (i.e. `_count.posts` becomes `postsCount`).
|
|
501
|
+
*
|
|
502
|
+
* @param {Object} resource The resource object as returned by the database
|
|
503
|
+
* client.
|
|
504
|
+
* @returns {Object} The same resource with the virtual fields resolved and, if a
|
|
505
|
+
* `_count` selection was present, with a count property per counted relation
|
|
506
|
+
* and the `_count` property removed.
|
|
507
|
+
*/
|
|
446
508
|
projectResource(resource) {
|
|
447
509
|
const projectedResource = (0, utils_1.projectVirtualFields)(resource, this._client.name);
|
|
448
|
-
if (!(0, common_1.
|
|
510
|
+
if (!(0, common_1.isPlainObject)(projectedResource['_count'])) {
|
|
449
511
|
return projectedResource;
|
|
450
512
|
}
|
|
451
513
|
// Create new relation count properties on the resource object
|
|
@@ -455,6 +517,25 @@ class ResourceService {
|
|
|
455
517
|
delete projectedResource['_count'];
|
|
456
518
|
return projectedResource;
|
|
457
519
|
}
|
|
520
|
+
/**
|
|
521
|
+
* Prepares a write payload for the database by removing the virtual fields
|
|
522
|
+
* that have no column, filling in default values for the hidden required
|
|
523
|
+
* scalars of a create action, and recursively applying the same rules to the
|
|
524
|
+
* nested relation and file payloads. Unique key values, arrays of them and null
|
|
525
|
+
* values are left untouched, so the relation actions can still map them to the
|
|
526
|
+
* connect and disconnect operations.
|
|
527
|
+
*
|
|
528
|
+
* @param {'create'|'update'} action The write action the payload is sanitized
|
|
529
|
+
* for. Default values for the hidden required scalars are only applied on a
|
|
530
|
+
* create action.
|
|
531
|
+
* @param {Object} data The write payload to sanitize.
|
|
532
|
+
* @param {string} [resourceName] The name of the model the payload belongs to.
|
|
533
|
+
* Defaults to the model of this service and is set to the related model name
|
|
534
|
+
* when recursing into a nested relation or file payload.
|
|
535
|
+
* @returns {Object} A shallow copy of the payload without the virtual fields,
|
|
536
|
+
* with the missing hidden required scalars defaulted, and with the nested
|
|
537
|
+
* payloads sanitized against their own models.
|
|
538
|
+
*/
|
|
458
539
|
sanitizeData(action, data, resourceName) {
|
|
459
540
|
const sanitizedData = { ...data };
|
|
460
541
|
const resourceModel = (0, context_1.injectModel)(resourceName ?? this._client.name, false);
|
|
@@ -481,257 +562,19 @@ class ResourceService {
|
|
|
481
562
|
const value = sanitizedData[key];
|
|
482
563
|
const relationSchema = (0, common_1.extractSchemaProperties)(relationsModel, key);
|
|
483
564
|
const fileSchema = (0, common_1.extractSchemaProperties)(filesModel, key);
|
|
484
|
-
|
|
565
|
+
// Only nested resource payloads are sanitized. Unique key values, arrays
|
|
566
|
+
// of them and null values are left untouched, so the relation actions can
|
|
567
|
+
// still map them to connect and disconnect operations.
|
|
568
|
+
if ((0, common_1.isPlainObject)(value) || ((0, common_1.isArray)(value) && (0, common_1.isPlainObject)(value[0]))) {
|
|
485
569
|
const resourceName = (0, common_1.extractResourceName)(relationSchema ?? fileSchema);
|
|
486
570
|
if (resourceName) {
|
|
487
571
|
sanitizedData[key] = (0, common_1.isArray)(value)
|
|
488
|
-
? value.map((item) => this.sanitizeData(item, resourceName))
|
|
572
|
+
? value.map((item) => this.sanitizeData(action, item, resourceName))
|
|
489
573
|
: this.sanitizeData(action, value, resourceName);
|
|
490
574
|
}
|
|
491
575
|
}
|
|
492
576
|
}
|
|
493
577
|
return sanitizedData;
|
|
494
578
|
}
|
|
495
|
-
mapQueryFilter(filter, resourceName) {
|
|
496
|
-
const queryFilter = {};
|
|
497
|
-
const resourceModel = (0, context_1.injectModel)(resourceName ?? this._client.name, false);
|
|
498
|
-
const readModel = resourceModel?.readModel;
|
|
499
|
-
const relationsModel = resourceModel?.relationsModel;
|
|
500
|
-
const filesModel = resourceModel?.filesModel;
|
|
501
|
-
for (const key in filter) {
|
|
502
|
-
const value = filter[key];
|
|
503
|
-
const readSchema = (0, common_1.extractSchemaProperties)(readModel, key);
|
|
504
|
-
const relationSchema = (0, common_1.extractSchemaProperties)(relationsModel, key);
|
|
505
|
-
const fileSchema = (0, common_1.extractSchemaProperties)(filesModel, key);
|
|
506
|
-
const isArrayType = readSchema?.type === 'array' ||
|
|
507
|
-
relationSchema?.type === 'array' ||
|
|
508
|
-
fileSchema?.type === 'array';
|
|
509
|
-
const isArrayValue = (0, common_1.isArray)(value);
|
|
510
|
-
// Recursively map nested objects and handle arrays of objects. Arrays of
|
|
511
|
-
// plain values are mapped below as inclusion, range or relation filters.
|
|
512
|
-
if (((0, common_1.isObject)(value) && !isArrayValue) ||
|
|
513
|
-
(isArrayValue && (0, common_1.isObject)(value[0]))) {
|
|
514
|
-
const resourceName = (0, common_1.extractResourceName)(relationSchema ?? fileSchema);
|
|
515
|
-
if (resourceName) {
|
|
516
|
-
queryFilter[key] = isArrayValue
|
|
517
|
-
? value.map((item) => this.mapQueryFilter(item, resourceName))
|
|
518
|
-
: this.mapQueryFilter(value, resourceName);
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
// Map ID values for both single and array types of relationships
|
|
522
|
-
else if (relationSchema || fileSchema) {
|
|
523
|
-
const queryId = { id: isArrayValue ? { in: value } : value };
|
|
524
|
-
queryFilter[key] = isArrayType ? { some: queryId } : queryId;
|
|
525
|
-
}
|
|
526
|
-
// Map fields without relationships, supporting array types
|
|
527
|
-
else if (readSchema) {
|
|
528
|
-
if (isArrayType) {
|
|
529
|
-
queryFilter[key] = isArrayValue ? { hasSome: value } : { has: value };
|
|
530
|
-
}
|
|
531
|
-
else if (isArrayValue) {
|
|
532
|
-
// For date and numeric types, apply range filtering with inclusive
|
|
533
|
-
// intervals
|
|
534
|
-
if (value.length > 0 &&
|
|
535
|
-
value.length <= 2 &&
|
|
536
|
-
(['number', 'integer'].includes(readSchema.type) ||
|
|
537
|
-
['date', 'date-time'].includes(readSchema.format))) {
|
|
538
|
-
queryFilter[key] = {
|
|
539
|
-
gte: value[0],
|
|
540
|
-
lte: value[1]
|
|
541
|
-
};
|
|
542
|
-
}
|
|
543
|
-
// Map array values for inclusion checks
|
|
544
|
-
else {
|
|
545
|
-
queryFilter[key] = { in: value };
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
// If no query filter was defined, assign the original value
|
|
550
|
-
if (queryFilter[key] === undefined) {
|
|
551
|
-
queryFilter[key] = value;
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
return queryFilter;
|
|
555
|
-
}
|
|
556
|
-
mapRelationInclusions(action, resourceName) {
|
|
557
|
-
const inclusion = {};
|
|
558
|
-
const resourceModel = (0, context_1.injectModel)(resourceName ?? this._client.name);
|
|
559
|
-
const relationConfig = resourceModel.config.relations;
|
|
560
|
-
const fileConfig = resourceModel.config.files;
|
|
561
|
-
const relationModelProps = (0, common_1.extractSchemaProperties)(resourceModel.relationsModel);
|
|
562
|
-
const fileModelProps = (0, common_1.extractSchemaProperties)(resourceModel.filesModel);
|
|
563
|
-
// Add relation and file fields to the inclusion map if the include type is
|
|
564
|
-
// satisfied or the requested action is not specified. Also, add the count
|
|
565
|
-
// aggregation actions for relations if configured.
|
|
566
|
-
for (const key of Object.keys({
|
|
567
|
-
...relationModelProps,
|
|
568
|
-
...fileModelProps
|
|
569
|
-
})) {
|
|
570
|
-
const relationField = relationConfig?.[key] || fileConfig?.[key];
|
|
571
|
-
if (relationField?.output?.count) {
|
|
572
|
-
inclusion._count = inclusion._count ?? { select: {} };
|
|
573
|
-
inclusion._count.select[key] = true;
|
|
574
|
-
}
|
|
575
|
-
// Check if the relation should be included based on the output type
|
|
576
|
-
if (this.shouldIncludeRelation(relationField?.output?.type, action)) {
|
|
577
|
-
inclusion[key] = this.buildNestedInclusion(relationField, action);
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
return inclusion;
|
|
581
|
-
}
|
|
582
|
-
buildNestedInclusion(relationField, action) {
|
|
583
|
-
const nestedIncludeConfig = relationField?.output?.include;
|
|
584
|
-
if (!nestedIncludeConfig || Object.keys(nestedIncludeConfig).length === 0) {
|
|
585
|
-
return true;
|
|
586
|
-
}
|
|
587
|
-
const nestedInclusion = {};
|
|
588
|
-
for (const [nestedKey, nestedOutput] of Object.entries(nestedIncludeConfig)) {
|
|
589
|
-
// Check if the nested relation should be included
|
|
590
|
-
if (this.shouldIncludeRelation(nestedOutput?.type, action)) {
|
|
591
|
-
// Recursively build nested inclusions
|
|
592
|
-
nestedInclusion[nestedKey] = this.buildNestedInclusion({ output: nestedOutput }, action);
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
return Object.keys(nestedInclusion).length > 0
|
|
596
|
-
? { include: nestedInclusion }
|
|
597
|
-
: true;
|
|
598
|
-
}
|
|
599
|
-
shouldIncludeRelation(outputType, action) {
|
|
600
|
-
if (outputType === 'none') {
|
|
601
|
-
return false;
|
|
602
|
-
}
|
|
603
|
-
if (outputType === 'single' && action === 'query') {
|
|
604
|
-
return false;
|
|
605
|
-
}
|
|
606
|
-
return !(outputType === 'multiple' && action && action !== 'query');
|
|
607
|
-
}
|
|
608
|
-
mapRelationActions(action, data, currentData) {
|
|
609
|
-
const relations = {};
|
|
610
|
-
const resourceModel = (0, context_1.injectModel)(this._client.name);
|
|
611
|
-
const readModel = resourceModel.readModel;
|
|
612
|
-
const relationsModel = resourceModel.relationsModel;
|
|
613
|
-
const relationsConfig = resourceModel.config.relations;
|
|
614
|
-
for (const key in data) {
|
|
615
|
-
let value = data[key];
|
|
616
|
-
const relationSchema = (0, common_1.extractSchemaProperties)(relationsModel, key);
|
|
617
|
-
if (!relationSchema) {
|
|
618
|
-
// Set empty array or undefined value for null array type fields
|
|
619
|
-
if (value === null) {
|
|
620
|
-
if ((0, common_1.extractSchemaProperties)(readModel, key)?.type === 'array') {
|
|
621
|
-
relations[key] = action === 'update' ? [] : undefined;
|
|
622
|
-
}
|
|
623
|
-
else if (action === 'create') {
|
|
624
|
-
relations[key] = undefined;
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
// Skip mapping for non-relation fields
|
|
628
|
-
continue;
|
|
629
|
-
}
|
|
630
|
-
const config = relationsConfig?.[key];
|
|
631
|
-
const uniqueKey = config?.input?.uniqueKey || 'id';
|
|
632
|
-
const isArrayType = relationSchema.type === 'array';
|
|
633
|
-
// Normalize array values to single values if a value type is not an array
|
|
634
|
-
if (!isArrayType && (0, common_1.isArray)(value)) {
|
|
635
|
-
value = value[0];
|
|
636
|
-
}
|
|
637
|
-
// Set null values to undefined value for create actions. On update action
|
|
638
|
-
// they will be returned as disconnected relations.
|
|
639
|
-
if (action === 'create' && value === null) {
|
|
640
|
-
relations[key] = undefined;
|
|
641
|
-
continue;
|
|
642
|
-
}
|
|
643
|
-
// Normalize plain unique key values or arrays to object values
|
|
644
|
-
if (isArrayType) {
|
|
645
|
-
if ((0, common_1.isArray)(value) && !(0, common_1.isObject)([0])) {
|
|
646
|
-
value = value.map((v) => ({
|
|
647
|
-
[uniqueKey]: v
|
|
648
|
-
}));
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
else {
|
|
652
|
-
if (!(0, common_1.isObject)(value)) {
|
|
653
|
-
value = { [uniqueKey]: value };
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
// Map relation connections with an option to create a non-existing entity
|
|
657
|
-
if (config?.createIfNotExists && value) {
|
|
658
|
-
const createdBy = this.createdByConnect(config.model);
|
|
659
|
-
if (isArrayType) {
|
|
660
|
-
relations[key] = {
|
|
661
|
-
connectOrCreate: value.map((v) => ({
|
|
662
|
-
where: v[uniqueKey] ? { [uniqueKey]: v[uniqueKey] } : { id: 0 },
|
|
663
|
-
create: { ...v, createdBy, id: undefined }
|
|
664
|
-
}))
|
|
665
|
-
};
|
|
666
|
-
}
|
|
667
|
-
else {
|
|
668
|
-
relations[key] = {
|
|
669
|
-
connectOrCreate: {
|
|
670
|
-
where: value[uniqueKey]
|
|
671
|
-
? { [uniqueKey]: value[uniqueKey] }
|
|
672
|
-
: { id: 0 },
|
|
673
|
-
create: { ...value, createdBy, id: undefined }
|
|
674
|
-
}
|
|
675
|
-
};
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
else if (value) {
|
|
679
|
-
if (isArrayType && (0, common_1.isArray)(value)) {
|
|
680
|
-
relations[key] = {
|
|
681
|
-
connect: value.map((v) => ({ [uniqueKey]: v[uniqueKey] }))
|
|
682
|
-
};
|
|
683
|
-
}
|
|
684
|
-
else {
|
|
685
|
-
relations[key] = { connect: { [uniqueKey]: value[uniqueKey] } };
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
// Map relation disconnects if unique keys are no longer present or the
|
|
689
|
-
// new value is null. Delete relations if orphanRemoval is set to true.
|
|
690
|
-
// Relations that are not set on the current resource are left untouched,
|
|
691
|
-
// so no relation action is applied for them.
|
|
692
|
-
if (action === 'update') {
|
|
693
|
-
const currentValue = currentData[key];
|
|
694
|
-
const removalMethod = config?.orphanRemoval ? 'delete' : 'disconnect';
|
|
695
|
-
if (currentValue && isArrayType) {
|
|
696
|
-
const newValueKeys = value?.map((v) => v[uniqueKey]) ?? [];
|
|
697
|
-
const currentValueKeys = currentValue
|
|
698
|
-
.filter((v) => newValueKeys.indexOf(v[uniqueKey]) === -1)
|
|
699
|
-
.map((v) => ({
|
|
700
|
-
[uniqueKey]: v[uniqueKey]
|
|
701
|
-
}));
|
|
702
|
-
if (currentValueKeys.length > 0) {
|
|
703
|
-
relations[key] = {
|
|
704
|
-
[removalMethod]: currentValueKeys,
|
|
705
|
-
...(relations[key] ?? {})
|
|
706
|
-
};
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
else if (currentValue && value === null) {
|
|
710
|
-
relations[key] = {
|
|
711
|
-
[removalMethod]: { [uniqueKey]: currentValue[uniqueKey] },
|
|
712
|
-
...(relations[key] ?? {})
|
|
713
|
-
};
|
|
714
|
-
}
|
|
715
|
-
else if (!currentValue) {
|
|
716
|
-
relations[key] = undefined;
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
return relations;
|
|
721
|
-
}
|
|
722
|
-
createdByConnect(resourceName) {
|
|
723
|
-
const resourceModel = (0, context_1.injectModel)(resourceName ?? this._client.name, false);
|
|
724
|
-
if (resourceModel?.config.audit?.createdById === false) {
|
|
725
|
-
return undefined;
|
|
726
|
-
}
|
|
727
|
-
const currentUser = (0, security_1.currentAuthUser)();
|
|
728
|
-
return currentUser
|
|
729
|
-
? {
|
|
730
|
-
connect: {
|
|
731
|
-
id: currentUser.id
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
: undefined;
|
|
735
|
-
}
|
|
736
579
|
}
|
|
737
580
|
exports.ResourceService = ResourceService;
|