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

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