@appweaver/data 0.0.0-stage → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +61 -2
  3. package/config.d.ts +6 -0
  4. package/config.js +22 -0
  5. package/database/create-client.d.ts +11 -0
  6. package/database/create-client.js +145 -0
  7. package/database/database-config.d.ts +14 -0
  8. package/database/database-config.js +40 -0
  9. package/database/database-error.d.ts +49 -0
  10. package/database/database-error.js +216 -0
  11. package/database/index.d.ts +3 -0
  12. package/database/index.js +19 -0
  13. package/database/prisma-database.d.ts +33 -0
  14. package/database/prisma-database.js +178 -0
  15. package/database/run-transaction.d.ts +21 -0
  16. package/database/run-transaction.js +15 -0
  17. package/export/export-config.d.ts +7 -0
  18. package/export/export-config.js +17 -0
  19. package/export/export-service.d.ts +22 -0
  20. package/export/export-service.js +188 -0
  21. package/export/index.d.ts +1 -0
  22. package/export/index.js +17 -0
  23. package/factory/create-model.d.ts +11 -0
  24. package/factory/create-model.js +479 -0
  25. package/factory/create-policy.d.ts +12 -0
  26. package/factory/create-policy.js +20 -0
  27. package/factory/create-service.d.ts +14 -0
  28. package/factory/create-service.js +116 -0
  29. package/index.d.ts +22 -0
  30. package/index.js +38 -0
  31. package/internal.d.ts +2 -0
  32. package/internal.js +6 -0
  33. package/module.d.ts +3 -0
  34. package/module.js +81 -0
  35. package/package.json +67 -3
  36. package/prisma/client/browser.d.ts +39 -0
  37. package/prisma/client/browser.js +56 -0
  38. package/prisma/client/client.d.ts +58 -0
  39. package/prisma/client/client.js +70 -0
  40. package/prisma/client/commonInputTypes.d.ts +345 -0
  41. package/prisma/client/commonInputTypes.js +11 -0
  42. package/prisma/client/enums.d.ts +1 -0
  43. package/prisma/client/enums.js +11 -0
  44. package/prisma/client/internal/class.d.ts +198 -0
  45. package/prisma/client/internal/class.js +85 -0
  46. package/prisma/client/internal/prismaNamespace.d.ts +1125 -0
  47. package/prisma/client/internal/prismaNamespace.js +204 -0
  48. package/prisma/client/internal/prismaNamespaceBrowser.d.ts +137 -0
  49. package/prisma/client/internal/prismaNamespaceBrowser.js +175 -0
  50. package/prisma/client/models/ApiKey.d.ts +1167 -0
  51. package/prisma/client/models/ApiKey.js +2 -0
  52. package/prisma/client/models/ConnectedAccount.d.ts +1094 -0
  53. package/prisma/client/models/ConnectedAccount.js +2 -0
  54. package/prisma/client/models/File.d.ts +1291 -0
  55. package/prisma/client/models/File.js +2 -0
  56. package/prisma/client/models/OneTimeToken.d.ts +1055 -0
  57. package/prisma/client/models/OneTimeToken.js +2 -0
  58. package/prisma/client/models/Permission.d.ts +1209 -0
  59. package/prisma/client/models/Permission.js +2 -0
  60. package/prisma/client/models/Role.d.ts +1209 -0
  61. package/prisma/client/models/Role.js +2 -0
  62. package/prisma/client/models/Seeder.d.ts +1023 -0
  63. package/prisma/client/models/Seeder.js +2 -0
  64. package/prisma/client/models.d.ts +8 -0
  65. package/prisma/client/models.js +2 -0
  66. package/resource/load-data-resources.d.ts +13 -0
  67. package/resource/load-data-resources.js +37 -0
  68. package/resource/model-schema.d.ts +34 -0
  69. package/resource/model-schema.js +54 -0
  70. package/resource/resource-config.d.ts +16 -0
  71. package/resource/resource-config.js +62 -0
  72. package/resource/resource-loader.d.ts +25 -0
  73. package/resource/resource-loader.js +138 -0
  74. package/resource/resource-service.d.ts +462 -0
  75. package/resource/resource-service.js +1042 -0
  76. package/resource/schemas/index.d.ts +3 -0
  77. package/resource/schemas/index.js +19 -0
  78. package/resource/schemas/resource-aggregate-schema.d.ts +53 -0
  79. package/resource/schemas/resource-aggregate-schema.js +130 -0
  80. package/resource/schemas/resource-filter-schema.d.ts +39 -0
  81. package/resource/schemas/resource-filter-schema.js +153 -0
  82. package/resource/schemas/resource-sort-schema.d.ts +38 -0
  83. package/resource/schemas/resource-sort-schema.js +113 -0
  84. package/resource/utils/aggregate-util.d.ts +113 -0
  85. package/resource/utils/aggregate-util.js +322 -0
  86. package/resource/utils/cursor-util.d.ts +60 -0
  87. package/resource/utils/cursor-util.js +116 -0
  88. package/resource/utils/date-util.d.ts +22 -0
  89. package/resource/utils/date-util.js +112 -0
  90. package/resource/utils/delete-util.d.ts +131 -0
  91. package/resource/utils/delete-util.js +385 -0
  92. package/resource/utils/filter-util.d.ts +24 -0
  93. package/resource/utils/filter-util.js +289 -0
  94. package/resource/utils/prune-util.d.ts +11 -0
  95. package/resource/utils/prune-util.js +56 -0
  96. package/resource/utils/relation-util.d.ts +75 -0
  97. package/resource/utils/relation-util.js +400 -0
  98. package/resource/utils/sort-util.d.ts +31 -0
  99. package/resource/utils/sort-util.js +235 -0
  100. package/resource/utils/text-search-util.d.ts +14 -0
  101. package/resource/utils/text-search-util.js +39 -0
  102. package/resources.d.ts +10 -0
  103. package/resources.js +26 -0
  104. package/security/auth-config.d.ts +23 -0
  105. package/security/auth-config.js +43 -0
  106. package/security/create-auth-resources.d.ts +28 -0
  107. package/security/create-auth-resources.js +163 -0
  108. package/security/helper.d.ts +109 -0
  109. package/security/helper.js +298 -0
  110. package/security/resources/api-key/model.d.ts +2 -0
  111. package/security/resources/api-key/model.js +67 -0
  112. package/security/resources/api-key/policy.d.ts +2 -0
  113. package/security/resources/api-key/policy.js +27 -0
  114. package/security/resources/api-key/service.d.ts +3 -0
  115. package/security/resources/api-key/service.js +34 -0
  116. package/security/resources/connected-account/model.d.ts +2 -0
  117. package/security/resources/connected-account/model.js +57 -0
  118. package/security/resources/connected-account/service.d.ts +2 -0
  119. package/security/resources/connected-account/service.js +7 -0
  120. package/security/resources/one-time-token/model.d.ts +2 -0
  121. package/security/resources/one-time-token/model.js +36 -0
  122. package/security/resources/permission/model.d.ts +2 -0
  123. package/security/resources/permission/model.js +16 -0
  124. package/security/resources/role/model.d.ts +2 -0
  125. package/security/resources/role/model.js +22 -0
  126. package/security/store/database-security-store.d.ts +5 -0
  127. package/security/store/database-security-store.js +54 -0
  128. package/seeder/create-seeder.d.ts +29 -0
  129. package/seeder/create-seeder.js +28 -0
  130. package/seeder/index.d.ts +2 -0
  131. package/seeder/index.js +18 -0
  132. package/seeder/resources/seeder/model.d.ts +2 -0
  133. package/seeder/resources/seeder/model.js +38 -0
  134. package/seeder/seeder.d.ts +29 -0
  135. package/seeder/seeder.js +197 -0
  136. package/storage/file-service.d.ts +133 -0
  137. package/storage/file-service.js +586 -0
  138. package/storage/image-config.d.ts +3 -0
  139. package/storage/image-config.js +11 -0
  140. package/storage/resources/file/model.d.ts +2 -0
  141. package/storage/resources/file/model.js +80 -0
  142. package/types/auth.d.ts +23 -0
  143. package/types/auth.js +2 -0
  144. package/types/generated.d.ts +366 -0
  145. package/types/generated.js +3 -0
  146. package/types/storage.d.ts +35 -0
  147. package/types/storage.js +2 -0
  148. package/utils/export-util.d.ts +8 -0
  149. package/utils/export-util.js +26 -0
  150. package/utils/file-util.d.ts +106 -0
  151. package/utils/file-util.js +254 -0
  152. package/utils/model-util.d.ts +36 -0
  153. package/utils/model-util.js +154 -0
  154. package/utils/policy-util.d.ts +18 -0
  155. package/utils/policy-util.js +28 -0
  156. package/utils/virtual-util.d.ts +11 -0
  157. package/utils/virtual-util.js +53 -0
@@ -0,0 +1,1042 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ResourceService = void 0;
4
+ const common_1 = require("@appweaver/common");
5
+ const resource_config_1 = require("./resource-config");
6
+ const core_1 = require("@appweaver/core");
7
+ const policy_util_1 = require("../utils/policy-util");
8
+ const model_util_1 = require("../utils/model-util");
9
+ const virtual_util_1 = require("../utils/virtual-util");
10
+ const database_error_1 = require("../database/database-error");
11
+ const file_service_1 = require("../storage/file-service");
12
+ const delete_util_1 = require("./utils/delete-util");
13
+ const aggregate_util_1 = require("./utils/aggregate-util");
14
+ const relation_util_1 = require("./utils/relation-util");
15
+ const cursor_util_1 = require("./utils/cursor-util");
16
+ const filter_util_1 = require("./utils/filter-util");
17
+ const sort_util_1 = require("./utils/sort-util");
18
+ class ResourceService {
19
+ modelName;
20
+ /** @internal */
21
+ _db = (0, core_1.inject)(common_1.Database);
22
+ /** @internal */
23
+ _events = (0, core_1.inject)(common_1.Events);
24
+ /** @internal */
25
+ _cacheService = (0, core_1.inject)(core_1.CacheService);
26
+ /** @internal */
27
+ _modelKey;
28
+ /**
29
+ * Creates the service for the given resource model and resolves its database
30
+ * client from the Prisma client instance.
31
+ *
32
+ * @param {string} modelName The resource model name this service operates on,
33
+ * as defined by its model file (i.e. `User`, `Post`).
34
+ * @throws An error if no database client exists for the provided model name.
35
+ */
36
+ constructor(modelName) {
37
+ this.modelName = modelName;
38
+ this._modelKey = (0, common_1.uncapitalize)(modelName);
39
+ if (!this._client) {
40
+ throw new common_1.ConfigurationError(common_1.ErrorCode.ConfigurationInvalid, `ResourceService initialized with invalid model name: ${modelName}`);
41
+ }
42
+ }
43
+ /**
44
+ * The underlying Prisma model delegate for this resource, useful for
45
+ * executing custom database operations that the service methods do not cover.
46
+ *
47
+ * @return {ResourceClient} The resource client of the model this service was
48
+ * created for, bound to the current transaction (see `runTransaction`).
49
+ */
50
+ get client() {
51
+ return this._client;
52
+ }
53
+ /**
54
+ * Finds a single resource by its id, applying the read restrictions and the
55
+ * access check of this service, and including all relation and file fields
56
+ * configured for output on the find action. A resource event is emitted after
57
+ * a successful lookup.
58
+ *
59
+ * @param {ResourceId} id The id of the resource to find.
60
+ * @return {Promise<Object>} The found resource with its virtual fields and
61
+ * relation counts projected.
62
+ * @throws {@link ResourceError} `RESOURCE_NOT_FOUND` if the resource does not
63
+ * exist or is filtered out by the read restrictions, `RESOURCE_FORBIDDEN` if
64
+ * the access check denies it, and a {@link DatabaseError} on a database error.
65
+ */
66
+ async find(id) {
67
+ const restrictions = await this.applyReadRestrictions('find', id);
68
+ const includeRelations = (0, relation_util_1.mapRelationInclusions)(this._client.name, 'find');
69
+ let resource;
70
+ try {
71
+ resource = await this._client.findFirst({
72
+ where: { id, ...restrictions, ...(0, model_util_1.liveRecordFilter)(this._client.name) },
73
+ include: includeRelations
74
+ });
75
+ }
76
+ catch (e) {
77
+ throw this.databaseError(e, id);
78
+ }
79
+ if (!resource || resource.id !== id) {
80
+ throw new common_1.ResourceError(common_1.ErrorCode.ResourceNotFound, `${this._client.name} not found`, { model: this._client.name, id });
81
+ }
82
+ const access = await this.applyAccessCheck('find', resource);
83
+ if (!access) {
84
+ throw new common_1.ResourceError(common_1.ErrorCode.ResourceForbidden, `${this._client.name} access is forbidden`, { model: this._client.name, action: 'find' });
85
+ }
86
+ this._events.emitResourceEvent(this._client.name, 'find', {
87
+ current: resource
88
+ });
89
+ return this.projectResource(resource);
90
+ }
91
+ /**
92
+ * Queries a page of resources matching the provided filter. The filter is
93
+ * mapped to a database query, combined with the optional `searchText` full
94
+ * text search query and the read restrictions of this service, and executed
95
+ * together with the total count in a single transaction. A resource event is
96
+ * emitted after a successful query.
97
+ *
98
+ * @param {QueryOptions} [options] The query options:
99
+ * - `filter` The query filter object, supporting the logical (`_and`, `_or`,
100
+ * `_not`, `_nor`), comparison (`_eq`, `_ne`, `_gt`, `_gte`, `_lt`, `_lte`,
101
+ * `_in`, `_nin`, `_between`, `_like`, `_ilike`, `_starts`, `_ends`,
102
+ * `_contains`, `_exists`), list (`_has`, `_hasSome`, `_hasEvery`,
103
+ * `_isEmpty`), and relation (`_some`, `_every`, `_none`) operators, as well
104
+ * as plain field values. Its `searchText` property, if present, is passed to
105
+ * {@link ResourceService.textSearchQuery} instead of being matched as a
106
+ * field.
107
+ * - `page` The one-based page number of results to return, ignored when a
108
+ * cursor is given (default: `1`).
109
+ * - `size` The maximum number of results per page (default: `50`).
110
+ * - `sort` The fields to sort by, either as a comma-separated list where a
111
+ * field prefixed with `-` is sorted in descending order (i.e.
112
+ * `-createdAt,id`), or as an object of field directions (i.e.
113
+ * `{ createdAt: 'desc', id: 'asc' }`). Both forms support the fields of the
114
+ * included to-one relations, given with a dot notation (`author.createdAt`)
115
+ * or as a nested object (`{ author: { createdAt: 'desc' } }`) (default:
116
+ * `-createdAt`).
117
+ * - `cursor` The cursor of the page to return, as issued in the `nextCursor`
118
+ * or `prevCursor` of an earlier response, which also carries the direction
119
+ * the page runs in. Takes precedence over `page`.
120
+ * - `totalCount` Whether to count all matching resources, which costs a scan
121
+ * of every one of them (default: `true`).
122
+ * @return {Promise<QueryResponse<Object>>} The paged query response containing
123
+ * the returned resources, the count of the returned items, the cursors of the
124
+ * adjacent pages, and the total count unless it was opted out of.
125
+ * @throws {@link ResourceError} `RESOURCE_INVALID_SORT` if the sort input names
126
+ * a field that cannot be sorted by, `RESOURCE_INVALID_CURSOR` if the cursor
127
+ * was issued for another filter or sort order, and a {@link DatabaseError} on
128
+ * a database error.
129
+ */
130
+ async query(options = {}) {
131
+ const { filter = {}, page = 1, size = 50, sort = '-createdAt', cursor, totalCount = true } = options;
132
+ const query = await this.queryConditions('query', filter);
133
+ const includeRelations = (0, relation_util_1.mapRelationInclusions)(this._client.name, 'query');
134
+ const orderBy = (0, sort_util_1.mapStableSortValues)(sort, this._client.name, 'query');
135
+ // Binding to the mapped query rather than the filter also invalidates a
136
+ // cursor on a changed text search or read restriction
137
+ const fingerprint = (0, cursor_util_1.queryFingerprint)(this._client.name, query, orderBy);
138
+ const decodedCursor = (0, cursor_util_1.decodeCursor)(cursor, fingerprint);
139
+ // One record past the page tells whether a further page exists
140
+ const backward = decodedCursor?.backward === true;
141
+ const take = size > 0 ? size + 1 : 0;
142
+ const findMany = this._client.findMany({
143
+ where: { ...query },
144
+ include: includeRelations,
145
+ // A cursor skips the record it addresses, an offset the pages before it
146
+ cursor: decodedCursor ? { id: decodedCursor.id } : undefined,
147
+ skip: decodedCursor ? 1 : (page - 1) * size,
148
+ take: backward ? -take : take,
149
+ orderBy
150
+ });
151
+ let resources;
152
+ let count;
153
+ try {
154
+ if (totalCount) {
155
+ [resources, count] = await this._db
156
+ .client()
157
+ .$transaction([
158
+ findMany,
159
+ this._client.count({ where: { ...query } })
160
+ ]);
161
+ }
162
+ else {
163
+ resources = await findMany;
164
+ }
165
+ }
166
+ catch (e) {
167
+ throw this.databaseError(e);
168
+ }
169
+ // The over-fetched record leads a backward page and trails a forward one
170
+ const hasMore = resources.length > size;
171
+ if (hasMore) {
172
+ resources = backward
173
+ ? resources.slice(resources.length - size)
174
+ : resources.slice(0, size);
175
+ }
176
+ // The page a cursor was followed from always exists
177
+ const hasNext = backward || hasMore;
178
+ const hasPrev = backward ? hasMore : !!decodedCursor || page > 1;
179
+ this._events.emitResourceEvent(this._client.name, 'query', {
180
+ current: resources
181
+ });
182
+ return {
183
+ resultCount: resources.length,
184
+ totalCount: count ?? null,
185
+ ...(0, cursor_util_1.pageCursors)(resources, fingerprint, hasNext, hasPrev),
186
+ items: resources.map((resource) => this.projectResource(resource))
187
+ };
188
+ }
189
+ /**
190
+ * Finds the first resource matching the provided filter, applying the read
191
+ * restrictions of the query action and the access check of the find action,
192
+ * and including the relation and file fields configured for output on the
193
+ * find action. A resource event of the find action is emitted when a resource
194
+ * matches.
195
+ *
196
+ * @param {Object} [filter] The query filter object, mapped the same way as in
197
+ * {@link ResourceService.query}.
198
+ * @param {QuerySort} [sort] The fields to sort the matching resources by, of
199
+ * which the first one is returned, in the same form as in
200
+ * {@link ResourceService.query} (default: `-createdAt`).
201
+ * @return {Promise<Object|null>} The first matching resource with its virtual
202
+ * fields and relation counts projected, or null when none matches.
203
+ * @throws {@link ResourceError} `RESOURCE_FORBIDDEN` if the access check denies
204
+ * the matching resource, `RESOURCE_INVALID_SORT` if the sort input names a
205
+ * field that cannot be sorted by, and a {@link DatabaseError} on a database
206
+ * error.
207
+ */
208
+ async single(filter = {}, sort = '-createdAt') {
209
+ const where = await this.queryConditions('query', filter);
210
+ const includeRelations = (0, relation_util_1.mapRelationInclusions)(this._client.name, 'find');
211
+ const orderBy = (0, sort_util_1.mapStableSortValues)(sort, this._client.name, 'find');
212
+ let resource;
213
+ try {
214
+ resource = await this._client.findFirst({
215
+ where,
216
+ include: includeRelations,
217
+ orderBy
218
+ });
219
+ }
220
+ catch (e) {
221
+ throw this.databaseError(e);
222
+ }
223
+ if (!resource) {
224
+ return null;
225
+ }
226
+ const access = await this.applyAccessCheck('find', resource);
227
+ if (!access) {
228
+ throw new common_1.ResourceError(common_1.ErrorCode.ResourceForbidden, `${this._client.name} access is forbidden`, { model: this._client.name, action: 'find' });
229
+ }
230
+ this._events.emitResourceEvent(this._client.name, 'find', {
231
+ current: resource
232
+ });
233
+ return this.projectResource(resource);
234
+ }
235
+ /**
236
+ * Counts the resources matching the provided filter, applying the read
237
+ * restrictions of the query action.
238
+ *
239
+ * @param {Object} [filter] The query filter object, mapped the same way as in
240
+ * {@link ResourceService.query}.
241
+ * @return {Promise<number>} The number of matching resources.
242
+ * @throws {@link DatabaseError} On a database error.
243
+ */
244
+ async count(filter = {}) {
245
+ const where = await this.queryConditions('query', filter);
246
+ try {
247
+ return await this._client.count({ where });
248
+ }
249
+ catch (e) {
250
+ throw this.databaseError(e);
251
+ }
252
+ }
253
+ /**
254
+ * Checks whether any resource matches the provided filter, applying the read
255
+ * restrictions of the query action.
256
+ *
257
+ * @param {Object} [filter] The query filter object, mapped the same way as in
258
+ * {@link ResourceService.query}.
259
+ * @return {Promise<boolean>} True if at least one resource matches, otherwise
260
+ * false.
261
+ * @throws {@link DatabaseError} On a database error.
262
+ */
263
+ async exists(filter = {}) {
264
+ const where = await this.queryConditions('query', filter);
265
+ let resource;
266
+ try {
267
+ resource = await this._client.findFirst({ where, select: { id: true } });
268
+ }
269
+ catch (e) {
270
+ throw this.databaseError(e);
271
+ }
272
+ return resource !== null;
273
+ }
274
+ /**
275
+ * Aggregates resources matching the provided filter over a date range, both
276
+ * as a single overall result and as a series of results for the equally sized
277
+ * periods the range is split into. All aggregations are executed in a single
278
+ * transaction.
279
+ *
280
+ * @param {AggregateOptions} options The aggregation options:
281
+ * - `select` The aggregation operations to perform per field (i.e.
282
+ * `{ views: { count: true, sum: true } }`). Only the numeric fields of the
283
+ * model accept every operator, while its date fields accept `count`, `min`,
284
+ * `max`, `first` and `last`. The `first` and `last` operators take the value
285
+ * the earliest and the latest record of a period holds, which costs one
286
+ * additional query per period and boundary, skipped for the periods holding
287
+ * no record.
288
+ * - `filter` The query filter object, mapped the same way as in
289
+ * {@link ResourceService.query}.
290
+ * - `dateField` The date field the range is applied on (default:
291
+ * `createdAt`).
292
+ * - `from` The ISO date string of the range start. Defaults to seven days
293
+ * before the range end.
294
+ * - `to` The ISO date string of the range end. Defaults to the current date
295
+ * and time.
296
+ * - `step` The size of a single period in units of the automatically selected
297
+ * time unit. If not provided, one unit is used and the unit is derived from
298
+ * the range length (seconds up to a minute, minutes up to an hour, hours up
299
+ * to a day, days up to a month, months up to a year, and years beyond that).
300
+ * - `safeIncrement` Whether the period increments use the derived time unit
301
+ * and stay consistent across daylight saving time changes. When false, the
302
+ * step is interpreted in seconds (default: `true`).
303
+ * @return {Promise<AggregateResponse<Object>>} The aggregation response with
304
+ * the overall total and one result per period, each labeled with the median
305
+ * date of its period. It is typed by the fields the selection named, not by
306
+ * the whole model, whenever the selection is passed as an object literal or
307
+ * annotated with `satisfies`.
308
+ * @throws {@link ResourceError} `RESOURCE_INVALID_AGGREGATE` if the selection
309
+ * is empty or names a field or operator that cannot be aggregated, and a
310
+ * {@link DatabaseError} on a database error.
311
+ */
312
+ async aggregate(options) {
313
+ const { select, filter = {}, dateField = 'createdAt', from, to, step, safeIncrement = true } = options;
314
+ const { fromDate, toDate, ranges: dateRanges } = (0, aggregate_util_1.buildAggregationPeriods)(from, to, step, safeIncrement);
315
+ const operations = (0, aggregate_util_1.mapAggregationSelect)(select, this._client.name);
316
+ (0, aggregate_util_1.checkAggregationDateField)(dateField, this._client.name);
317
+ const query = await this.queryConditions('aggregate', filter);
318
+ const rangeQuery = (rangeFrom, rangeTo) => ({
319
+ AND: [query, { [dateField]: { gte: rangeFrom, lt: rangeTo } }]
320
+ });
321
+ // Aggregates a single range and reads the boundary records it holds the
322
+ // first and last values of which the database cannot aggregate
323
+ const aggregateRange = async (txModel, where) => {
324
+ const result = await txModel.aggregate({
325
+ ...operations.aggregate,
326
+ where
327
+ });
328
+ const boundaries = await (0, aggregate_util_1.readAggregationBoundaries)(txModel, where, dateField, operations, (0, aggregate_util_1.aggregationRecordCount)(result));
329
+ return { ...result, ...boundaries };
330
+ };
331
+ let total = {};
332
+ let items = [];
333
+ try {
334
+ [total, items] = await this._db.client().$transaction(async (tx) => {
335
+ const txModel = tx[this._client.name];
336
+ const overall = await aggregateRange(txModel, rangeQuery(fromDate, toDate));
337
+ // Skip executing the same query if only one range value is generated
338
+ if (dateRanges.length === 1) {
339
+ return [overall, [overall]];
340
+ }
341
+ const ranges = await Promise.all(dateRanges.map((dateRange) => aggregateRange(txModel, rangeQuery(dateRange.from, dateRange.to))));
342
+ return [overall, ranges];
343
+ });
344
+ }
345
+ catch (e) {
346
+ throw this.databaseError(e);
347
+ }
348
+ return {
349
+ total: (0, aggregate_util_1.mapAggregationResult)(total),
350
+ items: items.map((item, index) => ({
351
+ date: dateRanges[index].median,
352
+ result: (0, aggregate_util_1.mapAggregationResult)(item)
353
+ }))
354
+ };
355
+ }
356
+ /**
357
+ * Creates a new resource. The provided data is merged with the write
358
+ * restrictions, checked for access, sanitized against the model configuration
359
+ * and mapped to the relation write actions (connect, create, or
360
+ * connect-or-create) before the record is created. The `createdBy` audit
361
+ * relation is connected to the currently authenticated user when configured.
362
+ * The resource cache is invalidated and a resource event is emitted after a
363
+ * successful create.
364
+ *
365
+ * @param {Object} data The data of the resource to create, including any inline
366
+ * relation and file payloads.
367
+ * @return {Promise<Object>} The created resource with its virtual fields and
368
+ * relation counts projected.
369
+ * @throws {@link ResourceError} `RESOURCE_FORBIDDEN` if the access check denies
370
+ * the action, `RESOURCE_INVALID_RELATION` if an inline relation payload is
371
+ * missing required fields or the relation does not accept new records, and a
372
+ * {@link DatabaseError} on a database error, i.e. `DATABASE_UNIQUE_VIOLATION`.
373
+ */
374
+ async create(data) {
375
+ const createdBy = (0, relation_util_1.createdByConnect)(this._client.name);
376
+ const restrictions = await this.applyWriteRestrictions('create', data);
377
+ const createData = (0, common_1.removeUndefined)({
378
+ ...data,
379
+ ...restrictions
380
+ });
381
+ const access = await this.applyAccessCheck('create', createData);
382
+ if (!access) {
383
+ throw new common_1.ResourceError(common_1.ErrorCode.ResourceForbidden, `${this._client.name} create action is forbidden`, { model: this._client.name, action: 'create' });
384
+ }
385
+ const sanitizedData = this.sanitizeData('create', createData);
386
+ const connectRelations = (0, relation_util_1.mapRelationActions)(this._client.name, 'create', sanitizedData);
387
+ const includeRelations = (0, relation_util_1.mapRelationInclusions)(this._client.name, 'create');
388
+ await (0, delete_util_1.assertLiveRelationTargets)(this._db.client(), this._client.name, connectRelations);
389
+ let resource;
390
+ try {
391
+ resource = await this._client.create({
392
+ data: {
393
+ ...sanitizedData,
394
+ ...connectRelations,
395
+ createdBy
396
+ },
397
+ include: includeRelations
398
+ });
399
+ }
400
+ catch (e) {
401
+ throw this.databaseError(e);
402
+ }
403
+ await this._cacheService.invalidateCache(this._client.name, 'create');
404
+ this._events.emitResourceEvent(this._client.name, 'create', {
405
+ current: resource
406
+ });
407
+ return this.projectResource(resource);
408
+ }
409
+ /**
410
+ * Updates an existing resource by its id. The current record is loaded with
411
+ * the read restrictions applied and checked for access, then updated with the
412
+ * data merged with the write restrictions inside a single transaction.
413
+ * Relations missing from the new value are disconnected, or deleted when
414
+ * `orphanRemoval` is configured for them, the same way a delete does. The resource cache is invalidated and a
415
+ * resource event carrying both the previous and the current state is emitted
416
+ * after a successful update.
417
+ *
418
+ * @param {ResourceId} id The id of the resource to update.
419
+ * @param {Object} data The partial data to update the resource with, including
420
+ * any inline relation and file payloads.
421
+ * @return {Promise<Object>} The updated resource with its virtual fields and
422
+ * relation counts projected.
423
+ * @throws {@link ResourceError} `RESOURCE_NOT_FOUND` if the resource does not
424
+ * exist or is filtered out by the read restrictions, `RESOURCE_FORBIDDEN` if
425
+ * the access check denies the action, `RESOURCE_INVALID_RELATION` if an
426
+ * inline relation payload is missing required fields or the relation does not
427
+ * accept new records, `RESOURCE_DELETE_RESTRICTED` if a live record
428
+ * references a soft deleted orphan through a restricting relation, and a
429
+ * {@link DatabaseError} on a database error, i.e. `DATABASE_UNIQUE_VIOLATION`.
430
+ */
431
+ async update(id, data) {
432
+ const { current } = await this.updateWithPrevious(id, data);
433
+ return current;
434
+ }
435
+ /**
436
+ * Replaces an existing resource by its id as a whole. The updatable scalar
437
+ * fields missing from the data are reset to their configured default value,
438
+ * or to null when they are optional (see
439
+ * {@link ResourceService.replacementData}), and the resource is then updated
440
+ * the same way as {@link ResourceService.update}, applying the same policies,
441
+ * invalidating the same cache, and emitting the same update event. The
442
+ * optional relations missing from the data are left untouched, and a null or
443
+ * an empty list clears them.
444
+ *
445
+ * @param {ResourceId} id The id of the resource to replace.
446
+ * @param {Object} data The new state of the resource, including any inline
447
+ * relation payloads.
448
+ * @return {Promise<Object>} The replaced resource with its virtual fields and
449
+ * relation counts projected.
450
+ * @throws The same errors as {@link ResourceService.update}.
451
+ */
452
+ async replace(id, data) {
453
+ const { current } = await this.updateWithPrevious(id, this.replacementData(data));
454
+ return current;
455
+ }
456
+ /**
457
+ * Deletes an existing resource by its id. The current record is loaded with
458
+ * the read restrictions applied and checked for access before it is deleted
459
+ * inside a single transaction.
460
+ *
461
+ * A model with `softDelete` enabled marks the record with the `deletedAt`
462
+ * and `deletedById` columns instead, soft deleting its cascade as well.
463
+ *
464
+ * The stored files of the deleted records follow the `onResourceDeleted`, or
465
+ * for soft deleted records the `onResourceSoftDeleted`, option of each file
466
+ * field: kept files are marked deleted in the transaction and no longer
467
+ * served, the others are removed once it commits. The cache of every affected
468
+ * model is invalidated and a resource event is emitted.
469
+ *
470
+ * @param {ResourceId} id The id of the resource to delete.
471
+ * @return {Promise<Object>} The deleted resource with its virtual fields and
472
+ * relation counts projected.
473
+ * @throws {@link ResourceError} `RESOURCE_NOT_FOUND` if the resource does not
474
+ * exist or is filtered out by the read restrictions, `RESOURCE_FORBIDDEN` if
475
+ * the access check denies it, `RESOURCE_DELETE_RESTRICTED` if a restricting
476
+ * relation still references a soft deleted resource, and a
477
+ * {@link DatabaseError} on a database error, i.e.
478
+ * `DATABASE_FOREIGN_KEY_VIOLATION` for a restricting relation of a removed
479
+ * resource.
480
+ */
481
+ async delete(id) {
482
+ const restrictions = await this.applyReadRestrictions('delete', id);
483
+ const softDelete = (0, common_1.hasSoftDelete)((0, core_1.injectModel)(this._client.name).config);
484
+ const includeRelations = (0, relation_util_1.mapRelationInclusions)(this._client.name, 'delete');
485
+ let resource;
486
+ let deleted;
487
+ try {
488
+ [resource, deleted] = await this._db.client().$transaction(async (tx) => {
489
+ const txModel = tx[this._client.name];
490
+ // A soft deleted record is read with its relations up front, as a
491
+ // database delete returns them before the cascade removes them
492
+ const current = await txModel.findFirst({
493
+ where: {
494
+ id,
495
+ ...restrictions,
496
+ ...(0, model_util_1.liveRecordFilter)(this._client.name)
497
+ },
498
+ include: softDelete ? includeRelations : undefined
499
+ });
500
+ if (!current || current.id !== id) {
501
+ throw new common_1.ResourceError(common_1.ErrorCode.ResourceNotFound, `${this._client.name} not found`, { model: this._client.name, id });
502
+ }
503
+ const access = await this.applyAccessCheck('delete', current);
504
+ if (!access) {
505
+ throw new common_1.ResourceError(common_1.ErrorCode.ResourceForbidden, `${this._client.name} delete action is forbidden`, { model: this._client.name, action: 'delete' });
506
+ }
507
+ const data = (0, delete_util_1.softDeleteData)();
508
+ let result;
509
+ let records;
510
+ if (softDelete) {
511
+ records = await (0, delete_util_1.softDeleteCascade)(tx, this._client.name, [id], data);
512
+ const updated = await txModel.update({ where: { id }, data });
513
+ result = { ...current, ...updated };
514
+ }
515
+ else {
516
+ records = await (0, delete_util_1.cascadedRecords)(tx, this._client.name, [id]);
517
+ result = await txModel.delete({
518
+ where: { id },
519
+ include: includeRelations
520
+ });
521
+ }
522
+ // Merged rather than spread, since a self cascade holds the same model
523
+ (0, delete_util_1.mergeAffectedRecords)(records, { [this._client.name]: [id] });
524
+ const removed = softDelete
525
+ ? { soft: records, hard: {} }
526
+ : { soft: {}, hard: records };
527
+ await (0, delete_util_1.retainDeletedFiles)(tx, removed, data);
528
+ return [result, removed];
529
+ });
530
+ }
531
+ catch (e) {
532
+ throw this.databaseError(e, id);
533
+ }
534
+ await this.cleanupDeletedRecords(deleted);
535
+ this._events.emitResourceEvent(this._client.name, 'delete', {
536
+ current: resource
537
+ });
538
+ return this.projectResource(resource);
539
+ }
540
+ /**
541
+ * Creates many resources in a single transaction, one after another the same
542
+ * way as {@link ResourceService.create}, so every item runs through the same
543
+ * policies and hooks. Either every resource is created or, when any item
544
+ * fails, none: the error of the failed item is thrown and the transaction is
545
+ * rolled back. The cache invalidation and the create resource event of every
546
+ * item wait for the commit, and are dropped on a rollback.
547
+ *
548
+ * @param {Object[]} items The data of every resource to create.
549
+ * @return {Promise<Object[]>} The created resources, in the order of the
550
+ * items.
551
+ * @throws The error of the first item that failed, the same errors as
552
+ * {@link ResourceService.create}.
553
+ */
554
+ async bulkCreate(items) {
555
+ return this.bulk(items, (item) => this.create(item));
556
+ }
557
+ /**
558
+ * Updates the given fields of many resources in a single transaction, one
559
+ * after another the same way as {@link ResourceService.update}. Either every
560
+ * resource is updated or none, see {@link ResourceService.bulkCreate}.
561
+ *
562
+ * @param {BulkItem[]} items The id and the partial data of every resource.
563
+ * @return {Promise<Object[]>} The updated resources, in the order of the
564
+ * items.
565
+ * @throws The error of the first item that failed, the same errors as
566
+ * {@link ResourceService.update}.
567
+ */
568
+ async bulkUpdate(items) {
569
+ return this.bulk(items, (item) => this.update(item.id, item.data));
570
+ }
571
+ /**
572
+ * Replaces many resources as a whole in a single transaction, one after
573
+ * another the same way as {@link ResourceService.replace}. Either every
574
+ * resource is replaced or none, see {@link ResourceService.bulkCreate}.
575
+ *
576
+ * @param {BulkItem[]} items The id and the new state of every resource.
577
+ * @return {Promise<Object[]>} The replaced resources, in the order of the
578
+ * items.
579
+ * @throws The error of the first item that failed, the same errors as
580
+ * {@link ResourceService.replace}.
581
+ */
582
+ async bulkReplace(items) {
583
+ return this.bulk(items, (item) => this.replace(item.id, item.data));
584
+ }
585
+ /**
586
+ * Deletes many resources in a single transaction, one after another the same
587
+ * way as {@link ResourceService.delete}, cascades and stored files included.
588
+ * Either every resource is deleted or none, see
589
+ * {@link ResourceService.bulkCreate}. Every id must exist up front, while an
590
+ * id the cascade of an earlier one already removed is skipped.
591
+ *
592
+ * @param {ResourceId[]} ids The ids of the resources to delete.
593
+ * @return {Promise<Object[]>} The resources deleted by their own id, in the
594
+ * order of the ids.
595
+ * @throws {@link ResourceError} `RESOURCE_NOT_FOUND` if an id does not exist,
596
+ * and otherwise the error of the first resource that failed, the same errors
597
+ * as {@link ResourceService.delete}.
598
+ */
599
+ async bulkDelete(ids) {
600
+ return this.bulk(ids, async (id) => (await this.liveIds([id])).length > 0 ? this.delete(id) : undefined, async () => {
601
+ const live = (await this.liveIds(ids)).map(String);
602
+ const missing = ids.find((id) => !live.includes(String(id)));
603
+ if (missing !== undefined) {
604
+ throw new common_1.ResourceError(common_1.ErrorCode.ResourceNotFound, `${this._client.name} not found`, { model: this._client.name, id: missing });
605
+ }
606
+ });
607
+ }
608
+ /**
609
+ * Updates an existing resource the same way as {@link ResourceService.update},
610
+ * returning the state the resource had before the update along with the
611
+ * updated one.
612
+ *
613
+ * @param {ResourceId} id The id of the resource to update.
614
+ * @param {Object} data The partial data to update the resource with.
615
+ * @return {Promise<{ previous: Object, current: Object }>} The resource before
616
+ * and after the update, both with their virtual fields and relation counts
617
+ * projected.
618
+ */
619
+ async updateWithPrevious(id, data) {
620
+ const readRestrictions = await this.applyReadRestrictions('update', {
621
+ id,
622
+ ...data
623
+ });
624
+ const writeRestrictions = await this.applyWriteRestrictions('update', {
625
+ id,
626
+ ...data
627
+ });
628
+ const updateData = (0, common_1.removeUndefined)({
629
+ ...data,
630
+ ...writeRestrictions
631
+ });
632
+ const sanitizedData = this.sanitizeData('update', updateData);
633
+ const includeRelations = (0, relation_util_1.mapRelationInclusions)(this._client.name, 'update');
634
+ let updateResource;
635
+ let resource;
636
+ let orphans;
637
+ try {
638
+ [updateResource, resource, orphans] = await this._db
639
+ .client()
640
+ .$transaction(async (tx) => {
641
+ const txModel = tx[this._client.name];
642
+ const current = await txModel.findFirst({
643
+ where: {
644
+ id,
645
+ ...readRestrictions,
646
+ ...(0, model_util_1.liveRecordFilter)(this._client.name)
647
+ },
648
+ include: includeRelations
649
+ });
650
+ if (!current || current.id !== id) {
651
+ throw new common_1.ResourceError(common_1.ErrorCode.ResourceNotFound, `${this._client.name} not found`, { model: this._client.name, id });
652
+ }
653
+ const access = await this.applyAccessCheck('update', current);
654
+ if (!access) {
655
+ throw new common_1.ResourceError(common_1.ErrorCode.ResourceForbidden, `${this._client.name} update action is forbidden`, { model: this._client.name, action: 'update' });
656
+ }
657
+ const setRelations = (0, relation_util_1.mapRelationActions)(this._client.name, 'update', sanitizedData, current);
658
+ await (0, delete_util_1.assertLiveRelationTargets)(tx, this._client.name, setRelations);
659
+ // Soft deleted orphans are marked before the update reads the
660
+ // relations back, so the response no longer holds them
661
+ const deleteData = (0, delete_util_1.softDeleteData)();
662
+ const removed = await (0, delete_util_1.removeOrphans)(tx, this._client.name, setRelations, deleteData);
663
+ await (0, delete_util_1.retainDeletedFiles)(tx, removed, deleteData);
664
+ const updated = await txModel.update({
665
+ where: { id },
666
+ include: includeRelations,
667
+ data: { ...sanitizedData, ...setRelations }
668
+ });
669
+ return [current, updated, removed];
670
+ });
671
+ }
672
+ catch (e) {
673
+ throw this.databaseError(e, id);
674
+ }
675
+ await this._cacheService.invalidateCache(this._client.name, 'update');
676
+ await this.cleanupDeletedRecords(orphans);
677
+ this._events.emitResourceEvent(this._client.name, 'update', {
678
+ previous: updateResource,
679
+ current: resource
680
+ });
681
+ return {
682
+ previous: this.projectResource(updateResource),
683
+ current: this.projectResource(resource)
684
+ };
685
+ }
686
+ /**
687
+ * Completes the data of a replacement with the updatable scalar fields it
688
+ * leaves out: each one is set to the `default` value of its configuration,
689
+ * or to null when it is optional. A required field without a default value,
690
+ * which the request validation of the PUT route never lets a request leave
691
+ * out, is left untouched, and so are the hidden fields, the fields the
692
+ * `update` option of the model excludes, the virtual fields, and the
693
+ * relations.
694
+ *
695
+ * @param {Object} data The data of the replacement.
696
+ * @return {Object} A shallow copy of the data, with the missing fields set.
697
+ */
698
+ replacementData(data) {
699
+ const resourceModel = (0, core_1.injectModel)(this._client.name);
700
+ const scalars = resourceModel.config.scalars ?? {};
701
+ const fields = (0, common_1.extractSchemaProperties)(resourceModel.replaceOneModel) ?? {};
702
+ const replacement = { ...data };
703
+ for (const name of Object.keys(fields)) {
704
+ const scalar = scalars[name];
705
+ if (!scalar || replacement[name] !== undefined) {
706
+ continue;
707
+ }
708
+ if (scalar.default !== undefined) {
709
+ replacement[name] = structuredClone(scalar.default);
710
+ }
711
+ else if (scalar.required === false) {
712
+ replacement[name] = null;
713
+ }
714
+ }
715
+ return replacement;
716
+ }
717
+ /**
718
+ * This method should be overridden with custom logic for restricting read
719
+ * operations on specific data for currently logged-in user and other
720
+ * authorization rules. The returned object will be applied as a filter on
721
+ * all actions (except create action) which will prevent unwanted data access
722
+ * and modifications. This method can also cancel the current action by
723
+ * throwing an error, i.e. a {@link ResourceError} with the
724
+ * `RESOURCE_FORBIDDEN` code or an `ApplicationError` with a code of the
725
+ * application.
726
+ *
727
+ * Not called for the calls run with internal access (see `withoutPolicies`).
728
+ *
729
+ * @param {ActionType} action The called action method on this service (find,
730
+ * query, aggregate, update, or delete)
731
+ * @param {Object|ResourceId} data The passed data to the called function can be
732
+ * number or object. If the data is a type of number, then it represents the
733
+ * resource id, otherwise it depends on the action and can be one of the
734
+ * following:
735
+ *
736
+ * - query and aggregate -> filter object
737
+ * - update -> combined id and the data object (i.e. { id, ...data })
738
+ *
739
+ * For other actions (find and delete) it represents the resource id.
740
+ * @return {Promise<Object>} The database-level `where` conditions containing
741
+ * additional query restrictions. The returned object is applied directly to
742
+ * the database query, so it uses the native Prisma filter syntax.
743
+ */
744
+ async readRestrictions(action, data) {
745
+ return {};
746
+ }
747
+ /**
748
+ * This method should be overridden with custom logic for restricting write
749
+ * operations on specific data for currently logged-in users and other
750
+ * authorization rules. The returned object will be applied as a filter on
751
+ * all actions (create and update) to ensure that users can only modify
752
+ * data they are authorized to access. This method can also cancel the
753
+ * current action by throwing an error, i.e. a {@link ResourceError} with the
754
+ * `RESOURCE_FORBIDDEN` code or an `ApplicationError` with a code of the
755
+ * application.
756
+ *
757
+ * Not called for the calls run with internal access (see `withoutPolicies`).
758
+ *
759
+ * @param {'create'|'update'} action The called action method on this service
760
+ * (create or update)
761
+ * @param {Object} data The passed data to the called function, which should be
762
+ * an object representing the resource data to be created or updated. For
763
+ * update operations, this object will also include the resource ID.
764
+ *
765
+ * @return {Promise<Object>} A partial object containing additional data
766
+ * restrictions to be applied, or an empty object if no restrictions are
767
+ * necessary.
768
+ */
769
+ async writeRestrictions(action, data) {
770
+ return {};
771
+ }
772
+ /**
773
+ * This method should provide logic for checking the access permissions for
774
+ * the provided resource object. If the resource should not be accessible by
775
+ * a currently authenticated user or other logic, this method should return
776
+ * false. Otherwise, it returns true and continues with the request execution.
777
+ *
778
+ * Not called for the calls run with internal access (see `withoutPolicies`).
779
+ *
780
+ * @param {ActionType} action The called action method on this service (find,
781
+ * query, aggregate, create, update, or delete)
782
+ * @param {Object} resource The resource object that is being checked for access.
783
+ * @return {Promise<boolean>} True if the access for resource is granted, false
784
+ * otherwise.
785
+ */
786
+ async checkAccess(action, resource) {
787
+ return true;
788
+ }
789
+ /**
790
+ * Constructs a query object for performing a text search on resources in the
791
+ * database based on the provided search text. This method will transform the
792
+ * search text into a format suitable for text search functionality, returning
793
+ * a query object that can be used to filter results.
794
+ *
795
+ * @param {string} searchText The text string used for searching resources.
796
+ * @return {Object} A query object that represents the conditions for the text
797
+ * search operation, using the native Prisma filter syntax. This will be used
798
+ * by the database query methods to retrieve matching resources.
799
+ */
800
+ textSearchQuery(searchText) {
801
+ return {};
802
+ }
803
+ /**
804
+ * Returns the read restrictions of {@link ResourceService.readRestrictions},
805
+ * or none when the call runs with internal access (see `withoutPolicies`).
806
+ *
807
+ * @internal
808
+ */
809
+ async applyReadRestrictions(action, data) {
810
+ return (0, policy_util_1.arePoliciesSkipped)() ? {} : this.readRestrictions(action, data);
811
+ }
812
+ /**
813
+ * Returns the write restrictions of {@link ResourceService.writeRestrictions},
814
+ * or none when the call runs with internal access (see `withoutPolicies`).
815
+ *
816
+ * @internal
817
+ */
818
+ async applyWriteRestrictions(action, data) {
819
+ return (0, policy_util_1.arePoliciesSkipped)() ? {} : this.writeRestrictions(action, data);
820
+ }
821
+ /**
822
+ * Runs the access check of {@link ResourceService.checkAccess}, which always
823
+ * grants the access when the call runs with internal access (see
824
+ * `withoutPolicies`).
825
+ *
826
+ * @internal
827
+ */
828
+ async applyAccessCheck(action, resource) {
829
+ return (0, policy_util_1.arePoliciesSkipped)() || this.checkAccess(action, resource);
830
+ }
831
+ /**
832
+ * Builds the database conditions of a query filter: the mapped filter, its
833
+ * text search, the read restrictions of the action, and the conditions
834
+ * hiding the soft deleted records.
835
+ *
836
+ * @internal
837
+ */
838
+ async queryConditions(action, filter) {
839
+ const restrictions = await this.applyReadRestrictions(action, filter);
840
+ const textSearch = this.extractTextSearchQuery(filter);
841
+ const mappedFilter = (0, filter_util_1.mapQueryFilter)(filter, this._client.name);
842
+ return {
843
+ AND: [mappedFilter, textSearch, restrictions, ...this.liveFilters()]
844
+ };
845
+ }
846
+ /**
847
+ * Removes the `searchText` property from the provided filter and converts it
848
+ * into a text search query using
849
+ * {@link ResourceService.textSearchQuery}. The filter object is mutated so the
850
+ * search text is not matched as a regular resource field.
851
+ *
852
+ * @param {Object} filter The request filter object, possibly containing a
853
+ * `searchText` property. The property is deleted from this object when present.
854
+ * @return {Object} The text search query for the extracted search text, or an
855
+ * empty object if the filter contains no search text.
856
+ */
857
+ extractTextSearchQuery(filter) {
858
+ if (filter.searchText) {
859
+ const searchQuery = this.textSearchQuery(filter.searchText);
860
+ delete filter.searchText;
861
+ return searchQuery;
862
+ }
863
+ return {};
864
+ }
865
+ /**
866
+ * Prepares a resource for the response by resolving the virtual fields of its
867
+ * model and flattening the Prisma `_count` aggregation into the individual
868
+ * relation count fields (i.e. `_count.posts` becomes `postsCount`).
869
+ *
870
+ * @param {Object} resource The resource object as returned by the database
871
+ * client.
872
+ * @return {Object} The same resource with the virtual fields resolved and, if a
873
+ * `_count` selection was present, with a count property per counted relation
874
+ * and the `_count` property removed.
875
+ */
876
+ projectResource(resource) {
877
+ const projectedResource = (0, virtual_util_1.projectVirtualFields)((0, delete_util_1.hideDeletedRelations)(resource, this._client.name), this._client.name);
878
+ if (!(0, common_1.isPlainObject)(projectedResource['_count'])) {
879
+ return projectedResource;
880
+ }
881
+ // Create new relation count properties on the resource object
882
+ for (const [key, count] of Object.entries(projectedResource['_count'])) {
883
+ projectedResource[(0, common_1.countFieldName)(key)] = count;
884
+ }
885
+ delete projectedResource['_count'];
886
+ return projectedResource;
887
+ }
888
+ /**
889
+ * Lists the conditions hiding the soft deleted resources from a query. It is
890
+ * empty for a model that deletes its records, so its query and the cursors
891
+ * bound to it stay the same.
892
+ *
893
+ * @return {Object[]} The `deletedAt: null` condition, or no condition.
894
+ */
895
+ liveFilters() {
896
+ const liveFilter = (0, model_util_1.liveRecordFilter)(this._client.name);
897
+ return Object.keys(liveFilter).length > 0 ? [liveFilter] : [];
898
+ }
899
+ /**
900
+ * Cleans up after records were deleted, once the deleting transaction
901
+ * commits: removes the stored files their fields do not keep, since the
902
+ * storage cannot take part in the transaction, and invalidates the cache of
903
+ * every model they belong to.
904
+ *
905
+ * @param {DeletedRecords} deleted The soft deleted and the removed records.
906
+ */
907
+ async cleanupDeletedRecords(deleted) {
908
+ const fileService = (0, core_1.inject)(file_service_1.FileService, false);
909
+ const groups = [
910
+ [true, deleted.soft],
911
+ [false, deleted.hard]
912
+ ];
913
+ for (const [softDeleted, records] of groups) {
914
+ for (const [modelName, ids] of Object.entries(records)) {
915
+ await fileService?.deleteResourcesFiles(modelName, ids, softDeleted);
916
+ await this._cacheService.invalidateCache(modelName, 'delete');
917
+ }
918
+ }
919
+ }
920
+ /**
921
+ * Prepares a write payload for the database by removing the virtual fields
922
+ * that have no column, filling in default values for the hidden required
923
+ * scalars of a create action, and recursively applying the same rules to the
924
+ * nested relation and file payloads. Unique key values, arrays of them and null
925
+ * values are left untouched, so the relation actions can still map them to the
926
+ * connect and disconnect operations.
927
+ *
928
+ * @param {'create'|'update'} action The write action the payload is sanitized
929
+ * for. Default values for the hidden required scalars are only applied on a
930
+ * create action.
931
+ * @param {Object} data The write payload to sanitize.
932
+ * @param {string} [resourceName] The name of the model the payload belongs to.
933
+ * Defaults to the model of this service and is set to the related model name
934
+ * when recursing into a nested relation or file payload.
935
+ * @return {Object} A shallow copy of the payload without the virtual fields,
936
+ * with the missing hidden required scalars defaulted, and with the nested
937
+ * payloads sanitized against their own models.
938
+ */
939
+ sanitizeData(action, data, resourceName) {
940
+ const sanitizedData = { ...data };
941
+ const resourceModel = (0, core_1.injectModel)(resourceName ?? this._client.name, false);
942
+ const relationsModel = resourceModel?.relationsModel;
943
+ const filesModel = resourceModel?.filesModel;
944
+ // Delete virtual fields from data object to avoid database errors
945
+ for (const fieldName of Object.keys(resourceModel?.config?.virtual ?? {})) {
946
+ delete sanitizedData[fieldName];
947
+ }
948
+ // Map default values for hidden scalars if a property is required without
949
+ // a provided default value, and scalar values are not set yet, this is only
950
+ // required for create actions
951
+ for (const [fieldName, scalar] of Object.entries(resourceModel?.config?.scalars ?? {})) {
952
+ if (action === 'create' &&
953
+ scalar.hidden &&
954
+ scalar.required !== false &&
955
+ scalar.default === undefined &&
956
+ sanitizedData[fieldName] === undefined) {
957
+ sanitizedData[fieldName] = (0, common_1.defaultScalarValue)(scalar);
958
+ }
959
+ }
960
+ // Recursively sanitize nested objects and arrays of objects
961
+ for (const key in sanitizedData) {
962
+ const value = sanitizedData[key];
963
+ const relationSchema = (0, common_1.extractSchemaProperties)(relationsModel, key);
964
+ const fileSchema = (0, common_1.extractSchemaProperties)(filesModel, key);
965
+ // Only nested resource payloads are sanitized. Unique key values, arrays
966
+ // of them and null values are left untouched, so the relation actions can
967
+ // still map them to connect and disconnect operations.
968
+ if ((0, common_1.isPlainObject)(value) || ((0, common_1.isArray)(value) && (0, common_1.isPlainObject)(value[0]))) {
969
+ const resourceName = (0, common_1.extractResourceName)(relationSchema ?? fileSchema);
970
+ if (resourceName) {
971
+ sanitizedData[key] = (0, common_1.isArray)(value)
972
+ ? value.map((item) => this.sanitizeData(action, item, resourceName))
973
+ : this.sanitizeData(action, value, resourceName);
974
+ }
975
+ }
976
+ }
977
+ return sanitizedData;
978
+ }
979
+ /**
980
+ * Runs the operation of every item one after another in a single
981
+ * transaction, or in the current one, which rolls back on the first error.
982
+ *
983
+ * @internal
984
+ */
985
+ async bulk(items, operation, prepare) {
986
+ try {
987
+ return await this._db.client().$transaction(async () => {
988
+ await prepare?.();
989
+ const results = [];
990
+ for (const item of items) {
991
+ const result = await operation(item);
992
+ if (result !== undefined) {
993
+ results.push(result);
994
+ }
995
+ }
996
+ return results;
997
+ }, { timeout: resource_config_1.resourceConfig.BULK_TRANSACTION_TIMEOUT });
998
+ }
999
+ catch (e) {
1000
+ // The errors of the items are translated already, which leaves the
1001
+ // errors of the transaction itself, i.e. its timeout
1002
+ throw this.databaseError(e);
1003
+ }
1004
+ }
1005
+ /**
1006
+ * Returns the ids of the given ones that exist and are not soft deleted,
1007
+ * regardless of the read restrictions.
1008
+ *
1009
+ * @internal
1010
+ */
1011
+ async liveIds(ids) {
1012
+ const records = await this._client.findMany({
1013
+ where: { id: { in: ids }, ...(0, model_util_1.liveRecordFilter)(this._client.name) },
1014
+ select: { id: true }
1015
+ });
1016
+ return records.map((record) => record.id);
1017
+ }
1018
+ /**
1019
+ * The model delegate of the current transaction, or of the connection outside
1020
+ * of one, resolved per call.
1021
+ *
1022
+ * @internal
1023
+ */
1024
+ get _client() {
1025
+ return this._db.client()[this._modelKey];
1026
+ }
1027
+ /**
1028
+ * Translates the error of a database operation, a record removed after it
1029
+ * was read, i.e. by a concurrent delete, being one not found.
1030
+ *
1031
+ * @internal
1032
+ */
1033
+ databaseError(error, id) {
1034
+ const translated = (0, database_error_1.toDatabaseError)(error, this._client.name);
1035
+ if ((0, common_1.isAppweaverError)(translated) &&
1036
+ translated.is(common_1.ErrorCode.DatabaseRecordNotFound)) {
1037
+ return new common_1.ResourceError(common_1.ErrorCode.ResourceNotFound, `${this._client.name} not found`, { model: this._client.name, id }, { cause: translated.cause });
1038
+ }
1039
+ return translated;
1040
+ }
1041
+ }
1042
+ exports.ResourceService = ResourceService;