@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.
- package/LICENSE +1 -1
- package/dist/api.cjs +299 -0
- package/dist/api.cjs.map +1 -0
- package/dist/api.d.cts +28 -0
- package/dist/api.d.ts +28 -0
- package/dist/api.js +264 -0
- package/dist/api.js.map +1 -0
- package/dist/express.cjs +75 -0
- package/dist/express.cjs.map +1 -0
- package/dist/express.d.cts +31 -0
- package/dist/express.d.ts +31 -0
- package/dist/express.js +50 -0
- package/dist/express.js.map +1 -0
- package/dist/types-BH-88xJo.d.cts +68 -0
- package/dist/types-BH-88xJo.d.ts +68 -0
- package/package.json +57 -58
- package/README.md +0 -5
- package/api/base.d.ts +0 -49
- package/api/base.js +0 -19
- package/api/base.js.map +0 -1
- package/api/index.d.ts +0 -2
- package/api/index.js +0 -8
- package/api/index.js.map +0 -1
- package/api/rest/index.d.ts +0 -34
- package/api/rest/index.js +0 -1598
- package/api/rest/index.js.map +0 -1
- package/api/rpc/index.d.ts +0 -4
- package/api/rpc/index.js +0 -248
- package/api/rpc/index.js.map +0 -1
- package/api/utils.d.ts +0 -8
- package/api/utils.js +0 -54
- package/api/utils.js.map +0 -1
- package/elysia/handler.d.ts +0 -44
- package/elysia/handler.js +0 -62
- package/elysia/handler.js.map +0 -1
- package/elysia/index.d.ts +0 -1
- package/elysia/index.js +0 -18
- package/elysia/index.js.map +0 -1
- package/express/index.d.ts +0 -2
- package/express/index.js +0 -21
- package/express/index.js.map +0 -1
- package/express/middleware.d.ts +0 -27
- package/express/middleware.js +0 -58
- package/express/middleware.js.map +0 -1
- package/fastify/index.d.ts +0 -2
- package/fastify/index.js +0 -21
- package/fastify/index.js.map +0 -1
- package/fastify/plugin.d.ts +0 -18
- package/fastify/plugin.js +0 -48
- package/fastify/plugin.js.map +0 -1
- package/hono/handler.d.ts +0 -12
- package/hono/handler.js +0 -47
- package/hono/handler.js.map +0 -1
- package/hono/index.d.ts +0 -1
- package/hono/index.js +0 -18
- package/hono/index.js.map +0 -1
- package/nestjs/api-handler.service.d.ts +0 -15
- package/nestjs/api-handler.service.js +0 -72
- package/nestjs/api-handler.service.js.map +0 -1
- package/nestjs/index.d.ts +0 -3
- package/nestjs/index.js +0 -20
- package/nestjs/index.js.map +0 -1
- package/nestjs/interfaces/api-handler-options.interface.d.ts +0 -17
- package/nestjs/interfaces/api-handler-options.interface.js +0 -3
- package/nestjs/interfaces/api-handler-options.interface.js.map +0 -1
- package/nestjs/interfaces/index.d.ts +0 -2
- package/nestjs/interfaces/index.js +0 -19
- package/nestjs/interfaces/index.js.map +0 -1
- package/nestjs/interfaces/zenstack-module-options.interface.d.ts +0 -35
- package/nestjs/interfaces/zenstack-module-options.interface.js +0 -3
- package/nestjs/interfaces/zenstack-module-options.interface.js.map +0 -1
- package/nestjs/zenstack.constants.d.ts +0 -4
- package/nestjs/zenstack.constants.js +0 -8
- package/nestjs/zenstack.constants.js.map +0 -1
- package/nestjs/zenstack.module.d.ts +0 -12
- package/nestjs/zenstack.module.js +0 -61
- package/nestjs/zenstack.module.js.map +0 -1
- package/next/app-route-handler.d.ts +0 -16
- package/next/app-route-handler.js +0 -65
- package/next/app-route-handler.js.map +0 -1
- package/next/index.d.ts +0 -38
- package/next/index.js +0 -17
- package/next/index.js.map +0 -1
- package/next/pages-route-handler.d.ts +0 -9
- package/next/pages-route-handler.js +0 -45
- package/next/pages-route-handler.js.map +0 -1
- package/nuxt/handler.d.ts +0 -12
- package/nuxt/handler.js +0 -45
- package/nuxt/handler.js.map +0 -1
- package/nuxt/index.d.ts +0 -1
- package/nuxt/index.js +0 -18
- package/nuxt/index.js.map +0 -1
- package/shared.d.ts +0 -20
- package/shared.js +0 -50
- package/shared.js.map +0 -1
- package/sveltekit/handler.d.ts +0 -20
- package/sveltekit/handler.js +0 -68
- package/sveltekit/handler.js.map +0 -1
- package/sveltekit/index.d.ts +0 -2
- package/sveltekit/index.js +0 -21
- package/sveltekit/index.js.map +0 -1
- package/tanstack-start/handler.d.ts +0 -11
- package/tanstack-start/handler.js +0 -75
- package/tanstack-start/handler.js.map +0 -1
- package/tanstack-start/index.d.ts +0 -17
- package/tanstack-start/index.js +0 -16
- package/tanstack-start/index.js.map +0 -1
- package/types.d.ts +0 -49
- package/types.js +0 -3
- 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
|