@zenstackhq/server 2.20.1 → 3.0.0-beta.12

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 (110) hide show
  1. package/LICENSE +1 -1
  2. package/dist/api.cjs +299 -0
  3. package/dist/api.cjs.map +1 -0
  4. package/dist/api.d.cts +28 -0
  5. package/dist/api.d.ts +28 -0
  6. package/dist/api.js +264 -0
  7. package/dist/api.js.map +1 -0
  8. package/dist/express.cjs +75 -0
  9. package/dist/express.cjs.map +1 -0
  10. package/dist/express.d.cts +31 -0
  11. package/dist/express.d.ts +31 -0
  12. package/dist/express.js +50 -0
  13. package/dist/express.js.map +1 -0
  14. package/dist/types-BH-88xJo.d.cts +68 -0
  15. package/dist/types-BH-88xJo.d.ts +68 -0
  16. package/package.json +57 -58
  17. package/README.md +0 -5
  18. package/api/base.d.ts +0 -49
  19. package/api/base.js +0 -19
  20. package/api/base.js.map +0 -1
  21. package/api/index.d.ts +0 -2
  22. package/api/index.js +0 -8
  23. package/api/index.js.map +0 -1
  24. package/api/rest/index.d.ts +0 -34
  25. package/api/rest/index.js +0 -1598
  26. package/api/rest/index.js.map +0 -1
  27. package/api/rpc/index.d.ts +0 -4
  28. package/api/rpc/index.js +0 -248
  29. package/api/rpc/index.js.map +0 -1
  30. package/api/utils.d.ts +0 -8
  31. package/api/utils.js +0 -54
  32. package/api/utils.js.map +0 -1
  33. package/elysia/handler.d.ts +0 -44
  34. package/elysia/handler.js +0 -62
  35. package/elysia/handler.js.map +0 -1
  36. package/elysia/index.d.ts +0 -1
  37. package/elysia/index.js +0 -18
  38. package/elysia/index.js.map +0 -1
  39. package/express/index.d.ts +0 -2
  40. package/express/index.js +0 -21
  41. package/express/index.js.map +0 -1
  42. package/express/middleware.d.ts +0 -27
  43. package/express/middleware.js +0 -58
  44. package/express/middleware.js.map +0 -1
  45. package/fastify/index.d.ts +0 -2
  46. package/fastify/index.js +0 -21
  47. package/fastify/index.js.map +0 -1
  48. package/fastify/plugin.d.ts +0 -18
  49. package/fastify/plugin.js +0 -48
  50. package/fastify/plugin.js.map +0 -1
  51. package/hono/handler.d.ts +0 -12
  52. package/hono/handler.js +0 -47
  53. package/hono/handler.js.map +0 -1
  54. package/hono/index.d.ts +0 -1
  55. package/hono/index.js +0 -18
  56. package/hono/index.js.map +0 -1
  57. package/nestjs/api-handler.service.d.ts +0 -15
  58. package/nestjs/api-handler.service.js +0 -72
  59. package/nestjs/api-handler.service.js.map +0 -1
  60. package/nestjs/index.d.ts +0 -3
  61. package/nestjs/index.js +0 -20
  62. package/nestjs/index.js.map +0 -1
  63. package/nestjs/interfaces/api-handler-options.interface.d.ts +0 -17
  64. package/nestjs/interfaces/api-handler-options.interface.js +0 -3
  65. package/nestjs/interfaces/api-handler-options.interface.js.map +0 -1
  66. package/nestjs/interfaces/index.d.ts +0 -2
  67. package/nestjs/interfaces/index.js +0 -19
  68. package/nestjs/interfaces/index.js.map +0 -1
  69. package/nestjs/interfaces/zenstack-module-options.interface.d.ts +0 -35
  70. package/nestjs/interfaces/zenstack-module-options.interface.js +0 -3
  71. package/nestjs/interfaces/zenstack-module-options.interface.js.map +0 -1
  72. package/nestjs/zenstack.constants.d.ts +0 -4
  73. package/nestjs/zenstack.constants.js +0 -8
  74. package/nestjs/zenstack.constants.js.map +0 -1
  75. package/nestjs/zenstack.module.d.ts +0 -12
  76. package/nestjs/zenstack.module.js +0 -61
  77. package/nestjs/zenstack.module.js.map +0 -1
  78. package/next/app-route-handler.d.ts +0 -16
  79. package/next/app-route-handler.js +0 -65
  80. package/next/app-route-handler.js.map +0 -1
  81. package/next/index.d.ts +0 -38
  82. package/next/index.js +0 -17
  83. package/next/index.js.map +0 -1
  84. package/next/pages-route-handler.d.ts +0 -9
  85. package/next/pages-route-handler.js +0 -45
  86. package/next/pages-route-handler.js.map +0 -1
  87. package/nuxt/handler.d.ts +0 -12
  88. package/nuxt/handler.js +0 -45
  89. package/nuxt/handler.js.map +0 -1
  90. package/nuxt/index.d.ts +0 -1
  91. package/nuxt/index.js +0 -18
  92. package/nuxt/index.js.map +0 -1
  93. package/shared.d.ts +0 -20
  94. package/shared.js +0 -50
  95. package/shared.js.map +0 -1
  96. package/sveltekit/handler.d.ts +0 -20
  97. package/sveltekit/handler.js +0 -68
  98. package/sveltekit/handler.js.map +0 -1
  99. package/sveltekit/index.d.ts +0 -2
  100. package/sveltekit/index.js +0 -21
  101. package/sveltekit/index.js.map +0 -1
  102. package/tanstack-start/handler.d.ts +0 -11
  103. package/tanstack-start/handler.js +0 -75
  104. package/tanstack-start/handler.js.map +0 -1
  105. package/tanstack-start/index.d.ts +0 -17
  106. package/tanstack-start/index.js +0 -16
  107. package/tanstack-start/index.js.map +0 -1
  108. package/types.d.ts +0 -49
  109. package/types.js +0 -3
  110. package/types.js.map +0 -1
package/api/rest/index.js DELETED
@@ -1,1598 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.default = makeHandler;
7
- exports.RestApiHandler = makeHandler;
8
- const runtime_1 = require("@zenstackhq/runtime");
9
- const local_helpers_1 = require("@zenstackhq/runtime/local-helpers");
10
- const superjson_1 = __importDefault(require("superjson"));
11
- const ts_japi_1 = require("ts-japi");
12
- const url_pattern_1 = __importDefault(require("url-pattern"));
13
- const zod_1 = __importDefault(require("zod"));
14
- const base_1 = require("../base");
15
- const utils_1 = require("../utils");
16
- var UrlPatterns;
17
- (function (UrlPatterns) {
18
- UrlPatterns["SINGLE"] = "single";
19
- UrlPatterns["FETCH_RELATIONSHIP"] = "fetchRelationship";
20
- UrlPatterns["RELATIONSHIP"] = "relationship";
21
- UrlPatterns["COLLECTION"] = "collection";
22
- })(UrlPatterns || (UrlPatterns = {}));
23
- class InvalidValueError extends Error {
24
- constructor(message) {
25
- super(message);
26
- this.message = message;
27
- }
28
- }
29
- const DEFAULT_PAGE_SIZE = 100;
30
- const FilterOperations = [
31
- 'lt',
32
- 'lte',
33
- 'gt',
34
- 'gte',
35
- 'contains',
36
- 'icontains',
37
- 'search',
38
- 'startsWith',
39
- 'endsWith',
40
- 'has',
41
- 'hasEvery',
42
- 'hasSome',
43
- 'isEmpty',
44
- ];
45
- const prismaIdDivider = '_';
46
- (0, utils_1.registerCustomSerializers)();
47
- /**
48
- * RESTful-style API request handler (compliant with JSON:API)
49
- */
50
- class RequestHandler extends base_1.APIHandlerBase {
51
- constructor(options) {
52
- super();
53
- this.options = options;
54
- // error responses
55
- this.errors = {
56
- unsupportedModel: {
57
- status: 404,
58
- title: 'Unsupported model type',
59
- detail: 'The model type is not supported',
60
- },
61
- unsupportedRelationship: {
62
- status: 400,
63
- title: 'Unsupported relationship',
64
- detail: 'The relationship is not supported',
65
- },
66
- invalidPath: {
67
- status: 400,
68
- title: 'The request path is invalid',
69
- },
70
- invalidVerb: {
71
- status: 400,
72
- title: 'The HTTP verb is not supported',
73
- },
74
- notFound: {
75
- status: 404,
76
- title: 'Resource not found',
77
- },
78
- noId: {
79
- status: 400,
80
- title: 'Model without an ID field is not supported',
81
- },
82
- invalidId: {
83
- status: 400,
84
- title: 'Resource ID is invalid',
85
- },
86
- invalidPayload: {
87
- status: 400,
88
- title: 'Invalid payload',
89
- },
90
- invalidRelationData: {
91
- status: 400,
92
- title: 'Invalid payload',
93
- detail: 'Invalid relationship data',
94
- },
95
- invalidRelation: {
96
- status: 400,
97
- title: 'Invalid payload',
98
- detail: 'Invalid relationship',
99
- },
100
- invalidFilter: {
101
- status: 400,
102
- title: 'Invalid filter',
103
- },
104
- invalidSort: {
105
- status: 400,
106
- title: 'Invalid sort',
107
- },
108
- invalidValue: {
109
- status: 400,
110
- title: 'Invalid value for type',
111
- },
112
- duplicatedFieldsParameter: {
113
- status: 400,
114
- title: 'Fields Parameter Duplicated',
115
- },
116
- forbidden: {
117
- status: 403,
118
- title: 'Operation is forbidden',
119
- },
120
- validationError: {
121
- status: 422,
122
- title: 'Operation is unprocessable due to validation errors',
123
- },
124
- unknownError: {
125
- status: 400,
126
- title: 'Unknown error',
127
- },
128
- };
129
- this.filterParamPattern = new RegExp(/^filter(?<match>(\[[^[\]]+\])+)$/);
130
- // zod schema for payload of creating and updating a resource
131
- this.createUpdatePayloadSchema = zod_1.default
132
- .object({
133
- data: zod_1.default.object({
134
- type: zod_1.default.string(),
135
- attributes: zod_1.default.object({}).passthrough().optional(),
136
- relationships: zod_1.default
137
- .record(zod_1.default.string(), zod_1.default.object({
138
- data: zod_1.default.union([
139
- zod_1.default.object({ type: zod_1.default.string(), id: zod_1.default.union([zod_1.default.string(), zod_1.default.number()]) }),
140
- 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()]) })),
141
- ]),
142
- }))
143
- .optional(),
144
- }),
145
- meta: zod_1.default.object({}).passthrough().optional(),
146
- })
147
- .strict();
148
- // zod schema for updating a single relationship
149
- this.updateSingleRelationSchema = zod_1.default.object({
150
- data: zod_1.default.object({ type: zod_1.default.string(), id: zod_1.default.union([zod_1.default.string(), zod_1.default.number()]) }).nullable(),
151
- });
152
- // zod schema for updating collection relationship
153
- this.updateCollectionRelationSchema = zod_1.default.object({
154
- 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()]) })),
155
- });
156
- this.upsertMetaSchema = zod_1.default.object({
157
- meta: zod_1.default.object({
158
- operation: zod_1.default.literal('upsert'),
159
- matchFields: zod_1.default.array(zod_1.default.string()).min(1),
160
- }),
161
- });
162
- this.idDivider = options.idDivider ?? prismaIdDivider;
163
- const segmentCharset = options.urlSegmentCharset ?? 'a-zA-Z0-9-_~ %';
164
- this.modelNameMapping = options.modelNameMapping ?? {};
165
- this.modelNameMapping = Object.fromEntries(Object.entries(this.modelNameMapping).map(([k, v]) => [(0, local_helpers_1.lowerCaseFirst)(k), v]));
166
- this.reverseModelNameMapping = Object.fromEntries(Object.entries(this.modelNameMapping).map(([k, v]) => [v, k]));
167
- this.externalIdMapping = options.externalIdMapping ?? {};
168
- this.externalIdMapping = Object.fromEntries(Object.entries(this.externalIdMapping).map(([k, v]) => [(0, local_helpers_1.lowerCaseFirst)(k), v]));
169
- this.urlPatternMap = this.buildUrlPatternMap(segmentCharset);
170
- }
171
- buildUrlPatternMap(urlSegmentNameCharset) {
172
- const options = { segmentValueCharset: urlSegmentNameCharset };
173
- const buildPath = (segments) => {
174
- return '/' + segments.join('/');
175
- };
176
- return {
177
- [UrlPatterns.SINGLE]: new url_pattern_1.default(buildPath([':type', ':id']), options),
178
- [UrlPatterns.FETCH_RELATIONSHIP]: new url_pattern_1.default(buildPath([':type', ':id', ':relationship']), options),
179
- [UrlPatterns.RELATIONSHIP]: new url_pattern_1.default(buildPath([':type', ':id', 'relationships', ':relationship']), options),
180
- [UrlPatterns.COLLECTION]: new url_pattern_1.default(buildPath([':type']), options),
181
- };
182
- }
183
- mapModelName(modelName) {
184
- return this.modelNameMapping[modelName] ?? modelName;
185
- }
186
- matchUrlPattern(path, routeType) {
187
- const pattern = this.urlPatternMap[routeType];
188
- if (!pattern) {
189
- throw new InvalidValueError(`Unknown route type: ${routeType}`);
190
- }
191
- const match = pattern.match(path);
192
- if (!match) {
193
- return;
194
- }
195
- if (match.type in this.modelNameMapping) {
196
- throw new InvalidValueError(`use the mapped model name: ${this.modelNameMapping[match.type]} and not ${match.type}`);
197
- }
198
- if (match.type in this.reverseModelNameMapping) {
199
- match.type = this.reverseModelNameMapping[match.type];
200
- }
201
- return match;
202
- }
203
- async handleRequest({ prisma, method, path, query, requestBody, logger, modelMeta, zodSchemas, }) {
204
- modelMeta = modelMeta ?? this.defaultModelMeta;
205
- if (!modelMeta) {
206
- throw new Error('Model metadata is not provided or loaded from default location');
207
- }
208
- if (!this.serializers) {
209
- this.buildSerializers(modelMeta);
210
- }
211
- if (!this.typeMap) {
212
- this.buildTypeMap(logger, modelMeta);
213
- }
214
- method = method.toUpperCase();
215
- if (!path.startsWith('/')) {
216
- path = '/' + path;
217
- }
218
- try {
219
- switch (method) {
220
- case 'GET': {
221
- let match = this.matchUrlPattern(path, UrlPatterns.SINGLE);
222
- if (match) {
223
- // single resource read
224
- return await this.processSingleRead(prisma, match.type, match.id, query);
225
- }
226
- match = this.matchUrlPattern(path, UrlPatterns.FETCH_RELATIONSHIP);
227
- if (match) {
228
- // fetch related resource(s)
229
- return await this.processFetchRelated(prisma, match.type, match.id, match.relationship, query);
230
- }
231
- match = this.matchUrlPattern(path, UrlPatterns.RELATIONSHIP);
232
- if (match) {
233
- // read relationship
234
- return await this.processReadRelationship(prisma, match.type, match.id, match.relationship, query);
235
- }
236
- match = this.matchUrlPattern(path, UrlPatterns.COLLECTION);
237
- if (match) {
238
- // collection read
239
- return await this.processCollectionRead(prisma, match.type, query);
240
- }
241
- return this.makeError('invalidPath');
242
- }
243
- case 'POST': {
244
- if (!requestBody) {
245
- return this.makeError('invalidPayload');
246
- }
247
- let match = this.matchUrlPattern(path, UrlPatterns.COLLECTION);
248
- if (match) {
249
- const body = requestBody;
250
- const upsertMeta = this.upsertMetaSchema.safeParse(body);
251
- if (upsertMeta.success) {
252
- // resource upsert
253
- return await this.processUpsert(prisma, match.type, query, requestBody, modelMeta, zodSchemas);
254
- }
255
- else {
256
- // resource creation
257
- return await this.processCreate(prisma, match.type, query, requestBody, modelMeta, zodSchemas);
258
- }
259
- }
260
- match = this.matchUrlPattern(path, UrlPatterns.RELATIONSHIP);
261
- if (match) {
262
- // relationship creation (collection relationship only)
263
- return await this.processRelationshipCRUD(prisma, 'create', match.type, match.id, match.relationship, query, requestBody);
264
- }
265
- return this.makeError('invalidPath');
266
- }
267
- // TODO: PUT for full update
268
- case 'PUT':
269
- case 'PATCH': {
270
- if (!requestBody) {
271
- return this.makeError('invalidPayload');
272
- }
273
- let match = this.matchUrlPattern(path, UrlPatterns.SINGLE);
274
- if (match) {
275
- // resource update
276
- return await this.processUpdate(prisma, match.type, match.id, query, requestBody, modelMeta, zodSchemas);
277
- }
278
- match = this.matchUrlPattern(path, UrlPatterns.RELATIONSHIP);
279
- if (match) {
280
- // relationship update
281
- return await this.processRelationshipCRUD(prisma, 'update', match.type, match.id, match.relationship, query, requestBody);
282
- }
283
- return this.makeError('invalidPath');
284
- }
285
- case 'DELETE': {
286
- let match = this.matchUrlPattern(path, UrlPatterns.SINGLE);
287
- if (match) {
288
- // resource deletion
289
- return await this.processDelete(prisma, match.type, match.id);
290
- }
291
- match = this.matchUrlPattern(path, UrlPatterns.RELATIONSHIP);
292
- if (match) {
293
- // relationship deletion (collection relationship only)
294
- return await this.processRelationshipCRUD(prisma, 'delete', match.type, match.id, match.relationship, query, requestBody);
295
- }
296
- return this.makeError('invalidPath');
297
- }
298
- default:
299
- return this.makeError('invalidPath');
300
- }
301
- }
302
- catch (err) {
303
- if (err instanceof InvalidValueError) {
304
- return this.makeError('invalidValue', err.message);
305
- }
306
- else {
307
- return this.handlePrismaError(err);
308
- }
309
- }
310
- }
311
- async processSingleRead(prisma, type, resourceId, query) {
312
- const typeInfo = this.typeMap[type];
313
- if (!typeInfo) {
314
- return this.makeUnsupportedModelError(type);
315
- }
316
- const args = { where: this.makePrismaIdFilter(typeInfo.idFields, resourceId) };
317
- // include IDs of relation fields so that they can be serialized
318
- this.includeRelationshipIds(type, args, 'include');
319
- // handle "include" query parameter
320
- let include;
321
- if (query?.include) {
322
- const { select, error, allIncludes } = this.buildRelationSelect(type, query.include, query);
323
- if (error) {
324
- return error;
325
- }
326
- if (select) {
327
- args.include = { ...args.include, ...select };
328
- }
329
- include = allIncludes;
330
- }
331
- // handle partial results for requested type
332
- const { select, error } = this.buildPartialSelect(type, query);
333
- if (error)
334
- return error;
335
- if (select) {
336
- args.select = { ...select, ...args.select };
337
- if (args.include) {
338
- args.select = {
339
- ...args.select,
340
- ...args.include,
341
- };
342
- args.include = undefined;
343
- }
344
- }
345
- const entity = await prisma[type].findUnique(args);
346
- if (entity) {
347
- return {
348
- status: 200,
349
- body: await this.serializeItems(type, entity, { include }),
350
- };
351
- }
352
- else {
353
- return this.makeError('notFound');
354
- }
355
- }
356
- async processFetchRelated(prisma, type, resourceId, relationship, query) {
357
- const typeInfo = this.typeMap[type];
358
- if (!typeInfo) {
359
- return this.makeUnsupportedModelError(type);
360
- }
361
- const relationInfo = typeInfo.relationships[relationship];
362
- if (!relationInfo) {
363
- return this.makeUnsupportedRelationshipError(type, relationship, 404);
364
- }
365
- let select;
366
- // handle "include" query parameter
367
- let include;
368
- if (query?.include) {
369
- const { select: relationSelect, error, allIncludes } = this.buildRelationSelect(type, query.include, query);
370
- if (error) {
371
- return error;
372
- }
373
- // trim the leading `$relationship.` from the include paths
374
- include = allIncludes
375
- .filter((i) => i.startsWith(`${relationship}.`))
376
- .map((i) => i.substring(`${relationship}.`.length));
377
- select = relationSelect;
378
- }
379
- // handle partial results for requested type
380
- if (!select) {
381
- const { select: partialFields, error } = this.buildPartialSelect((0, local_helpers_1.lowerCaseFirst)(relationInfo.type), query);
382
- if (error)
383
- return error;
384
- select = partialFields ? { [relationship]: { select: { ...partialFields } } } : { [relationship]: true };
385
- }
386
- const args = {
387
- where: this.makePrismaIdFilter(typeInfo.idFields, resourceId),
388
- select,
389
- };
390
- if (relationInfo.isCollection) {
391
- // if related data is a collection, it can be filtered, sorted, and paginated
392
- const error = this.injectRelationQuery(relationInfo.type, select, relationship, query);
393
- if (error) {
394
- return error;
395
- }
396
- }
397
- const entity = await prisma[type].findUnique(args);
398
- let paginator;
399
- if (entity?._count?.[relationship] !== undefined) {
400
- // build up paginator
401
- const total = entity?._count?.[relationship];
402
- const url = this.makeNormalizedUrl(`/${type}/${resourceId}/${relationship}`, query);
403
- const { offset, limit } = this.getPagination(query);
404
- paginator = this.makePaginator(url, offset, limit, total);
405
- }
406
- if (entity?.[relationship]) {
407
- const mappedType = this.mapModelName(type);
408
- return {
409
- status: 200,
410
- body: await this.serializeItems(relationInfo.type, entity[relationship], {
411
- linkers: {
412
- document: new ts_japi_1.Linker(() => this.makeLinkUrl(`/${mappedType}/${resourceId}/${relationship}`)),
413
- paginator,
414
- },
415
- include,
416
- }),
417
- };
418
- }
419
- else {
420
- return this.makeError('notFound');
421
- }
422
- }
423
- async processReadRelationship(prisma, type, resourceId, relationship, query) {
424
- const typeInfo = this.typeMap[type];
425
- if (!typeInfo) {
426
- return this.makeUnsupportedModelError(type);
427
- }
428
- const relationInfo = typeInfo.relationships[relationship];
429
- if (!relationInfo) {
430
- return this.makeUnsupportedRelationshipError(type, relationship, 404);
431
- }
432
- const args = {
433
- where: this.makePrismaIdFilter(typeInfo.idFields, resourceId),
434
- select: this.makeIdSelect(typeInfo.idFields),
435
- };
436
- // include IDs of relation fields so that they can be serialized
437
- args.select = { ...args.select, [relationship]: { select: this.makeIdSelect(relationInfo.idFields) } };
438
- let paginator;
439
- if (relationInfo.isCollection) {
440
- // if related data is a collection, it can be filtered, sorted, and paginated
441
- const error = this.injectRelationQuery(relationInfo.type, args.select, relationship, query);
442
- if (error) {
443
- return error;
444
- }
445
- }
446
- const entity = await prisma[type].findUnique(args);
447
- const mappedType = this.mapModelName(type);
448
- if (entity?._count?.[relationship] !== undefined) {
449
- // build up paginator
450
- const total = entity?._count?.[relationship];
451
- const url = this.makeNormalizedUrl(`/${mappedType}/${resourceId}/relationships/${relationship}`, query);
452
- const { offset, limit } = this.getPagination(query);
453
- paginator = this.makePaginator(url, offset, limit, total);
454
- }
455
- if (entity?.[relationship]) {
456
- const serialized = await this.serializeItems(relationInfo.type, entity[relationship], {
457
- linkers: {
458
- document: new ts_japi_1.Linker(() => this.makeLinkUrl(`/${mappedType}/${resourceId}/relationships/${relationship}`)),
459
- paginator,
460
- },
461
- onlyIdentifier: true,
462
- });
463
- return {
464
- status: 200,
465
- body: serialized,
466
- };
467
- }
468
- else {
469
- return this.makeError('notFound');
470
- }
471
- }
472
- async processCollectionRead(prisma, type, query) {
473
- const typeInfo = this.typeMap[type];
474
- if (!typeInfo) {
475
- return this.makeUnsupportedModelError(type);
476
- }
477
- const args = {};
478
- // add filter
479
- const { filter, error: filterError } = this.buildFilter(type, query);
480
- if (filterError) {
481
- return filterError;
482
- }
483
- if (filter) {
484
- args.where = filter;
485
- }
486
- const { sort, error: sortError } = this.buildSort(type, query);
487
- if (sortError) {
488
- return sortError;
489
- }
490
- if (sort) {
491
- args.orderBy = sort;
492
- }
493
- // include IDs of relation fields so that they can be serialized
494
- this.includeRelationshipIds(type, args, 'include');
495
- // handle "include" query parameter
496
- let include;
497
- if (query?.include) {
498
- const { select, error, allIncludes } = this.buildRelationSelect(type, query.include, query);
499
- if (error) {
500
- return error;
501
- }
502
- if (select) {
503
- args.include = { ...args.include, ...select };
504
- }
505
- include = allIncludes;
506
- }
507
- // handle partial results for requested type
508
- const { select, error } = this.buildPartialSelect(type, query);
509
- if (error)
510
- return error;
511
- if (select) {
512
- args.select = { ...select, ...args.select };
513
- if (args.include) {
514
- args.select = {
515
- ...args.select,
516
- ...args.include,
517
- };
518
- args.include = undefined;
519
- }
520
- }
521
- const { offset, limit } = this.getPagination(query);
522
- if (offset > 0) {
523
- args.skip = offset;
524
- }
525
- if (limit === Infinity) {
526
- const entities = await prisma[type].findMany(args);
527
- const body = await this.serializeItems(type, entities, { include });
528
- const total = entities.length;
529
- body.meta = this.addTotalCountToMeta(body.meta, total);
530
- return {
531
- status: 200,
532
- body: body,
533
- };
534
- }
535
- else {
536
- args.take = limit;
537
- const [entities, count] = await Promise.all([
538
- prisma[type].findMany(args),
539
- prisma[type].count({ where: args.where ?? {} }),
540
- ]);
541
- const total = count;
542
- const mappedType = this.mapModelName(type);
543
- const url = this.makeNormalizedUrl(`/${mappedType}`, query);
544
- const options = {
545
- include,
546
- linkers: {
547
- paginator: this.makePaginator(url, offset, limit, total),
548
- },
549
- };
550
- const body = await this.serializeItems(type, entities, options);
551
- body.meta = this.addTotalCountToMeta(body.meta, total);
552
- return {
553
- status: 200,
554
- body: body,
555
- };
556
- }
557
- }
558
- buildPartialSelect(type, query) {
559
- const selectFieldsQuery = query?.[`fields[${type}]`];
560
- if (!selectFieldsQuery) {
561
- return { select: undefined, error: undefined };
562
- }
563
- if (Array.isArray(selectFieldsQuery)) {
564
- return {
565
- select: undefined,
566
- error: this.makeError('duplicatedFieldsParameter', `duplicated fields query for type ${type}`),
567
- };
568
- }
569
- const typeInfo = this.typeMap[(0, local_helpers_1.lowerCaseFirst)(type)];
570
- if (!typeInfo) {
571
- return { select: undefined, error: this.makeUnsupportedModelError(type) };
572
- }
573
- const selectFieldNames = selectFieldsQuery.split(',').filter((i) => i);
574
- const fields = selectFieldNames.reduce((acc, curr) => ({ ...acc, [curr]: true }), {});
575
- return {
576
- select: { ...this.makeIdSelect(typeInfo.idFields), ...fields },
577
- };
578
- }
579
- addTotalCountToMeta(meta, total) {
580
- return meta ? Object.assign(meta, { total }) : Object.assign({}, { total });
581
- }
582
- makePaginator(baseUrl, offset, limit, total) {
583
- if (limit === Infinity) {
584
- return undefined;
585
- }
586
- const totalPages = Math.ceil(total / limit);
587
- return new ts_japi_1.Paginator(() => ({
588
- first: this.replaceURLSearchParams(baseUrl, { 'page[limit]': limit }),
589
- last: this.replaceURLSearchParams(baseUrl, {
590
- 'page[offset]': (totalPages - 1) * limit,
591
- }),
592
- prev: offset - limit >= 0 && offset - limit <= total - 1
593
- ? this.replaceURLSearchParams(baseUrl, {
594
- 'page[offset]': offset - limit,
595
- 'page[limit]': limit,
596
- })
597
- : null,
598
- next: offset + limit <= total - 1
599
- ? this.replaceURLSearchParams(baseUrl, {
600
- 'page[offset]': offset + limit,
601
- 'page[limit]': limit,
602
- })
603
- : null,
604
- }));
605
- }
606
- processRequestBody(type, requestBody, zodSchemas, mode) {
607
- let body = requestBody;
608
- if (body.meta?.serialization) {
609
- // superjson deserialize body if a serialization meta is provided
610
- body = superjson_1.default.deserialize({ json: body, meta: body.meta.serialization });
611
- }
612
- const parsed = this.createUpdatePayloadSchema.parse(body);
613
- const attributes = parsed.data.attributes;
614
- if (attributes) {
615
- // use the zod schema (that only contains non-relation fields) to validate the payload,
616
- // if available
617
- const schemaName = `${(0, local_helpers_1.upperCaseFirst)(type)}${(0, local_helpers_1.upperCaseFirst)(mode)}ScalarSchema`;
618
- const payloadSchema = zodSchemas?.models?.[schemaName];
619
- if (payloadSchema) {
620
- const parsed = payloadSchema.safeParse(attributes);
621
- if (!parsed.success) {
622
- return {
623
- error: this.makeError('invalidPayload', (0, local_helpers_1.getZodErrorMessage)(parsed.error), 422, runtime_1.CrudFailureReason.DATA_VALIDATION_VIOLATION, parsed.error),
624
- };
625
- }
626
- }
627
- }
628
- return { attributes, relationships: parsed.data.relationships };
629
- }
630
- async processCreate(prisma, type, _query, requestBody, modelMeta, zodSchemas) {
631
- const typeInfo = this.typeMap[type];
632
- if (!typeInfo) {
633
- return this.makeUnsupportedModelError(type);
634
- }
635
- const { error, attributes, relationships } = this.processRequestBody(type, requestBody, zodSchemas, 'create');
636
- if (error) {
637
- return error;
638
- }
639
- const createPayload = { data: { ...attributes } };
640
- // turn relationship payload into Prisma connect objects
641
- if (relationships) {
642
- for (const [key, data] of Object.entries(relationships)) {
643
- if (!data?.data) {
644
- return this.makeError('invalidRelationData');
645
- }
646
- const relationInfo = typeInfo.relationships[key];
647
- if (!relationInfo) {
648
- return this.makeUnsupportedRelationshipError(type, key, 400);
649
- }
650
- if (relationInfo.isCollection) {
651
- createPayload.data[key] = {
652
- connect: (0, runtime_1.enumerate)(data.data).map((item) => this.makeIdConnect(relationInfo.idFields, item.id)),
653
- };
654
- }
655
- else {
656
- if (typeof data.data !== 'object') {
657
- return this.makeError('invalidRelationData');
658
- }
659
- createPayload.data[key] = {
660
- connect: this.makeIdConnect(relationInfo.idFields, data.data.id),
661
- };
662
- }
663
- // make sure ID fields are included for result serialization
664
- createPayload.include = {
665
- ...createPayload.include,
666
- [key]: { select: { [this.makePrismaIdKey(relationInfo.idFields)]: true } },
667
- };
668
- }
669
- }
670
- // include IDs of relation fields so that they can be serialized.
671
- this.includeRelationshipIds(type, createPayload, 'include');
672
- const entity = await prisma[type].create(createPayload);
673
- return {
674
- status: 201,
675
- body: await this.serializeItems(type, entity),
676
- };
677
- }
678
- async processUpsert(prisma, type, _query, requestBody, modelMeta, zodSchemas) {
679
- const typeInfo = this.typeMap[type];
680
- if (!typeInfo) {
681
- return this.makeUnsupportedModelError(type);
682
- }
683
- const { error, attributes, relationships } = this.processRequestBody(type, requestBody, zodSchemas, 'create');
684
- if (error) {
685
- return error;
686
- }
687
- const matchFields = this.upsertMetaSchema.parse(requestBody).meta.matchFields;
688
- const uniqueFields = Object.values(modelMeta.models[type].uniqueConstraints || {}).map((uf) => uf.fields);
689
- if (!uniqueFields.some((uniqueCombination) => uniqueCombination.every((field) => matchFields.includes(field)))) {
690
- return this.makeError('invalidPayload', 'Match fields must be unique fields', 400);
691
- }
692
- const upsertPayload = {
693
- where: this.makeUpsertWhere(matchFields, attributes, typeInfo),
694
- create: { ...attributes },
695
- update: {
696
- ...Object.fromEntries(Object.entries(attributes).filter((e) => !matchFields.includes(e[0]))),
697
- },
698
- };
699
- if (relationships) {
700
- for (const [key, data] of Object.entries(relationships)) {
701
- if (!data?.data) {
702
- return this.makeError('invalidRelationData');
703
- }
704
- const relationInfo = typeInfo.relationships[key];
705
- if (!relationInfo) {
706
- return this.makeUnsupportedRelationshipError(type, key, 400);
707
- }
708
- if (relationInfo.isCollection) {
709
- upsertPayload.create[key] = {
710
- connect: (0, runtime_1.enumerate)(data.data).map((item) => this.makeIdConnect(relationInfo.idFields, item.id)),
711
- };
712
- upsertPayload.update[key] = {
713
- set: (0, runtime_1.enumerate)(data.data).map((item) => this.makeIdConnect(relationInfo.idFields, item.id)),
714
- };
715
- }
716
- else {
717
- if (typeof data.data !== 'object') {
718
- return this.makeError('invalidRelationData');
719
- }
720
- upsertPayload.create[key] = {
721
- connect: this.makeIdConnect(relationInfo.idFields, data.data.id),
722
- };
723
- upsertPayload.update[key] = {
724
- connect: this.makeIdConnect(relationInfo.idFields, data.data.id),
725
- };
726
- }
727
- }
728
- }
729
- // include IDs of relation fields so that they can be serialized.
730
- this.includeRelationshipIds(type, upsertPayload, 'include');
731
- const entity = await prisma[type].upsert(upsertPayload);
732
- return {
733
- status: 201,
734
- body: await this.serializeItems(type, entity),
735
- };
736
- }
737
- async processRelationshipCRUD(prisma, mode, type, resourceId, relationship, query, requestBody) {
738
- const typeInfo = this.typeMap[type];
739
- if (!typeInfo) {
740
- return this.makeUnsupportedModelError(type);
741
- }
742
- const relationInfo = typeInfo.relationships[relationship];
743
- if (!relationInfo) {
744
- return this.makeUnsupportedRelationshipError(type, relationship, 404);
745
- }
746
- if (!relationInfo.isCollection && mode !== 'update') {
747
- // to-one relation can only be updated
748
- return this.makeError('invalidVerb');
749
- }
750
- const updateArgs = {
751
- where: this.makePrismaIdFilter(typeInfo.idFields, resourceId),
752
- select: {
753
- ...typeInfo.idFields.reduce((acc, field) => ({ ...acc, [field.name]: true }), {}),
754
- [relationship]: { select: this.makeIdSelect(relationInfo.idFields) },
755
- },
756
- };
757
- if (!relationInfo.isCollection) {
758
- // zod-parse payload
759
- const parsed = this.updateSingleRelationSchema.safeParse(requestBody);
760
- if (!parsed.success) {
761
- return this.makeError('invalidPayload', (0, local_helpers_1.getZodErrorMessage)(parsed.error), undefined, runtime_1.CrudFailureReason.DATA_VALIDATION_VIOLATION, parsed.error);
762
- }
763
- if (parsed.data.data === null) {
764
- if (!relationInfo.isOptional) {
765
- // cannot disconnect a required relation
766
- return this.makeError('invalidPayload');
767
- }
768
- // set null -> disconnect
769
- updateArgs.data = {
770
- [relationship]: {
771
- disconnect: true,
772
- },
773
- };
774
- }
775
- else {
776
- updateArgs.data = {
777
- [relationship]: {
778
- connect: this.makeIdConnect(relationInfo.idFields, parsed.data.data.id),
779
- },
780
- };
781
- }
782
- }
783
- else {
784
- // zod-parse payload
785
- const parsed = this.updateCollectionRelationSchema.safeParse(requestBody);
786
- if (!parsed.success) {
787
- return this.makeError('invalidPayload', (0, local_helpers_1.getZodErrorMessage)(parsed.error), undefined, runtime_1.CrudFailureReason.DATA_VALIDATION_VIOLATION, parsed.error);
788
- }
789
- // create -> connect, delete -> disconnect, update -> set
790
- const relationVerb = mode === 'create' ? 'connect' : mode === 'delete' ? 'disconnect' : 'set';
791
- updateArgs.data = {
792
- [relationship]: {
793
- [relationVerb]: (0, runtime_1.enumerate)(parsed.data.data).map((item) => this.makePrismaIdFilter(relationInfo.idFields, item.id)),
794
- },
795
- };
796
- }
797
- const entity = await prisma[type].update(updateArgs);
798
- const mappedType = this.mapModelName(type);
799
- const serialized = await this.serializeItems(relationInfo.type, entity[relationship], {
800
- linkers: {
801
- document: new ts_japi_1.Linker(() => this.makeLinkUrl(`/${mappedType}/${resourceId}/relationships/${relationship}`)),
802
- },
803
- onlyIdentifier: true,
804
- });
805
- return {
806
- status: 200,
807
- body: serialized,
808
- };
809
- }
810
- async processUpdate(prisma, type, resourceId, _query, requestBody, modelMeta, zodSchemas) {
811
- const typeInfo = this.typeMap[type];
812
- if (!typeInfo) {
813
- return this.makeUnsupportedModelError(type);
814
- }
815
- const { error, attributes, relationships } = this.processRequestBody(type, requestBody, zodSchemas, 'update');
816
- if (error) {
817
- return error;
818
- }
819
- const updatePayload = {
820
- where: this.makePrismaIdFilter(typeInfo.idFields, resourceId),
821
- data: { ...attributes },
822
- };
823
- // turn relationships into prisma payload
824
- if (relationships) {
825
- for (const [key, data] of Object.entries(relationships)) {
826
- if (!data?.data) {
827
- return this.makeError('invalidRelationData');
828
- }
829
- const relationInfo = typeInfo.relationships[key];
830
- if (!relationInfo) {
831
- return this.makeUnsupportedRelationshipError(type, key, 400);
832
- }
833
- if (relationInfo.isCollection) {
834
- updatePayload.data[key] = {
835
- set: (0, runtime_1.enumerate)(data.data).map((item) => ({
836
- [this.makePrismaIdKey(relationInfo.idFields)]: item.id,
837
- })),
838
- };
839
- }
840
- else {
841
- if (typeof data.data !== 'object') {
842
- return this.makeError('invalidRelationData');
843
- }
844
- updatePayload.data[key] = {
845
- connect: {
846
- [this.makePrismaIdKey(relationInfo.idFields)]: data.data.id,
847
- },
848
- };
849
- }
850
- updatePayload.include = {
851
- ...updatePayload.include,
852
- [key]: { select: { [this.makePrismaIdKey(relationInfo.idFields)]: true } },
853
- };
854
- }
855
- }
856
- // include IDs of relation fields so that they can be serialized.
857
- this.includeRelationshipIds(type, updatePayload, 'include');
858
- const entity = await prisma[type].update(updatePayload);
859
- return {
860
- status: 200,
861
- body: await this.serializeItems(type, entity),
862
- };
863
- }
864
- async processDelete(prisma, type, resourceId) {
865
- const typeInfo = this.typeMap[type];
866
- if (!typeInfo) {
867
- return this.makeUnsupportedModelError(type);
868
- }
869
- await prisma[type].delete({
870
- where: this.makePrismaIdFilter(typeInfo.idFields, resourceId),
871
- });
872
- return {
873
- status: 200,
874
- body: { meta: {} },
875
- };
876
- }
877
- //#region utilities
878
- getIdFields(modelMeta, model) {
879
- const modelLower = (0, local_helpers_1.lowerCaseFirst)(model);
880
- if (!(modelLower in this.externalIdMapping)) {
881
- return (0, runtime_1.getIdFields)(modelMeta, model);
882
- }
883
- const metaData = modelMeta.models[modelLower] ?? {};
884
- const externalIdName = this.externalIdMapping[modelLower];
885
- const uniqueConstraints = metaData.uniqueConstraints ?? {};
886
- for (const [name, constraint] of Object.entries(uniqueConstraints)) {
887
- if (name === externalIdName) {
888
- return constraint.fields.map((f) => (0, runtime_1.requireField)(modelMeta, model, f));
889
- }
890
- }
891
- throw new Error(`Model ${model} does not have unique key ${externalIdName}`);
892
- }
893
- buildTypeMap(logger, modelMeta) {
894
- this.typeMap = {};
895
- for (const [model, { fields }] of Object.entries(modelMeta.models)) {
896
- const idFields = this.getIdFields(modelMeta, model);
897
- if (idFields.length === 0) {
898
- (0, utils_1.logWarning)(logger, `Not including model ${model} in the API because it has no ID field`);
899
- continue;
900
- }
901
- this.typeMap[model] = {
902
- idFields,
903
- relationships: {},
904
- fields,
905
- };
906
- for (const [field, fieldInfo] of Object.entries(fields)) {
907
- if (!fieldInfo.isDataModel) {
908
- continue;
909
- }
910
- const fieldTypeIdFields = this.getIdFields(modelMeta, fieldInfo.type);
911
- if (fieldTypeIdFields.length === 0) {
912
- (0, utils_1.logWarning)(logger, `Not including relation ${model}.${field} in the API because it has no ID field`);
913
- continue;
914
- }
915
- this.typeMap[model].relationships[field] = {
916
- type: fieldInfo.type,
917
- idFields: fieldTypeIdFields,
918
- isCollection: !!fieldInfo.isArray,
919
- isOptional: !!fieldInfo.isOptional,
920
- };
921
- }
922
- }
923
- }
924
- makeLinkUrl(path) {
925
- return `${this.options.endpoint}${path}`;
926
- }
927
- buildSerializers(modelMeta) {
928
- this.serializers = new Map();
929
- const linkers = {};
930
- for (const model of Object.keys(modelMeta.models)) {
931
- const ids = this.getIdFields(modelMeta, model);
932
- const mappedModel = this.mapModelName(model);
933
- if (ids.length < 1) {
934
- continue;
935
- }
936
- const linker = new ts_japi_1.Linker((items) => Array.isArray(items)
937
- ? this.makeLinkUrl(`/${mappedModel}`)
938
- : this.makeLinkUrl(`/${mappedModel}/${this.getId(model, items, modelMeta)}`));
939
- linkers[model] = linker;
940
- let projection = {};
941
- for (const [field, fieldMeta] of Object.entries(modelMeta.models[model].fields)) {
942
- if (fieldMeta.isDataModel) {
943
- projection[field] = 0;
944
- }
945
- }
946
- if (Object.keys(projection).length === 0) {
947
- projection = null;
948
- }
949
- const serializer = new ts_japi_1.Serializer(model, {
950
- version: '1.1',
951
- idKey: this.makeIdKey(ids),
952
- linkers: {
953
- resource: linker,
954
- document: linker,
955
- },
956
- projection,
957
- });
958
- this.serializers.set(model, serializer);
959
- }
960
- // set relators
961
- for (const model of Object.keys(modelMeta.models)) {
962
- const serializer = this.serializers.get(model);
963
- if (!serializer) {
964
- continue;
965
- }
966
- const relators = {};
967
- for (const [field, fieldMeta] of Object.entries(modelMeta.models[model].fields)) {
968
- if (!fieldMeta.isDataModel) {
969
- continue;
970
- }
971
- const fieldSerializer = this.serializers.get((0, local_helpers_1.lowerCaseFirst)(fieldMeta.type));
972
- if (!fieldSerializer) {
973
- continue;
974
- }
975
- const fieldIds = this.getIdFields(modelMeta, fieldMeta.type);
976
- if (fieldIds.length > 0) {
977
- const mappedModel = this.mapModelName(model);
978
- const relator = new ts_japi_1.Relator(async (data) => {
979
- return data[field];
980
- }, fieldSerializer, {
981
- relatedName: field,
982
- linkers: {
983
- related: new ts_japi_1.Linker((primary) => this.makeLinkUrl(`/${(0, local_helpers_1.lowerCaseFirst)(model)}/${this.getId(model, primary, modelMeta)}/${field}`)),
984
- relationship: new ts_japi_1.Linker((primary) => this.makeLinkUrl(`/${(0, local_helpers_1.lowerCaseFirst)(mappedModel)}/${this.getId(model, primary, modelMeta)}/relationships/${field}`)),
985
- },
986
- });
987
- relators[field] = relator;
988
- }
989
- }
990
- serializer.setRelators(relators);
991
- }
992
- }
993
- getId(model, data, modelMeta) {
994
- if (!data) {
995
- return undefined;
996
- }
997
- const ids = this.getIdFields(modelMeta, model);
998
- if (ids.length === 0) {
999
- return undefined;
1000
- }
1001
- else {
1002
- return data[this.makeIdKey(ids)];
1003
- }
1004
- }
1005
- async serializeItems(model, items, options) {
1006
- model = (0, local_helpers_1.lowerCaseFirst)(model);
1007
- const serializer = this.serializers.get(model);
1008
- if (!serializer) {
1009
- throw new Error(`serializer not found for model ${model}`);
1010
- }
1011
- const itemsWithId = (0, runtime_1.clone)(items);
1012
- this.injectCompoundId(model, itemsWithId);
1013
- // serialize to JSON:API structure
1014
- const serialized = await serializer.serialize(itemsWithId, options);
1015
- // convert the serialization result to plain object otherwise SuperJSON won't work
1016
- const plainResult = this.toPlainObject(serialized);
1017
- // superjson serialize the result
1018
- const { json, meta } = superjson_1.default.serialize(plainResult);
1019
- const result = json;
1020
- if (meta) {
1021
- result.meta = { ...result.meta, serialization: meta };
1022
- }
1023
- return result;
1024
- }
1025
- injectCompoundId(model, items) {
1026
- const typeInfo = this.typeMap[(0, local_helpers_1.lowerCaseFirst)(model)];
1027
- if (!typeInfo) {
1028
- return;
1029
- }
1030
- // recursively traverse the entity to create synthetic ID field for models with compound ID
1031
- (0, runtime_1.enumerate)(items).forEach((item) => {
1032
- if (!item) {
1033
- return;
1034
- }
1035
- if (typeInfo.idFields.length > 1) {
1036
- item[this.makeIdKey(typeInfo.idFields)] = this.makeCompoundId(typeInfo.idFields, item);
1037
- }
1038
- for (const [key, value] of Object.entries(item)) {
1039
- if (typeInfo.relationships[key]) {
1040
- // field is a relationship, recurse
1041
- this.injectCompoundId(typeInfo.relationships[key].type, value);
1042
- }
1043
- }
1044
- });
1045
- }
1046
- toPlainObject(data) {
1047
- if (data === undefined || data === null) {
1048
- return data;
1049
- }
1050
- if (Array.isArray(data)) {
1051
- return data.map((item) => this.toPlainObject(item));
1052
- }
1053
- if (typeof data === 'object') {
1054
- if (typeof data.toJSON === 'function') {
1055
- // custom toJSON function
1056
- return data.toJSON();
1057
- }
1058
- const result = {};
1059
- for (const [field, value] of Object.entries(data)) {
1060
- if (value === undefined || typeof value === 'function') {
1061
- // trim undefined and functions
1062
- continue;
1063
- }
1064
- else if (field === 'attributes') {
1065
- // don't visit into entity data
1066
- result[field] = value;
1067
- }
1068
- else {
1069
- result[field] = this.toPlainObject(value);
1070
- }
1071
- }
1072
- return result;
1073
- }
1074
- return data;
1075
- }
1076
- replaceURLSearchParams(url, params) {
1077
- const r = new URL(url);
1078
- for (const [key, value] of Object.entries(params)) {
1079
- r.searchParams.set(key, value.toString());
1080
- }
1081
- return r.toString();
1082
- }
1083
- makePrismaIdFilter(idFields, resourceId, nested = true) {
1084
- const decodedId = decodeURIComponent(resourceId);
1085
- if (idFields.length === 1) {
1086
- return { [idFields[0].name]: this.coerce(idFields[0], decodedId) };
1087
- }
1088
- else if (nested) {
1089
- return {
1090
- // TODO: support `@@id` with custom name
1091
- [idFields.map((idf) => idf.name).join(prismaIdDivider)]: idFields.reduce((acc, curr, idx) => ({
1092
- ...acc,
1093
- [curr.name]: this.coerce(curr, decodedId.split(this.idDivider)[idx]),
1094
- }), {}),
1095
- };
1096
- }
1097
- else {
1098
- return idFields.reduce((acc, curr, idx) => ({
1099
- ...acc,
1100
- [curr.name]: this.coerce(curr, decodedId.split(this.idDivider)[idx]),
1101
- }), {});
1102
- }
1103
- }
1104
- makeIdSelect(idFields) {
1105
- if (idFields.length === 0) {
1106
- throw this.errors.noId;
1107
- }
1108
- return idFields.reduce((acc, curr) => ({ ...acc, [curr.name]: true }), {});
1109
- }
1110
- makeIdConnect(idFields, id) {
1111
- if (idFields.length === 1) {
1112
- return { [idFields[0].name]: this.coerce(idFields[0], id) };
1113
- }
1114
- else {
1115
- return {
1116
- [this.makePrismaIdKey(idFields)]: idFields.reduce((acc, curr, idx) => ({
1117
- ...acc,
1118
- [curr.name]: this.coerce(curr, `${id}`.split(this.idDivider)[idx]),
1119
- }), {}),
1120
- };
1121
- }
1122
- }
1123
- makeIdKey(idFields) {
1124
- return idFields.map((idf) => idf.name).join(this.idDivider);
1125
- }
1126
- makePrismaIdKey(idFields) {
1127
- // TODO: support `@@id` with custom name
1128
- return idFields.map((idf) => idf.name).join(prismaIdDivider);
1129
- }
1130
- makeCompoundId(idFields, item) {
1131
- return idFields.map((idf) => item[idf.name]).join(this.idDivider);
1132
- }
1133
- makeUpsertWhere(matchFields, attributes, typeInfo) {
1134
- const where = matchFields.reduce((acc, field) => {
1135
- acc[field] = attributes[field] ?? null;
1136
- return acc;
1137
- }, {});
1138
- if (typeInfo.idFields.length > 1 &&
1139
- matchFields.some((mf) => typeInfo.idFields.map((idf) => idf.name).includes(mf))) {
1140
- return {
1141
- [this.makePrismaIdKey(typeInfo.idFields)]: where,
1142
- };
1143
- }
1144
- return where;
1145
- }
1146
- includeRelationshipIds(model, args, mode) {
1147
- const typeInfo = this.typeMap[model];
1148
- if (!typeInfo) {
1149
- return;
1150
- }
1151
- for (const [relation, relationInfo] of Object.entries(typeInfo.relationships)) {
1152
- args[mode] = { ...args[mode], [relation]: { select: this.makeIdSelect(relationInfo.idFields) } };
1153
- }
1154
- }
1155
- coerce(fieldInfo, value) {
1156
- if (typeof value === 'string') {
1157
- if (fieldInfo.isTypeDef || fieldInfo.type === 'Json') {
1158
- try {
1159
- return JSON.parse(value);
1160
- }
1161
- catch {
1162
- throw new InvalidValueError(`invalid JSON value: ${value}`);
1163
- }
1164
- }
1165
- const type = fieldInfo.type;
1166
- if (type === 'Int' || type === 'BigInt') {
1167
- const parsed = parseInt(value);
1168
- if (isNaN(parsed)) {
1169
- throw new InvalidValueError(`invalid ${type} value: ${value}`);
1170
- }
1171
- return parsed;
1172
- }
1173
- else if (type === 'Float' || type === 'Decimal') {
1174
- const parsed = parseFloat(value);
1175
- if (isNaN(parsed)) {
1176
- throw new InvalidValueError(`invalid ${type} value: ${value}`);
1177
- }
1178
- return parsed;
1179
- }
1180
- else if (type === 'Boolean') {
1181
- if (value === 'true') {
1182
- return true;
1183
- }
1184
- else if (value === 'false') {
1185
- return false;
1186
- }
1187
- else {
1188
- throw new InvalidValueError(`invalid ${type} value: ${value}`);
1189
- }
1190
- }
1191
- }
1192
- return value;
1193
- }
1194
- makeNormalizedUrl(path, query) {
1195
- const url = new URL(this.makeLinkUrl(path));
1196
- for (const [key, value] of Object.entries(query ?? {})) {
1197
- if (key.startsWith('filter[') ||
1198
- key.startsWith('sort[') ||
1199
- key.startsWith('include[') ||
1200
- key.startsWith('fields[')) {
1201
- for (const v of (0, runtime_1.enumerate)(value)) {
1202
- url.searchParams.append(key, v);
1203
- }
1204
- }
1205
- }
1206
- return url.toString();
1207
- }
1208
- getPagination(query) {
1209
- if (!query) {
1210
- return { offset: 0, limit: this.options.pageSize ?? DEFAULT_PAGE_SIZE };
1211
- }
1212
- let offset = 0;
1213
- if (query['page[offset]']) {
1214
- const value = query['page[offset]'];
1215
- const offsetText = Array.isArray(value) ? value[value.length - 1] : value;
1216
- offset = parseInt(offsetText);
1217
- if (isNaN(offset) || offset < 0) {
1218
- offset = 0;
1219
- }
1220
- }
1221
- let pageSizeOption = this.options.pageSize ?? DEFAULT_PAGE_SIZE;
1222
- if (pageSizeOption <= 0) {
1223
- pageSizeOption = DEFAULT_PAGE_SIZE;
1224
- }
1225
- let limit = pageSizeOption;
1226
- if (query['page[limit]']) {
1227
- const value = query['page[limit]'];
1228
- const limitText = Array.isArray(value) ? value[value.length - 1] : value;
1229
- limit = parseInt(limitText);
1230
- if (isNaN(limit) || limit <= 0) {
1231
- limit = pageSizeOption;
1232
- }
1233
- limit = Math.min(pageSizeOption, limit);
1234
- }
1235
- return { offset, limit };
1236
- }
1237
- buildFilter(type, query) {
1238
- if (!query) {
1239
- return { filter: undefined, error: undefined };
1240
- }
1241
- const typeInfo = this.typeMap[(0, local_helpers_1.lowerCaseFirst)(type)];
1242
- if (!typeInfo) {
1243
- return { filter: undefined, error: this.makeUnsupportedModelError(type) };
1244
- }
1245
- const items = [];
1246
- for (const [key, value] of Object.entries(query)) {
1247
- if (!value) {
1248
- continue;
1249
- }
1250
- // try matching query parameter key as "filter[x][y]..."
1251
- const match = key.match(this.filterParamPattern);
1252
- if (!match || !match.groups) {
1253
- continue;
1254
- }
1255
- const filterKeys = match.groups.match
1256
- .replaceAll(/[[\]]/g, ' ')
1257
- .split(' ')
1258
- .filter((i) => i);
1259
- if (!filterKeys.length) {
1260
- continue;
1261
- }
1262
- // turn filter into a nested Prisma query object
1263
- const item = {};
1264
- let curr = item;
1265
- let currType = typeInfo;
1266
- for (const filterValue of (0, runtime_1.enumerate)(value)) {
1267
- for (let i = 0; i < filterKeys.length; i++) {
1268
- // extract filter operation from (optional) trailing $op
1269
- let filterKey = filterKeys[i];
1270
- let filterOp;
1271
- const pos = filterKey.indexOf('$');
1272
- if (pos > 0) {
1273
- filterOp = filterKey.substring(pos + 1);
1274
- filterKey = filterKey.substring(0, pos);
1275
- }
1276
- if (!!filterOp && !FilterOperations.includes(filterOp)) {
1277
- return {
1278
- filter: undefined,
1279
- error: this.makeError('invalidFilter', `invalid filter operation: ${filterOp}`),
1280
- };
1281
- }
1282
- const fieldInfo = filterKey === 'id'
1283
- ? Object.values(currType.fields).find((f) => f.isId)
1284
- : currType.fields[filterKey];
1285
- if (!fieldInfo) {
1286
- return { filter: undefined, error: this.makeError('invalidFilter') };
1287
- }
1288
- if (!fieldInfo.isDataModel) {
1289
- // regular field
1290
- if (i !== filterKeys.length - 1) {
1291
- // must be the last segment of a filter
1292
- return { filter: undefined, error: this.makeError('invalidFilter') };
1293
- }
1294
- curr[fieldInfo.name] = this.makeFilterValue(fieldInfo, filterValue, filterOp);
1295
- }
1296
- else {
1297
- // relation field
1298
- if (i === filterKeys.length - 1) {
1299
- curr[fieldInfo.name] = this.makeFilterValue(fieldInfo, filterValue, filterOp);
1300
- }
1301
- else {
1302
- // keep going
1303
- if (fieldInfo.isArray) {
1304
- // collection filtering implies "some" operation
1305
- curr[fieldInfo.name] = { some: {} };
1306
- curr = curr[fieldInfo.name].some;
1307
- }
1308
- else {
1309
- curr = curr[fieldInfo.name] = {};
1310
- }
1311
- currType = this.typeMap[(0, local_helpers_1.lowerCaseFirst)(fieldInfo.type)];
1312
- }
1313
- }
1314
- }
1315
- items.push(item);
1316
- }
1317
- }
1318
- if (items.length === 0) {
1319
- return { filter: undefined, error: undefined };
1320
- }
1321
- else {
1322
- // combine filters with AND
1323
- return { filter: items.length === 1 ? items[0] : { AND: items }, error: undefined };
1324
- }
1325
- }
1326
- buildSort(type, query) {
1327
- if (!query?.['sort']) {
1328
- return { sort: undefined, error: undefined };
1329
- }
1330
- const typeInfo = this.typeMap[(0, local_helpers_1.lowerCaseFirst)(type)];
1331
- if (!typeInfo) {
1332
- return { sort: undefined, error: this.makeUnsupportedModelError(type) };
1333
- }
1334
- const result = [];
1335
- for (const sortSpec of (0, runtime_1.enumerate)(query['sort'])) {
1336
- const sortFields = sortSpec.split(',').filter((i) => i);
1337
- for (const sortField of sortFields) {
1338
- const dir = sortField.startsWith('-') ? 'desc' : 'asc';
1339
- const cleanedSortField = sortField.startsWith('-') ? sortField.substring(1) : sortField;
1340
- const parts = cleanedSortField.split('.').filter((i) => i);
1341
- const sortItem = {};
1342
- let curr = sortItem;
1343
- let currType = typeInfo;
1344
- for (let i = 0; i < parts.length; i++) {
1345
- const part = parts[i];
1346
- const fieldInfo = currType.fields[part];
1347
- if (!fieldInfo || fieldInfo.isArray) {
1348
- return {
1349
- sort: undefined,
1350
- error: this.makeError('invalidSort', 'sorting by array field is not supported'),
1351
- };
1352
- }
1353
- if (i === parts.length - 1) {
1354
- if (fieldInfo.isDataModel) {
1355
- // relation field: sort by id
1356
- const relationType = this.typeMap[(0, local_helpers_1.lowerCaseFirst)(fieldInfo.type)];
1357
- if (!relationType) {
1358
- return { sort: undefined, error: this.makeUnsupportedModelError(fieldInfo.type) };
1359
- }
1360
- curr[fieldInfo.name] = relationType.idFields.reduce((acc, idField) => {
1361
- acc[idField.name] = dir;
1362
- return acc;
1363
- }, {});
1364
- }
1365
- else {
1366
- // regular field
1367
- curr[fieldInfo.name] = dir;
1368
- }
1369
- }
1370
- else {
1371
- if (!fieldInfo.isDataModel) {
1372
- // must be a relation field
1373
- return {
1374
- sort: undefined,
1375
- error: this.makeError('invalidSort', 'intermediate sort segments must be relationships'),
1376
- };
1377
- }
1378
- // keep going
1379
- curr = curr[fieldInfo.name] = {};
1380
- currType = this.typeMap[(0, local_helpers_1.lowerCaseFirst)(fieldInfo.type)];
1381
- if (!currType) {
1382
- return { sort: undefined, error: this.makeUnsupportedModelError(fieldInfo.type) };
1383
- }
1384
- }
1385
- result.push(sortItem);
1386
- }
1387
- }
1388
- }
1389
- return { sort: result, error: undefined };
1390
- }
1391
- buildRelationSelect(type, include, query) {
1392
- const typeInfo = this.typeMap[(0, local_helpers_1.lowerCaseFirst)(type)];
1393
- if (!typeInfo) {
1394
- return { select: undefined, error: this.makeUnsupportedModelError(type) };
1395
- }
1396
- const result = {};
1397
- const allIncludes = [];
1398
- for (const includeItem of (0, runtime_1.enumerate)(include)) {
1399
- const inclusions = includeItem.split(',').filter((i) => i);
1400
- for (const inclusion of inclusions) {
1401
- allIncludes.push(inclusion);
1402
- const parts = inclusion.split('.');
1403
- let currPayload = result;
1404
- let currType = typeInfo;
1405
- for (let i = 0; i < parts.length; i++) {
1406
- const relation = parts[i];
1407
- const relationInfo = currType.relationships[relation];
1408
- if (!relationInfo) {
1409
- return { select: undefined, error: this.makeUnsupportedRelationshipError(type, relation, 400) };
1410
- }
1411
- currType = this.typeMap[(0, local_helpers_1.lowerCaseFirst)(relationInfo.type)];
1412
- if (!currType) {
1413
- return { select: undefined, error: this.makeUnsupportedModelError(relationInfo.type) };
1414
- }
1415
- // handle partial results for requested type
1416
- const { select, error } = this.buildPartialSelect((0, local_helpers_1.lowerCaseFirst)(relationInfo.type), query);
1417
- if (error)
1418
- return { select: undefined, error };
1419
- if (i !== parts.length - 1) {
1420
- if (select) {
1421
- currPayload[relation] = { select: { ...select } };
1422
- currPayload = currPayload[relation].select;
1423
- }
1424
- else {
1425
- currPayload[relation] = { include: { ...currPayload[relation]?.include } };
1426
- currPayload = currPayload[relation].include;
1427
- }
1428
- }
1429
- else {
1430
- currPayload[relation] = select
1431
- ? {
1432
- select: { ...select },
1433
- }
1434
- : true;
1435
- }
1436
- }
1437
- }
1438
- }
1439
- return { select: result, error: undefined, allIncludes };
1440
- }
1441
- makeFilterValue(fieldInfo, value, op) {
1442
- // TODO: inequality filters?
1443
- if (fieldInfo.isDataModel) {
1444
- // relation filter is converted to an ID filter
1445
- const info = this.typeMap[(0, local_helpers_1.lowerCaseFirst)(fieldInfo.type)];
1446
- if (fieldInfo.isArray) {
1447
- // filtering a to-many relation, imply 'some' operator
1448
- const values = value.split(',').filter((i) => i);
1449
- const filterValue = values.length > 1
1450
- ? { OR: values.map((v) => this.makePrismaIdFilter(info.idFields, v, false)) }
1451
- : this.makePrismaIdFilter(info.idFields, value, false);
1452
- return { some: filterValue };
1453
- }
1454
- else {
1455
- const values = value.split(',').filter((i) => i);
1456
- if (values.length > 1) {
1457
- return { OR: values.map((v) => this.makePrismaIdFilter(info.idFields, v, false)) };
1458
- }
1459
- else {
1460
- return { is: this.makePrismaIdFilter(info.idFields, value, false) };
1461
- }
1462
- }
1463
- }
1464
- else {
1465
- const coerced = this.coerce(fieldInfo, value);
1466
- switch (op) {
1467
- case 'icontains':
1468
- return { contains: coerced, mode: 'insensitive' };
1469
- case 'hasSome':
1470
- case 'hasEvery': {
1471
- const values = value
1472
- .split(',')
1473
- .filter((i) => i)
1474
- .map((v) => this.coerce(fieldInfo, v));
1475
- return { [op]: values };
1476
- }
1477
- case 'isEmpty':
1478
- if (value !== 'true' && value !== 'false') {
1479
- throw new InvalidValueError(`Not a boolean: ${value}`);
1480
- }
1481
- return { isEmpty: value === 'true' ? true : false };
1482
- default:
1483
- if (op === undefined) {
1484
- if (fieldInfo.isTypeDef || fieldInfo.type === 'Json') {
1485
- // handle JSON value equality filter
1486
- return { equals: coerced };
1487
- }
1488
- // regular filter, split value by comma
1489
- const values = value
1490
- .split(',')
1491
- .filter((i) => i)
1492
- .map((v) => this.coerce(fieldInfo, v));
1493
- return values.length > 1 ? { in: values } : { equals: values[0] };
1494
- }
1495
- else {
1496
- return { [op]: coerced };
1497
- }
1498
- }
1499
- }
1500
- }
1501
- injectRelationQuery(type, injectTarget, injectKey, query) {
1502
- const { filter, error: filterError } = this.buildFilter(type, query);
1503
- if (filterError) {
1504
- return filterError;
1505
- }
1506
- if (filter) {
1507
- injectTarget[injectKey] = { ...injectTarget[injectKey], where: filter };
1508
- }
1509
- const { sort, error: sortError } = this.buildSort(type, query);
1510
- if (sortError) {
1511
- return sortError;
1512
- }
1513
- if (sort) {
1514
- injectTarget[injectKey] = { ...injectTarget[injectKey], orderBy: sort };
1515
- }
1516
- const pagination = this.getPagination(query);
1517
- const offset = pagination.offset;
1518
- if (offset > 0) {
1519
- // inject skip
1520
- injectTarget[injectKey] = { ...injectTarget[injectKey], skip: offset };
1521
- }
1522
- const limit = pagination.limit;
1523
- if (limit !== Infinity) {
1524
- // inject take
1525
- injectTarget[injectKey] = { ...injectTarget[injectKey], take: limit };
1526
- // include a count query for the relationship
1527
- injectTarget._count = { select: { [injectKey]: true } };
1528
- }
1529
- }
1530
- handlePrismaError(err) {
1531
- if ((0, runtime_1.isPrismaClientKnownRequestError)(err)) {
1532
- if (err.code === runtime_1.PrismaErrorCode.CONSTRAINT_FAILED) {
1533
- if (err.meta?.reason === runtime_1.CrudFailureReason.DATA_VALIDATION_VIOLATION) {
1534
- return this.makeError('validationError', undefined, 422, err.meta?.reason, err.meta?.zodErrors);
1535
- }
1536
- else {
1537
- return this.makeError('forbidden', undefined, 403, err.meta?.reason);
1538
- }
1539
- }
1540
- else if (err.code === 'P2025' || err.code === 'P2018') {
1541
- return this.makeError('notFound');
1542
- }
1543
- else {
1544
- return {
1545
- status: 400,
1546
- body: {
1547
- errors: [
1548
- {
1549
- status: 400,
1550
- code: 'prisma-error',
1551
- prismaCode: err.code,
1552
- title: 'Prisma error',
1553
- detail: err.message,
1554
- },
1555
- ],
1556
- },
1557
- };
1558
- }
1559
- }
1560
- else {
1561
- const _err = err;
1562
- return this.makeError('unknownError', `${_err.message}\n${_err.stack}`);
1563
- }
1564
- }
1565
- makeError(code, detail, status, reason, zodErrors) {
1566
- const error = {
1567
- status: status ?? this.errors[code].status,
1568
- code: (0, local_helpers_1.paramCase)(code),
1569
- title: this.errors[code].title,
1570
- };
1571
- if (detail) {
1572
- error.detail = detail;
1573
- }
1574
- if (reason) {
1575
- error.reason = reason;
1576
- }
1577
- if (zodErrors) {
1578
- error.zodErrors = zodErrors;
1579
- }
1580
- return {
1581
- status: status ?? this.errors[code].status,
1582
- body: {
1583
- errors: [error],
1584
- },
1585
- };
1586
- }
1587
- makeUnsupportedModelError(model) {
1588
- return this.makeError('unsupportedModel', `Model ${model} doesn't exist`);
1589
- }
1590
- makeUnsupportedRelationshipError(model, relationship, status) {
1591
- return this.makeError('unsupportedRelationship', `Relationship ${model}.${relationship} doesn't exist`, status);
1592
- }
1593
- }
1594
- function makeHandler(options) {
1595
- const handler = new RequestHandler(options);
1596
- return handler.handleRequest.bind(handler);
1597
- }
1598
- //# sourceMappingURL=index.js.map