@zenstackhq/server 1.0.0-alpha.99 → 1.0.0-beta.2

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 (49) hide show
  1. package/api/rest/index.d.ts +19 -0
  2. package/api/rest/index.js +1271 -0
  3. package/api/rest/index.js.map +1 -0
  4. package/api/rpc/index.d.ts +2 -0
  5. package/api/rpc/index.js +166 -0
  6. package/api/rpc/index.js.map +1 -0
  7. package/api/utils.d.ts +18 -0
  8. package/api/utils.js +82 -0
  9. package/api/utils.js.map +1 -0
  10. package/express/index.d.ts +1 -0
  11. package/express/index.js +15 -0
  12. package/express/index.js.map +1 -1
  13. package/express/middleware.d.ts +2 -12
  14. package/express/middleware.js +50 -15
  15. package/express/middleware.js.map +1 -1
  16. package/fastify/index.d.ts +1 -0
  17. package/fastify/index.js +15 -0
  18. package/fastify/index.js.map +1 -1
  19. package/fastify/plugin.d.ts +2 -12
  20. package/fastify/plugin.js +35 -20
  21. package/fastify/plugin.js.map +1 -1
  22. package/next/app-route-handler.d.ts +15 -0
  23. package/next/app-route-handler.js +85 -0
  24. package/next/app-route-handler.js.map +1 -0
  25. package/next/index.d.ts +38 -0
  26. package/next/index.js +18 -0
  27. package/next/index.js.map +1 -0
  28. package/next/pages-route-handler.d.ts +9 -0
  29. package/next/pages-route-handler.js +73 -0
  30. package/next/pages-route-handler.js.map +1 -0
  31. package/package.json +17 -7
  32. package/sveltekit/handler.d.ts +19 -0
  33. package/sveltekit/handler.js +100 -0
  34. package/sveltekit/handler.js.map +1 -0
  35. package/sveltekit/index.d.ts +2 -0
  36. package/sveltekit/index.js +24 -0
  37. package/sveltekit/index.js.map +1 -0
  38. package/types.d.ts +97 -0
  39. package/types.js +3 -0
  40. package/types.js.map +1 -0
  41. package/utils.d.ts +17 -0
  42. package/utils.js +93 -0
  43. package/utils.js.map +1 -0
  44. package/openapi/index.d.ts +0 -46
  45. package/openapi/index.js +0 -193
  46. package/openapi/index.js.map +0 -1
  47. package/openapi/utils.d.ts +0 -4
  48. package/openapi/utils.js +0 -25
  49. package/openapi/utils.js.map +0 -1
@@ -0,0 +1,1271 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ /* eslint-disable @typescript-eslint/no-explicit-any */
16
+ const runtime_1 = require("@zenstackhq/runtime");
17
+ const model_meta_1 = require("@zenstackhq/runtime/enhancements/model-meta");
18
+ const change_case_1 = require("change-case");
19
+ const lower_case_first_1 = require("lower-case-first");
20
+ const ts_japi_1 = require("ts-japi");
21
+ const url_pattern_1 = __importDefault(require("url-pattern"));
22
+ const zod_1 = __importDefault(require("zod"));
23
+ const zod_validation_error_1 = require("zod-validation-error");
24
+ const utils_1 = require("../utils");
25
+ const urlPatterns = {
26
+ // collection operations
27
+ collection: new url_pattern_1.default('/:type'),
28
+ // single resource operations
29
+ single: new url_pattern_1.default('/:type/:id'),
30
+ // related entity fetching
31
+ fetchRelationship: new url_pattern_1.default('/:type/:id/:relationship'),
32
+ // relationship operations
33
+ relationship: new url_pattern_1.default('/:type/:id/relationships/:relationship'),
34
+ };
35
+ class InvalidValueError extends Error {
36
+ constructor(message) {
37
+ super(message);
38
+ this.message = message;
39
+ }
40
+ }
41
+ const DEFAULT_PAGE_SIZE = 100;
42
+ const FilterOperations = [
43
+ 'lt',
44
+ 'lte',
45
+ 'gt',
46
+ 'gte',
47
+ 'contains',
48
+ 'icontains',
49
+ 'search',
50
+ 'startsWith',
51
+ 'endsWith',
52
+ 'has',
53
+ 'hasEvery',
54
+ 'hasSome',
55
+ 'isEmpty',
56
+ ];
57
+ /**
58
+ * RESTful-style API request handler (compliant with JSON:API)
59
+ */
60
+ class RequestHandler {
61
+ constructor(options) {
62
+ this.options = options;
63
+ // error responses
64
+ this.errors = {
65
+ unsupportedModel: {
66
+ status: 404,
67
+ title: 'Unsupported model type',
68
+ detail: 'The model type is not supported',
69
+ },
70
+ unsupportedRelationship: {
71
+ status: 400,
72
+ title: 'Unsupported relationship',
73
+ detail: 'The relationship is not supported',
74
+ },
75
+ invalidPath: {
76
+ status: 400,
77
+ title: 'The request path is invalid',
78
+ },
79
+ invalidVerb: {
80
+ status: 400,
81
+ title: 'The HTTP verb is not supported',
82
+ },
83
+ notFound: {
84
+ status: 404,
85
+ title: 'Resource not found',
86
+ },
87
+ noId: {
88
+ status: 400,
89
+ title: 'Model without an ID field is not supported',
90
+ },
91
+ multiId: {
92
+ status: 400,
93
+ title: 'Model with multiple ID fields is not supported',
94
+ },
95
+ invalidId: {
96
+ status: 400,
97
+ title: 'Resource ID is invalid',
98
+ },
99
+ invalidPayload: {
100
+ status: 400,
101
+ title: 'Invalid payload',
102
+ },
103
+ invalidRelationData: {
104
+ status: 400,
105
+ title: 'Invalid payload',
106
+ detail: 'Invalid relationship data',
107
+ },
108
+ invalidRelation: {
109
+ status: 400,
110
+ title: 'Invalid payload',
111
+ detail: 'Invalid relationship',
112
+ },
113
+ invalidFilter: {
114
+ status: 400,
115
+ title: 'Invalid filter',
116
+ },
117
+ invalidSort: {
118
+ status: 400,
119
+ title: 'Invalid sort',
120
+ },
121
+ invalidValue: {
122
+ status: 400,
123
+ title: 'Invalid value for type',
124
+ },
125
+ forbidden: {
126
+ status: 403,
127
+ title: 'Operation is forbidden',
128
+ },
129
+ unknownError: {
130
+ status: 400,
131
+ title: 'Unknown error',
132
+ },
133
+ };
134
+ this.filterParamPattern = new RegExp(/^filter(?<match>(\[[^[\]]+\])+)$/);
135
+ // zod schema for payload of creating and updating a resource
136
+ this.createUpdatePayloadSchema = zod_1.default
137
+ .object({
138
+ data: zod_1.default.object({
139
+ type: zod_1.default.string(),
140
+ attributes: zod_1.default.object({}).passthrough().optional(),
141
+ relationships: zod_1.default
142
+ .record(zod_1.default.object({
143
+ data: zod_1.default.union([
144
+ zod_1.default.object({ type: zod_1.default.string(), id: zod_1.default.union([zod_1.default.string(), zod_1.default.number()]) }),
145
+ zod_1.default.array(zod_1.default.object({ type: zod_1.default.string(), id: zod_1.default.union([zod_1.default.string(), zod_1.default.number()]) })),
146
+ ]),
147
+ }))
148
+ .optional(),
149
+ }),
150
+ })
151
+ .strict();
152
+ // zod schema for updating a single relationship
153
+ this.updateSingleRelationSchema = zod_1.default.object({
154
+ data: zod_1.default.object({ type: zod_1.default.string(), id: zod_1.default.union([zod_1.default.string(), zod_1.default.number()]) }).nullable(),
155
+ });
156
+ // zod schema for updating collection relationship
157
+ this.updateCollectionRelationSchema = zod_1.default.object({
158
+ data: zod_1.default.array(zod_1.default.object({ type: zod_1.default.string(), id: zod_1.default.union([zod_1.default.string(), zod_1.default.number()]) })),
159
+ });
160
+ try {
161
+ this.defaultModelMeta = (0, model_meta_1.getDefaultModelMeta)();
162
+ }
163
+ catch (_a) {
164
+ // noop
165
+ }
166
+ }
167
+ handleRequest({ prisma, method, path, query, requestBody, logger, modelMeta, }) {
168
+ return __awaiter(this, void 0, void 0, function* () {
169
+ if (!this.serializers) {
170
+ this.buildSerializers(modelMeta !== null && modelMeta !== void 0 ? modelMeta : this.defaultModelMeta);
171
+ }
172
+ if (!this.typeMap) {
173
+ this.buildTypeMap(logger, modelMeta !== null && modelMeta !== void 0 ? modelMeta : this.defaultModelMeta);
174
+ }
175
+ method = method.toUpperCase();
176
+ if (!path.startsWith('/')) {
177
+ path = '/' + path;
178
+ }
179
+ try {
180
+ switch (method) {
181
+ case 'GET': {
182
+ let match = urlPatterns.single.match(path);
183
+ if (match) {
184
+ // single resource read
185
+ return yield this.processSingleRead(prisma, match.type, match.id, query);
186
+ }
187
+ match = urlPatterns.fetchRelationship.match(path);
188
+ if (match) {
189
+ // fetch related resource(s)
190
+ return yield this.processFetchRelated(prisma, match.type, match.id, match.relationship, query);
191
+ }
192
+ match = urlPatterns.relationship.match(path);
193
+ if (match) {
194
+ // read relationship
195
+ return yield this.processReadRelationship(prisma, match.type, match.id, match.relationship, query, modelMeta !== null && modelMeta !== void 0 ? modelMeta : this.defaultModelMeta);
196
+ }
197
+ match = urlPatterns.collection.match(path);
198
+ if (match) {
199
+ // collection read
200
+ return yield this.processCollectionRead(prisma, match.type, query);
201
+ }
202
+ return this.makeError('invalidPath');
203
+ }
204
+ case 'POST': {
205
+ let match = urlPatterns.collection.match(path);
206
+ if (match) {
207
+ // resource creation
208
+ return yield this.processCreate(prisma, match.type, query, requestBody /*, zodSchemas */);
209
+ }
210
+ match = urlPatterns.relationship.match(path);
211
+ if (match) {
212
+ // relationship creation (collection relationship only)
213
+ return yield this.processRelationshipCRUD(prisma, 'create', match.type, match.id, match.relationship, query, requestBody);
214
+ }
215
+ return this.makeError('invalidPath');
216
+ }
217
+ // TODO: PUT for full update
218
+ case 'PUT':
219
+ case 'PATCH': {
220
+ let match = urlPatterns.single.match(path);
221
+ if (match) {
222
+ // resource update
223
+ return yield this.processUpdate(prisma, match.type, match.id, query, requestBody /*, zodSchemas */);
224
+ }
225
+ match = urlPatterns.relationship.match(path);
226
+ if (match) {
227
+ // relationship update
228
+ return yield this.processRelationshipCRUD(prisma, 'update', match.type, match.id, match.relationship, query, requestBody);
229
+ }
230
+ return this.makeError('invalidPath');
231
+ }
232
+ case 'DELETE': {
233
+ let match = urlPatterns.single.match(path);
234
+ if (match) {
235
+ // resource deletion
236
+ return yield this.processDelete(prisma, match.type, match.id);
237
+ }
238
+ match = urlPatterns.relationship.match(path);
239
+ if (match) {
240
+ // relationship deletion (collection relationship only)
241
+ return yield this.processRelationshipCRUD(prisma, 'delete', match.type, match.id, match.relationship, query, requestBody);
242
+ }
243
+ return this.makeError('invalidPath');
244
+ }
245
+ default:
246
+ return this.makeError('invalidPath');
247
+ }
248
+ }
249
+ catch (err) {
250
+ if (err instanceof InvalidValueError) {
251
+ return this.makeError('invalidValue', err.message);
252
+ }
253
+ else {
254
+ return this.handlePrismaError(err);
255
+ }
256
+ }
257
+ });
258
+ }
259
+ processSingleRead(prisma, type, resourceId, query) {
260
+ return __awaiter(this, void 0, void 0, function* () {
261
+ const typeInfo = this.typeMap[type];
262
+ if (!typeInfo) {
263
+ return this.makeUnsupportedModelError(type);
264
+ }
265
+ const args = { where: this.makeIdFilter(typeInfo.idField, typeInfo.idFieldType, resourceId) };
266
+ // include IDs of relation fields so that they can be serialized
267
+ this.includeRelationshipIds(type, args, 'include');
268
+ // handle "include" query parameter
269
+ let include;
270
+ if (query === null || query === void 0 ? void 0 : query.include) {
271
+ const { select, error, allIncludes } = this.buildRelationSelect(type, query.include);
272
+ if (error) {
273
+ return error;
274
+ }
275
+ if (select) {
276
+ args.include = Object.assign(Object.assign({}, args.include), select);
277
+ }
278
+ include = allIncludes;
279
+ }
280
+ const entity = yield prisma[type].findUnique(args);
281
+ if (entity) {
282
+ return {
283
+ status: 200,
284
+ body: yield this.serializeItems(type, entity, { include }),
285
+ };
286
+ }
287
+ else {
288
+ return this.makeError('notFound');
289
+ }
290
+ });
291
+ }
292
+ processFetchRelated(prisma, type, resourceId, relationship, query) {
293
+ var _a, _b;
294
+ return __awaiter(this, void 0, void 0, function* () {
295
+ const typeInfo = this.typeMap[type];
296
+ if (!typeInfo) {
297
+ return this.makeUnsupportedModelError(type);
298
+ }
299
+ const relationInfo = typeInfo.relationships[relationship];
300
+ if (!relationInfo) {
301
+ return this.makeUnsupportedRelationshipError(type, relationship, 404);
302
+ }
303
+ let select;
304
+ // handle "include" query parameter
305
+ let include;
306
+ if (query === null || query === void 0 ? void 0 : query.include) {
307
+ const { select: relationSelect, error, allIncludes } = this.buildRelationSelect(type, query.include);
308
+ if (error) {
309
+ return error;
310
+ }
311
+ // trim the leading `$relationship.` from the include paths
312
+ include = allIncludes
313
+ .filter((i) => i.startsWith(`${relationship}.`))
314
+ .map((i) => i.substring(`${relationship}.`.length));
315
+ select = relationSelect;
316
+ }
317
+ select = select !== null && select !== void 0 ? select : { [relationship]: true };
318
+ const args = {
319
+ where: this.makeIdFilter(typeInfo.idField, typeInfo.idFieldType, resourceId),
320
+ select,
321
+ };
322
+ if (relationInfo.isCollection) {
323
+ // if related data is a collection, it can be filtered, sorted, and paginated
324
+ const error = this.injectRelationQuery(relationInfo.type, select, relationship, query);
325
+ if (error) {
326
+ return error;
327
+ }
328
+ }
329
+ const entity = yield prisma[type].findUnique(args);
330
+ let paginator;
331
+ if (((_a = entity === null || entity === void 0 ? void 0 : entity._count) === null || _a === void 0 ? void 0 : _a[relationship]) !== undefined) {
332
+ // build up paginator
333
+ const total = (_b = entity === null || entity === void 0 ? void 0 : entity._count) === null || _b === void 0 ? void 0 : _b[relationship];
334
+ const url = this.makeNormalizedUrl(`/${type}/${resourceId}/${relationship}`, query);
335
+ const { offset, limit } = this.getPagination(query);
336
+ paginator = this.makePaginator(url, offset, limit, total);
337
+ }
338
+ if (entity === null || entity === void 0 ? void 0 : entity[relationship]) {
339
+ return {
340
+ status: 200,
341
+ body: yield this.serializeItems(relationInfo.type, entity[relationship], {
342
+ linkers: {
343
+ document: new ts_japi_1.Linker(() => this.makeLinkUrl(`/${type}/${resourceId}/${relationship}`)),
344
+ paginator,
345
+ },
346
+ include,
347
+ }),
348
+ };
349
+ }
350
+ else {
351
+ return this.makeError('notFound');
352
+ }
353
+ });
354
+ }
355
+ processReadRelationship(prisma, type, resourceId, relationship, query, modelMeta) {
356
+ var _a, _b;
357
+ return __awaiter(this, void 0, void 0, function* () {
358
+ const typeInfo = this.typeMap[type];
359
+ if (!typeInfo) {
360
+ return this.makeUnsupportedModelError(type);
361
+ }
362
+ const relationInfo = typeInfo.relationships[relationship];
363
+ if (!relationInfo) {
364
+ return this.makeUnsupportedRelationshipError(type, relationship, 404);
365
+ }
366
+ const args = {
367
+ where: this.makeIdFilter(typeInfo.idField, typeInfo.idFieldType, resourceId),
368
+ select: this.makeIdSelect(type, modelMeta),
369
+ };
370
+ // include IDs of relation fields so that they can be serialized
371
+ // this.includeRelationshipIds(type, args, 'select');
372
+ args.select = Object.assign(Object.assign({}, args.select), { [relationship]: { select: this.makeIdSelect(relationInfo.type, modelMeta) } });
373
+ let paginator;
374
+ if (relationInfo.isCollection) {
375
+ // if related data is a collection, it can be filtered, sorted, and paginated
376
+ const error = this.injectRelationQuery(relationInfo.type, args.select, relationship, query);
377
+ if (error) {
378
+ return error;
379
+ }
380
+ }
381
+ const entity = yield prisma[type].findUnique(args);
382
+ if (((_a = entity === null || entity === void 0 ? void 0 : entity._count) === null || _a === void 0 ? void 0 : _a[relationship]) !== undefined) {
383
+ // build up paginator
384
+ const total = (_b = entity === null || entity === void 0 ? void 0 : entity._count) === null || _b === void 0 ? void 0 : _b[relationship];
385
+ const url = this.makeNormalizedUrl(`/${type}/${resourceId}/relationships/${relationship}`, query);
386
+ const { offset, limit } = this.getPagination(query);
387
+ paginator = this.makePaginator(url, offset, limit, total);
388
+ }
389
+ if (entity === null || entity === void 0 ? void 0 : entity[relationship]) {
390
+ const serialized = yield this.serializeItems(relationInfo.type, entity[relationship], {
391
+ linkers: {
392
+ document: new ts_japi_1.Linker(() => this.makeLinkUrl(`/${type}/${resourceId}/relationships/${relationship}`)),
393
+ paginator,
394
+ },
395
+ onlyIdentifier: true,
396
+ });
397
+ return {
398
+ status: 200,
399
+ body: serialized,
400
+ };
401
+ }
402
+ else {
403
+ return this.makeError('notFound');
404
+ }
405
+ });
406
+ }
407
+ processCollectionRead(prisma, type, query) {
408
+ return __awaiter(this, void 0, void 0, function* () {
409
+ const typeInfo = this.typeMap[type];
410
+ if (!typeInfo) {
411
+ return this.makeUnsupportedModelError(type);
412
+ }
413
+ const args = {};
414
+ // add filter
415
+ const { filter, error: filterError } = this.buildFilter(type, query);
416
+ if (filterError) {
417
+ return filterError;
418
+ }
419
+ if (filter) {
420
+ args.where = filter;
421
+ }
422
+ const { sort, error: sortError } = this.buildSort(type, query);
423
+ if (sortError) {
424
+ return sortError;
425
+ }
426
+ if (sort) {
427
+ args.orderBy = sort;
428
+ }
429
+ // include IDs of relation fields so that they can be serialized
430
+ this.includeRelationshipIds(type, args, 'include');
431
+ // handle "include" query parameter
432
+ let include;
433
+ if (query === null || query === void 0 ? void 0 : query.include) {
434
+ const { select, error, allIncludes } = this.buildRelationSelect(type, query.include);
435
+ if (error) {
436
+ return error;
437
+ }
438
+ if (select) {
439
+ args.include = Object.assign(Object.assign({}, args.include), select);
440
+ }
441
+ include = allIncludes;
442
+ }
443
+ const { offset, limit } = this.getPagination(query);
444
+ if (offset > 0) {
445
+ args.skip = offset;
446
+ }
447
+ if (limit === Infinity) {
448
+ const entities = yield prisma[type].findMany(args);
449
+ return {
450
+ status: 200,
451
+ body: yield this.serializeItems(type, entities, { include }),
452
+ };
453
+ }
454
+ else {
455
+ args.take = limit;
456
+ const [entities, count] = yield Promise.all([
457
+ prisma[type].findMany(args),
458
+ prisma[type].count({ where: args.where }),
459
+ ]);
460
+ const total = count;
461
+ const url = this.makeNormalizedUrl(`/${type}`, query);
462
+ const options = {
463
+ include,
464
+ linkers: {
465
+ paginator: this.makePaginator(url, offset, limit, total),
466
+ },
467
+ };
468
+ return {
469
+ status: 200,
470
+ body: yield this.serializeItems(type, entities, options),
471
+ };
472
+ }
473
+ });
474
+ }
475
+ makePaginator(baseUrl, offset, limit, total) {
476
+ if (limit === Infinity) {
477
+ return undefined;
478
+ }
479
+ const totalPages = Math.ceil(total / limit);
480
+ return new ts_japi_1.Paginator(() => ({
481
+ first: this.replaceURLSearchParams(baseUrl, { 'page[limit]': limit }),
482
+ last: this.replaceURLSearchParams(baseUrl, {
483
+ 'page[offset]': (totalPages - 1) * limit,
484
+ }),
485
+ prev: offset - limit >= 0 && offset - limit <= total - 1
486
+ ? this.replaceURLSearchParams(baseUrl, {
487
+ 'page[offset]': offset - limit,
488
+ 'page[limit]': limit,
489
+ })
490
+ : null,
491
+ next: offset + limit <= total - 1
492
+ ? this.replaceURLSearchParams(baseUrl, {
493
+ 'page[offset]': offset + limit,
494
+ 'page[limit]': limit,
495
+ })
496
+ : null,
497
+ }));
498
+ }
499
+ processCreate(prisma, type, _query, requestBody
500
+ // zodSchemas?: ModelZodSchema
501
+ ) {
502
+ var _a, _b;
503
+ return __awaiter(this, void 0, void 0, function* () {
504
+ const typeInfo = this.typeMap[type];
505
+ if (!typeInfo) {
506
+ return this.makeUnsupportedModelError(type);
507
+ }
508
+ // zod-parse payload
509
+ const parsed = this.createUpdatePayloadSchema.safeParse(requestBody);
510
+ if (!parsed.success) {
511
+ return this.makeError('invalidPayload', (0, zod_validation_error_1.fromZodError)(parsed.error).message);
512
+ }
513
+ const parsedPayload = parsed.data;
514
+ const createPayload = { data: Object.assign({}, (_a = parsedPayload.data) === null || _a === void 0 ? void 0 : _a.attributes) };
515
+ // TODO: we need to somehow exclude relation fields from the zod schema because relations are
516
+ // not part of "data" in the payload
517
+ //
518
+ // // zod-parse attributes if a schema is provided
519
+ // const dataSchema = zodSchemas ? getZodSchema(zodSchemas, type, 'create') : undefined;
520
+ // if (dataSchema) {
521
+ // const dataParsed = dataSchema.safeParse(createPayload);
522
+ // if (!dataParsed.success) {
523
+ // return this.makeError('invalidPayload', fromZodError(dataParsed.error).message);
524
+ // }
525
+ // }
526
+ // turn relashionship payload into Prisma connect objects
527
+ const relationships = (_b = parsedPayload.data) === null || _b === void 0 ? void 0 : _b.relationships;
528
+ if (relationships) {
529
+ for (const [key, data] of Object.entries(relationships)) {
530
+ if (!(data === null || data === void 0 ? void 0 : data.data)) {
531
+ return this.makeError('invalidRelationData');
532
+ }
533
+ const relationInfo = typeInfo.relationships[key];
534
+ if (!relationInfo) {
535
+ return this.makeUnsupportedRelationshipError(type, key, 400);
536
+ }
537
+ if (relationInfo.isCollection) {
538
+ createPayload.data[key] = {
539
+ connect: (0, runtime_1.enumerate)(data.data).map((item) => ({
540
+ [relationInfo.idField]: this.coerce(relationInfo.idFieldType, item.id),
541
+ })),
542
+ };
543
+ }
544
+ else {
545
+ if (typeof data.data !== 'object') {
546
+ return this.makeError('invalidRelationData');
547
+ }
548
+ createPayload.data[key] = {
549
+ connect: { [relationInfo.idField]: this.coerce(relationInfo.idFieldType, data.data.id) },
550
+ };
551
+ }
552
+ // make sure ID fields are included for result serialization
553
+ createPayload.include = Object.assign(Object.assign({}, createPayload.include), { [key]: { select: { [relationInfo.idField]: true } } });
554
+ }
555
+ }
556
+ const entity = yield prisma[type].create(createPayload);
557
+ return {
558
+ status: 201,
559
+ body: yield this.serializeItems(type, entity),
560
+ };
561
+ });
562
+ }
563
+ processRelationshipCRUD(prisma, mode, type, resourceId, relationship, query, requestBody) {
564
+ return __awaiter(this, void 0, void 0, function* () {
565
+ const typeInfo = this.typeMap[type];
566
+ if (!typeInfo) {
567
+ return this.makeUnsupportedModelError(type);
568
+ }
569
+ const relationInfo = typeInfo.relationships[relationship];
570
+ if (!relationInfo) {
571
+ return this.makeUnsupportedRelationshipError(type, relationship, 404);
572
+ }
573
+ if (!relationInfo.isCollection && mode !== 'update') {
574
+ // to-one relation can only be updated
575
+ return this.makeError('invalidVerb');
576
+ }
577
+ const updateArgs = {
578
+ where: this.makeIdFilter(typeInfo.idField, typeInfo.idFieldType, resourceId),
579
+ select: { [typeInfo.idField]: true, [relationship]: { select: { [relationInfo.idField]: true } } },
580
+ };
581
+ if (!relationInfo.isCollection) {
582
+ // zod-parse payload
583
+ const parsed = this.updateSingleRelationSchema.safeParse(requestBody);
584
+ if (!parsed.success) {
585
+ return this.makeError('invalidPayload', (0, zod_validation_error_1.fromZodError)(parsed.error).message);
586
+ }
587
+ if (parsed.data.data === null) {
588
+ if (!relationInfo.isOptional) {
589
+ // cannot disconnect a required relation
590
+ return this.makeError('invalidPayload');
591
+ }
592
+ // set null -> disconnect
593
+ updateArgs.data = {
594
+ [relationship]: {
595
+ disconnect: true,
596
+ },
597
+ };
598
+ }
599
+ else {
600
+ updateArgs.data = {
601
+ [relationship]: {
602
+ connect: {
603
+ [relationInfo.idField]: this.coerce(relationInfo.idFieldType, parsed.data.data.id),
604
+ },
605
+ },
606
+ };
607
+ }
608
+ }
609
+ else {
610
+ // zod-parse payload
611
+ const parsed = this.updateCollectionRelationSchema.safeParse(requestBody);
612
+ if (!parsed.success) {
613
+ return this.makeError('invalidPayload', (0, zod_validation_error_1.fromZodError)(parsed.error).message);
614
+ }
615
+ // create -> connect, delete -> disconnect, update -> set
616
+ const relationVerb = mode === 'create' ? 'connect' : mode === 'delete' ? 'disconnect' : 'set';
617
+ updateArgs.data = {
618
+ [relationship]: {
619
+ [relationVerb]: (0, runtime_1.enumerate)(parsed.data.data).map((item) => ({
620
+ [relationInfo.idField]: this.coerce(relationInfo.idFieldType, item.id),
621
+ })),
622
+ },
623
+ };
624
+ }
625
+ const entity = yield prisma[type].update(updateArgs);
626
+ const serialized = yield this.serializeItems(relationInfo.type, entity[relationship], {
627
+ linkers: {
628
+ document: new ts_japi_1.Linker(() => this.makeLinkUrl(`/${type}/${resourceId}/relationships/${relationship}`)),
629
+ },
630
+ onlyIdentifier: true,
631
+ });
632
+ return {
633
+ status: 200,
634
+ body: serialized,
635
+ };
636
+ });
637
+ }
638
+ processUpdate(prisma, type, resourceId, query, requestBody
639
+ // zodSchemas?: ModelZodSchema
640
+ ) {
641
+ var _a, _b;
642
+ return __awaiter(this, void 0, void 0, function* () {
643
+ const typeInfo = this.typeMap[type];
644
+ if (!typeInfo) {
645
+ return this.makeUnsupportedModelError(type);
646
+ }
647
+ // zod-parse payload
648
+ const parsed = this.createUpdatePayloadSchema.safeParse(requestBody);
649
+ if (!parsed.success) {
650
+ return this.makeError('invalidPayload', (0, zod_validation_error_1.fromZodError)(parsed.error).message);
651
+ }
652
+ const parsedPayload = parsed.data;
653
+ const attributes = (_a = parsedPayload.data) === null || _a === void 0 ? void 0 : _a.attributes;
654
+ if (!attributes) {
655
+ return this.makeError('invalidPayload');
656
+ }
657
+ const updatePayload = {
658
+ where: this.makeIdFilter(typeInfo.idField, typeInfo.idFieldType, resourceId),
659
+ data: Object.assign({}, attributes),
660
+ };
661
+ // TODO: we need to somehow exclude relation fields from the zod schema because relations are
662
+ // not part of "data" in the payload
663
+ //
664
+ // // zod-parse attributes if a schema is provided
665
+ // const dataSchema = zodSchemas ? getZodSchema(zodSchemas, type, 'update') : undefined;
666
+ // if (dataSchema) {
667
+ // const dataParsed = dataSchema.safeParse(updatePayload);
668
+ // if (!dataParsed.success) {
669
+ // return this.makeError('invalidPayload', fromZodError(dataParsed.error).message);
670
+ // }
671
+ // }
672
+ // turn relationships into prisma payload
673
+ const relationships = (_b = parsedPayload.data) === null || _b === void 0 ? void 0 : _b.relationships;
674
+ if (relationships) {
675
+ for (const [key, data] of Object.entries(relationships)) {
676
+ if (!(data === null || data === void 0 ? void 0 : data.data)) {
677
+ return this.makeError('invalidRelationData');
678
+ }
679
+ const relationInfo = typeInfo.relationships[key];
680
+ if (!relationInfo) {
681
+ return this.makeUnsupportedRelationshipError(type, key, 400);
682
+ }
683
+ if (relationInfo.isCollection) {
684
+ updatePayload.data[key] = {
685
+ set: (0, runtime_1.enumerate)(data.data).map((item) => ({
686
+ [relationInfo.idField]: this.coerce(relationInfo.idFieldType, item.id),
687
+ })),
688
+ };
689
+ }
690
+ else {
691
+ if (typeof data.data !== 'object') {
692
+ return this.makeError('invalidRelationData');
693
+ }
694
+ updatePayload.data[key] = {
695
+ set: { [relationInfo.idField]: this.coerce(relationInfo.idFieldType, data.data.id) },
696
+ };
697
+ }
698
+ updatePayload.include = Object.assign(Object.assign({}, updatePayload.include), { [key]: { select: { [relationInfo.idField]: true } } });
699
+ }
700
+ }
701
+ const entity = yield prisma[type].update(updatePayload);
702
+ return {
703
+ status: 200,
704
+ body: yield this.serializeItems(type, entity),
705
+ };
706
+ });
707
+ }
708
+ processDelete(prisma, type, resourceId) {
709
+ return __awaiter(this, void 0, void 0, function* () {
710
+ const typeInfo = this.typeMap[type];
711
+ if (!typeInfo) {
712
+ return this.makeUnsupportedModelError(type);
713
+ }
714
+ yield prisma[type].delete({
715
+ where: this.makeIdFilter(typeInfo.idField, typeInfo.idFieldType, resourceId),
716
+ });
717
+ return {
718
+ status: 204,
719
+ body: undefined,
720
+ };
721
+ });
722
+ }
723
+ //#region utilities
724
+ buildTypeMap(logger, modelMeta) {
725
+ this.typeMap = {};
726
+ for (const [model, fields] of Object.entries(modelMeta.fields)) {
727
+ const idFields = (0, runtime_1.getIdFields)(modelMeta, model);
728
+ if (idFields.length === 0) {
729
+ (0, utils_1.logWarning)(logger, `Not including model ${model} in the API because it has no ID field`);
730
+ continue;
731
+ }
732
+ if (idFields.length > 1) {
733
+ (0, utils_1.logWarning)(logger, `Not including model ${model} in the API because it has multiple ID fields`);
734
+ continue;
735
+ }
736
+ this.typeMap[model] = {
737
+ idField: idFields[0].name,
738
+ idFieldType: idFields[0].type,
739
+ relationships: {},
740
+ fields,
741
+ };
742
+ for (const [field, fieldInfo] of Object.entries(fields)) {
743
+ if (!fieldInfo.isDataModel) {
744
+ continue;
745
+ }
746
+ const fieldTypeIdFields = (0, runtime_1.getIdFields)(modelMeta, fieldInfo.type);
747
+ if (fieldTypeIdFields.length === 0) {
748
+ (0, utils_1.logWarning)(logger, `Not including relation ${model}.${field} in the API because it has no ID field`);
749
+ continue;
750
+ }
751
+ if (fieldTypeIdFields.length > 1) {
752
+ (0, utils_1.logWarning)(logger, `Not including relation ${model}.${field} in the API because it has multiple ID fields`);
753
+ continue;
754
+ }
755
+ this.typeMap[model].relationships[field] = {
756
+ type: fieldInfo.type,
757
+ idField: fieldTypeIdFields[0].name,
758
+ idFieldType: fieldTypeIdFields[0].type,
759
+ isCollection: fieldInfo.isArray,
760
+ isOptional: fieldInfo.isOptional,
761
+ };
762
+ }
763
+ }
764
+ }
765
+ makeLinkUrl(path) {
766
+ return `${this.options.endpoint}${path}`;
767
+ }
768
+ buildSerializers(modelMeta) {
769
+ this.serializers = new Map();
770
+ const linkers = {};
771
+ for (const model of Object.keys(modelMeta.fields)) {
772
+ const ids = (0, runtime_1.getIdFields)(modelMeta, model);
773
+ if (ids.length !== 1) {
774
+ continue;
775
+ }
776
+ const linker = new ts_japi_1.Linker((items) => Array.isArray(items)
777
+ ? this.makeLinkUrl(`/${model}`)
778
+ : this.makeLinkUrl(`/${model}/${this.getId(model, items, modelMeta)}`));
779
+ linkers[model] = linker;
780
+ let projection = {};
781
+ for (const [field, fieldMeta] of Object.entries(modelMeta.fields[model])) {
782
+ if (fieldMeta.isDataModel) {
783
+ projection[field] = 0;
784
+ }
785
+ }
786
+ if (Object.keys(projection).length === 0) {
787
+ projection = null;
788
+ }
789
+ const serializer = new ts_japi_1.Serializer(model, {
790
+ version: '1.1',
791
+ idKey: ids[0].name,
792
+ linkers: {
793
+ resource: linker,
794
+ document: linker,
795
+ },
796
+ projection,
797
+ });
798
+ this.serializers.set(model, serializer);
799
+ }
800
+ // set relators
801
+ for (const model of Object.keys(modelMeta.fields)) {
802
+ const serializer = this.serializers.get(model);
803
+ if (!serializer) {
804
+ continue;
805
+ }
806
+ const relators = {};
807
+ for (const [field, fieldMeta] of Object.entries(modelMeta.fields[model])) {
808
+ if (!fieldMeta.isDataModel) {
809
+ continue;
810
+ }
811
+ const fieldSerializer = this.serializers.get((0, lower_case_first_1.lowerCaseFirst)(fieldMeta.type));
812
+ if (!fieldSerializer) {
813
+ continue;
814
+ }
815
+ const fieldIds = (0, runtime_1.getIdFields)(modelMeta, fieldMeta.type);
816
+ if (fieldIds.length === 1) {
817
+ const relator = new ts_japi_1.Relator((data) => __awaiter(this, void 0, void 0, function* () {
818
+ return data[field];
819
+ }), fieldSerializer, {
820
+ relatedName: field,
821
+ linkers: {
822
+ related: new ts_japi_1.Linker((primary) => this.makeLinkUrl(`/${(0, lower_case_first_1.lowerCaseFirst)(model)}/${this.getId(model, primary, modelMeta)}/${field}`)),
823
+ relationship: new ts_japi_1.Linker((primary) => this.makeLinkUrl(`/${(0, lower_case_first_1.lowerCaseFirst)(model)}/${this.getId(model, primary, modelMeta)}/relationships/${field}`)),
824
+ },
825
+ });
826
+ relators[field] = relator;
827
+ }
828
+ }
829
+ serializer.setRelators(relators);
830
+ }
831
+ }
832
+ getId(model, data, modelMeta) {
833
+ if (!data) {
834
+ return undefined;
835
+ }
836
+ const ids = (0, runtime_1.getIdFields)(modelMeta, model);
837
+ if (ids.length === 1) {
838
+ return data[ids[0].name];
839
+ }
840
+ else {
841
+ return undefined;
842
+ }
843
+ }
844
+ serializeItems(model, items, options) {
845
+ return __awaiter(this, void 0, void 0, function* () {
846
+ model = (0, lower_case_first_1.lowerCaseFirst)(model);
847
+ const serializer = this.serializers.get(model);
848
+ if (!serializer) {
849
+ throw new Error(`serializer not found for model ${model}`);
850
+ }
851
+ (0, utils_1.stripAuxFields)(items);
852
+ return JSON.parse(JSON.stringify(yield serializer.serialize(items, options)));
853
+ });
854
+ }
855
+ replaceURLSearchParams(url, params) {
856
+ const r = new URL(url);
857
+ for (const [key, value] of Object.entries(params)) {
858
+ r.searchParams.set(key, value.toString());
859
+ }
860
+ return r.toString();
861
+ }
862
+ makeIdFilter(idField, idFieldType, resourceId) {
863
+ return { [idField]: this.coerce(idFieldType, resourceId) };
864
+ }
865
+ makeIdSelect(model, modelMeta) {
866
+ const idFields = (0, runtime_1.getIdFields)(modelMeta, model);
867
+ if (idFields.length === 0) {
868
+ throw this.errors.noId;
869
+ }
870
+ else if (idFields.length > 1) {
871
+ throw this.errors.multiId;
872
+ }
873
+ return { [idFields[0].name]: true };
874
+ }
875
+ includeRelationshipIds(model, args, mode) {
876
+ const typeInfo = this.typeMap[model];
877
+ if (!typeInfo) {
878
+ return;
879
+ }
880
+ for (const [relation, relationInfo] of Object.entries(typeInfo.relationships)) {
881
+ args[mode] = Object.assign(Object.assign({}, args[mode]), { [relation]: { select: { [relationInfo.idField]: true } } });
882
+ }
883
+ }
884
+ coerce(type, value) {
885
+ if (typeof value === 'string') {
886
+ if (type === 'Int' || type === 'BigInt') {
887
+ const parsed = parseInt(value);
888
+ if (isNaN(parsed)) {
889
+ throw new InvalidValueError(`invalid ${type} value: ${value}`);
890
+ }
891
+ return parsed;
892
+ }
893
+ else if (type === 'Float' || type === 'Decimal') {
894
+ const parsed = parseFloat(value);
895
+ if (isNaN(parsed)) {
896
+ throw new InvalidValueError(`invalid ${type} value: ${value}`);
897
+ }
898
+ return parsed;
899
+ }
900
+ else if (type === 'Boolean') {
901
+ if (value === 'true') {
902
+ return true;
903
+ }
904
+ else if (value === 'false') {
905
+ return false;
906
+ }
907
+ else {
908
+ throw new InvalidValueError(`invalid ${type} value: ${value}`);
909
+ }
910
+ }
911
+ }
912
+ return value;
913
+ }
914
+ makeNormalizedUrl(path, query) {
915
+ const url = new URL(this.makeLinkUrl(path));
916
+ for (const [key, value] of Object.entries(query !== null && query !== void 0 ? query : {})) {
917
+ if (key.startsWith('filter[') ||
918
+ key.startsWith('sort[') ||
919
+ key.startsWith('include[') ||
920
+ key.startsWith('fields[')) {
921
+ for (const v of (0, runtime_1.enumerate)(value)) {
922
+ url.searchParams.append(key, v);
923
+ }
924
+ }
925
+ }
926
+ return url.toString();
927
+ }
928
+ getPagination(query) {
929
+ var _a, _b;
930
+ if (!query) {
931
+ return { offset: 0, limit: (_a = this.options.pageSize) !== null && _a !== void 0 ? _a : DEFAULT_PAGE_SIZE };
932
+ }
933
+ let offset = 0;
934
+ if (query['page[offset]']) {
935
+ const value = query['page[offset]'];
936
+ const offsetText = Array.isArray(value) ? value[value.length - 1] : value;
937
+ offset = parseInt(offsetText);
938
+ if (isNaN(offset) || offset < 0) {
939
+ offset = 0;
940
+ }
941
+ }
942
+ let pageSizeOption = (_b = this.options.pageSize) !== null && _b !== void 0 ? _b : DEFAULT_PAGE_SIZE;
943
+ if (pageSizeOption <= 0) {
944
+ pageSizeOption = DEFAULT_PAGE_SIZE;
945
+ }
946
+ let limit = pageSizeOption;
947
+ if (query['page[limit]']) {
948
+ const value = query['page[limit]'];
949
+ const limitText = Array.isArray(value) ? value[value.length - 1] : value;
950
+ limit = parseInt(limitText);
951
+ if (isNaN(limit) || limit <= 0) {
952
+ limit = pageSizeOption;
953
+ }
954
+ limit = Math.min(pageSizeOption, limit);
955
+ }
956
+ return { offset, limit };
957
+ }
958
+ buildFilter(type, query) {
959
+ if (!query) {
960
+ return { filter: undefined, error: undefined };
961
+ }
962
+ const typeInfo = this.typeMap[(0, lower_case_first_1.lowerCaseFirst)(type)];
963
+ if (!typeInfo) {
964
+ return { filter: undefined, error: this.makeUnsupportedModelError(type) };
965
+ }
966
+ const items = [];
967
+ let currType = typeInfo;
968
+ for (const [key, value] of Object.entries(query)) {
969
+ if (!value) {
970
+ continue;
971
+ }
972
+ // try matching query parameter key as "filter[x][y]..."
973
+ const match = key.match(this.filterParamPattern);
974
+ if (!match || !match.groups) {
975
+ continue;
976
+ }
977
+ const filterKeys = match.groups.match
978
+ .replaceAll(/[[\]]/g, ' ')
979
+ .split(' ')
980
+ .filter((i) => i);
981
+ if (!filterKeys.length) {
982
+ continue;
983
+ }
984
+ // turn filter into a nested Prisma query object
985
+ const item = {};
986
+ let curr = item;
987
+ for (const filterValue of (0, runtime_1.enumerate)(value)) {
988
+ for (let i = 0; i < filterKeys.length; i++) {
989
+ // extract filter operation from (optional) trailing $op
990
+ let filterKey = filterKeys[i];
991
+ let filterOp;
992
+ const pos = filterKey.indexOf('$');
993
+ if (pos > 0) {
994
+ filterOp = filterKey.substring(pos + 1);
995
+ filterKey = filterKey.substring(0, pos);
996
+ }
997
+ if (!!filterOp && !FilterOperations.includes(filterOp)) {
998
+ return {
999
+ filter: undefined,
1000
+ error: this.makeError('invalidFilter', `invalid filter operation: ${filterOp}`),
1001
+ };
1002
+ }
1003
+ const fieldInfo = filterKey === 'id'
1004
+ ? Object.values(currType.fields).find((f) => f.isId)
1005
+ : currType.fields[filterKey];
1006
+ if (!fieldInfo) {
1007
+ return { filter: undefined, error: this.makeError('invalidFilter') };
1008
+ }
1009
+ if (!fieldInfo.isDataModel) {
1010
+ // regular field
1011
+ if (i !== filterKeys.length - 1) {
1012
+ // must be the last segment of a filter
1013
+ return { filter: undefined, error: this.makeError('invalidFilter') };
1014
+ }
1015
+ curr[fieldInfo.name] = this.makeFilterValue(fieldInfo, filterValue, filterOp);
1016
+ }
1017
+ else {
1018
+ // relation field
1019
+ if (i === filterKeys.length - 1) {
1020
+ curr[fieldInfo.name] = this.makeFilterValue(fieldInfo, filterValue, filterOp);
1021
+ }
1022
+ else {
1023
+ // keep going
1024
+ if (fieldInfo.isArray) {
1025
+ // collection filtering implies "some" operation
1026
+ curr[fieldInfo.name] = { some: {} };
1027
+ curr = curr[fieldInfo.name].some;
1028
+ }
1029
+ else {
1030
+ curr = curr[fieldInfo.name] = {};
1031
+ }
1032
+ currType = this.typeMap[(0, lower_case_first_1.lowerCaseFirst)(fieldInfo.type)];
1033
+ }
1034
+ }
1035
+ }
1036
+ items.push(item);
1037
+ }
1038
+ }
1039
+ if (items.length === 0) {
1040
+ return { filter: undefined, error: undefined };
1041
+ }
1042
+ else {
1043
+ // combine filters with AND
1044
+ return { filter: items.length === 1 ? items[0] : { AND: items }, error: undefined };
1045
+ }
1046
+ }
1047
+ buildSort(type, query) {
1048
+ if (!(query === null || query === void 0 ? void 0 : query['sort'])) {
1049
+ return { sort: undefined, error: undefined };
1050
+ }
1051
+ const typeInfo = this.typeMap[(0, lower_case_first_1.lowerCaseFirst)(type)];
1052
+ if (!typeInfo) {
1053
+ return { sort: undefined, error: this.makeUnsupportedModelError(type) };
1054
+ }
1055
+ const result = [];
1056
+ for (const sortSpec of (0, runtime_1.enumerate)(query['sort'])) {
1057
+ const sortFields = sortSpec.split(',').filter((i) => i);
1058
+ for (const sortField of sortFields) {
1059
+ const dir = sortField.startsWith('-') ? 'desc' : 'asc';
1060
+ const cleanedSortField = sortField.startsWith('-') ? sortField.substring(1) : sortField;
1061
+ const parts = cleanedSortField.split('.').filter((i) => i);
1062
+ const sortItem = {};
1063
+ let curr = sortItem;
1064
+ let currType = typeInfo;
1065
+ for (let i = 0; i < parts.length; i++) {
1066
+ const part = parts[i];
1067
+ const fieldInfo = currType.fields[part];
1068
+ if (!fieldInfo || fieldInfo.isArray) {
1069
+ return {
1070
+ sort: undefined,
1071
+ error: this.makeError('invalidSort', 'sorting by array field is not supported'),
1072
+ };
1073
+ }
1074
+ if (i === parts.length - 1) {
1075
+ if (fieldInfo.isDataModel) {
1076
+ // relation field: sort by id
1077
+ const relationType = this.typeMap[(0, lower_case_first_1.lowerCaseFirst)(fieldInfo.type)];
1078
+ if (!relationType) {
1079
+ return { sort: undefined, error: this.makeUnsupportedModelError(fieldInfo.type) };
1080
+ }
1081
+ curr[fieldInfo.name] = { [relationType.idField]: dir };
1082
+ }
1083
+ else {
1084
+ // regular field
1085
+ curr[fieldInfo.name] = dir;
1086
+ }
1087
+ }
1088
+ else {
1089
+ if (!fieldInfo.isDataModel) {
1090
+ // must be a relation field
1091
+ return {
1092
+ sort: undefined,
1093
+ error: this.makeError('invalidSort', 'intermediate sort segments must be relationships'),
1094
+ };
1095
+ }
1096
+ // keep going
1097
+ curr = curr[fieldInfo.name] = {};
1098
+ currType = this.typeMap[(0, lower_case_first_1.lowerCaseFirst)(fieldInfo.type)];
1099
+ if (!currType) {
1100
+ return { sort: undefined, error: this.makeUnsupportedModelError(fieldInfo.type) };
1101
+ }
1102
+ }
1103
+ result.push(sortItem);
1104
+ }
1105
+ }
1106
+ }
1107
+ return { sort: result, error: undefined };
1108
+ }
1109
+ buildRelationSelect(type, include) {
1110
+ var _a;
1111
+ const typeInfo = this.typeMap[(0, lower_case_first_1.lowerCaseFirst)(type)];
1112
+ if (!typeInfo) {
1113
+ return { select: undefined, error: this.makeUnsupportedModelError(type) };
1114
+ }
1115
+ const result = {};
1116
+ const allIncludes = [];
1117
+ for (const includeItem of (0, runtime_1.enumerate)(include)) {
1118
+ const inclusions = includeItem.split(',').filter((i) => i);
1119
+ for (const inclusion of inclusions) {
1120
+ allIncludes.push(inclusion);
1121
+ const parts = inclusion.split('.');
1122
+ let currPayload = result;
1123
+ let currType = typeInfo;
1124
+ for (let i = 0; i < parts.length; i++) {
1125
+ const relation = parts[i];
1126
+ const relationInfo = currType.relationships[relation];
1127
+ if (!relationInfo) {
1128
+ return { select: undefined, error: this.makeUnsupportedRelationshipError(type, relation, 400) };
1129
+ }
1130
+ currType = this.typeMap[(0, lower_case_first_1.lowerCaseFirst)(relationInfo.type)];
1131
+ if (!currType) {
1132
+ return { select: undefined, error: this.makeUnsupportedModelError(relationInfo.type) };
1133
+ }
1134
+ if (i !== parts.length - 1) {
1135
+ currPayload[relation] = { include: Object.assign({}, (_a = currPayload[relation]) === null || _a === void 0 ? void 0 : _a.include) };
1136
+ currPayload = currPayload[relation].include;
1137
+ }
1138
+ else {
1139
+ currPayload[relation] = true;
1140
+ }
1141
+ }
1142
+ }
1143
+ }
1144
+ return { select: result, error: undefined, allIncludes };
1145
+ }
1146
+ makeFilterValue(fieldInfo, value, op) {
1147
+ if (fieldInfo.isDataModel) {
1148
+ // relation filter is converted to an ID filter
1149
+ const info = this.typeMap[(0, lower_case_first_1.lowerCaseFirst)(fieldInfo.type)];
1150
+ if (fieldInfo.isArray) {
1151
+ // filtering a to-many relation, imply 'some' operator
1152
+ const values = value.split(',').filter((i) => i);
1153
+ const filterValue = values.length > 1
1154
+ ? { OR: values.map((v) => this.makeIdFilter(info.idField, info.idFieldType, v)) }
1155
+ : this.makeIdFilter(info.idField, info.idFieldType, value);
1156
+ return { some: filterValue };
1157
+ }
1158
+ else {
1159
+ return { is: this.makeIdFilter(info.idField, info.idFieldType, value) };
1160
+ }
1161
+ }
1162
+ else {
1163
+ const coerced = this.coerce(fieldInfo.type, value);
1164
+ switch (op) {
1165
+ case 'icontains':
1166
+ return { contains: coerced, mode: 'insensitive' };
1167
+ case 'hasSome':
1168
+ case 'hasEvery': {
1169
+ const values = value
1170
+ .split(',')
1171
+ .filter((i) => i)
1172
+ .map((v) => this.coerce(fieldInfo.type, v));
1173
+ return { [op]: values };
1174
+ }
1175
+ case 'isEmpty':
1176
+ if (value !== 'true' && value !== 'false') {
1177
+ throw new InvalidValueError(`Not a boolean: ${value}`);
1178
+ }
1179
+ return { isEmpty: value === 'true' ? true : false };
1180
+ default:
1181
+ return op ? { [op]: coerced } : { equals: coerced };
1182
+ }
1183
+ }
1184
+ }
1185
+ injectRelationQuery(type, injectTarget, injectKey, query) {
1186
+ const { filter, error: filterError } = this.buildFilter(type, query);
1187
+ if (filterError) {
1188
+ return filterError;
1189
+ }
1190
+ if (filter) {
1191
+ injectTarget[injectKey] = Object.assign(Object.assign({}, injectTarget[injectKey]), { where: filter });
1192
+ }
1193
+ const { sort, error: sortError } = this.buildSort(type, query);
1194
+ if (sortError) {
1195
+ return sortError;
1196
+ }
1197
+ if (sort) {
1198
+ injectTarget[injectKey] = Object.assign(Object.assign({}, injectTarget[injectKey]), { orderBy: sort });
1199
+ }
1200
+ const pagination = this.getPagination(query);
1201
+ const offset = pagination.offset;
1202
+ if (offset > 0) {
1203
+ // inject skip
1204
+ injectTarget[injectKey] = Object.assign(Object.assign({}, injectTarget[injectKey]), { skip: offset });
1205
+ }
1206
+ const limit = pagination.limit;
1207
+ if (limit !== Infinity) {
1208
+ // inject take
1209
+ injectTarget[injectKey] = Object.assign(Object.assign({}, injectTarget[injectKey]), { take: limit });
1210
+ // include a count query for the relationship
1211
+ injectTarget._count = { select: { [injectKey]: true } };
1212
+ }
1213
+ }
1214
+ handlePrismaError(err) {
1215
+ if ((0, runtime_1.isPrismaClientKnownRequestError)(err)) {
1216
+ if (err.code === 'P2004') {
1217
+ return this.makeError('forbidden');
1218
+ }
1219
+ else if (err.code === 'P2025' || err.code === 'P2018') {
1220
+ return this.makeError('notFound');
1221
+ }
1222
+ else {
1223
+ return {
1224
+ status: 400,
1225
+ body: {
1226
+ errors: [
1227
+ {
1228
+ status: 400,
1229
+ code: 'prisma-error',
1230
+ prismaCode: err.code,
1231
+ title: 'Prisma error',
1232
+ detail: err.message,
1233
+ },
1234
+ ],
1235
+ },
1236
+ };
1237
+ }
1238
+ }
1239
+ else {
1240
+ const _err = err;
1241
+ return this.makeError('unknownError', `${_err.message}\n${_err.stack}`);
1242
+ }
1243
+ }
1244
+ makeError(code, detail, status) {
1245
+ return {
1246
+ status: status !== null && status !== void 0 ? status : this.errors[code].status,
1247
+ body: {
1248
+ errors: [
1249
+ {
1250
+ status: status !== null && status !== void 0 ? status : this.errors[code].status,
1251
+ code: (0, change_case_1.paramCase)(code),
1252
+ title: this.errors[code].title,
1253
+ detail: detail || this.errors[code].detail,
1254
+ },
1255
+ ],
1256
+ },
1257
+ };
1258
+ }
1259
+ makeUnsupportedModelError(model) {
1260
+ return this.makeError('unsupportedModel', `Model ${model} doesn't exist`);
1261
+ }
1262
+ makeUnsupportedRelationshipError(model, relationship, status) {
1263
+ return this.makeError('unsupportedRelationship', `Relationship ${model}.${relationship} doesn't exist`, status);
1264
+ }
1265
+ }
1266
+ function makeHandler(options) {
1267
+ const handler = new RequestHandler(options);
1268
+ return handler.handleRequest.bind(handler);
1269
+ }
1270
+ exports.default = makeHandler;
1271
+ //# sourceMappingURL=index.js.map