@zenstackhq/server 3.0.0-beta.12 → 3.0.0-beta.14
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/dist/api.cjs +1767 -19
- package/dist/api.cjs.map +1 -1
- package/dist/api.d.cts +123 -4
- package/dist/api.d.ts +123 -4
- package/dist/api.js +1762 -15
- package/dist/api.js.map +1 -1
- package/dist/common-6DG-xEmM.d.cts +14 -0
- package/dist/common-CyapsM8n.d.ts +14 -0
- package/dist/elysia.cjs +118 -0
- package/dist/elysia.cjs.map +1 -0
- package/dist/elysia.d.cts +53 -0
- package/dist/elysia.d.ts +53 -0
- package/dist/elysia.js +83 -0
- package/dist/elysia.js.map +1 -0
- package/dist/express.cjs +41 -3
- package/dist/express.cjs.map +1 -1
- package/dist/express.d.cts +7 -7
- package/dist/express.d.ts +7 -7
- package/dist/express.js +30 -2
- package/dist/express.js.map +1 -1
- package/dist/fastify.cjs +103 -0
- package/dist/fastify.cjs.map +1 -0
- package/dist/fastify.d.cts +22 -0
- package/dist/fastify.d.ts +22 -0
- package/dist/fastify.js +68 -0
- package/dist/fastify.js.map +1 -0
- package/dist/hono.cjs +111 -0
- package/dist/hono.cjs.map +1 -0
- package/dist/hono.d.cts +18 -0
- package/dist/hono.d.ts +18 -0
- package/dist/hono.js +76 -0
- package/dist/hono.js.map +1 -0
- package/dist/next.cjs +178 -0
- package/dist/next.cjs.map +1 -0
- package/dist/next.d.cts +61 -0
- package/dist/next.d.ts +61 -0
- package/dist/next.js +143 -0
- package/dist/next.js.map +1 -0
- package/dist/nuxt.cjs +109 -0
- package/dist/nuxt.cjs.map +1 -0
- package/dist/nuxt.d.cts +19 -0
- package/dist/nuxt.d.ts +19 -0
- package/dist/nuxt.js +74 -0
- package/dist/nuxt.js.map +1 -0
- package/dist/sveltekit.cjs +134 -0
- package/dist/sveltekit.cjs.map +1 -0
- package/dist/sveltekit.d.cts +25 -0
- package/dist/sveltekit.d.ts +25 -0
- package/dist/sveltekit.js +99 -0
- package/dist/sveltekit.js.map +1 -0
- package/dist/tanstack-start.cjs +139 -0
- package/dist/tanstack-start.cjs.map +1 -0
- package/dist/tanstack-start.d.cts +32 -0
- package/dist/tanstack-start.d.ts +32 -0
- package/dist/tanstack-start.js +104 -0
- package/dist/tanstack-start.js.map +1 -0
- package/dist/{types-BH-88xJo.d.cts → types-D5t0sUEw.d.cts} +6 -2
- package/dist/{types-BH-88xJo.d.ts → types-D5t0sUEw.d.ts} +6 -2
- package/package.json +120 -8
package/dist/api.js
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
var __defProp = Object.defineProperty;
|
|
2
2
|
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
3
3
|
|
|
4
|
-
// src/api/
|
|
5
|
-
import { lowerCaseFirst,
|
|
6
|
-
import { InputValidationError, NotFoundError, RejectedByPolicyError, ZenStackError } from "@zenstackhq/
|
|
4
|
+
// src/api/rest/index.ts
|
|
5
|
+
import { clone, enumerate, lowerCaseFirst, paramCase } from "@zenstackhq/common-helpers";
|
|
6
|
+
import { InputValidationError, NotFoundError, QueryError, RejectedByPolicyError, ZenStackError } from "@zenstackhq/orm";
|
|
7
|
+
import { Decimal as Decimal2 } from "decimal.js";
|
|
7
8
|
import SuperJSON2 from "superjson";
|
|
9
|
+
import { Linker, Paginator, Relator, Serializer } from "ts-japi";
|
|
10
|
+
import UrlPattern from "url-pattern";
|
|
11
|
+
import z from "zod";
|
|
8
12
|
|
|
9
13
|
// src/api/utils.ts
|
|
10
14
|
import { Decimal } from "decimal.js";
|
|
11
15
|
import SuperJSON from "superjson";
|
|
12
16
|
import { match } from "ts-pattern";
|
|
17
|
+
import { fromError as fromError3 } from "zod-validation-error/v3";
|
|
18
|
+
import { fromError as fromError4 } from "zod-validation-error/v4";
|
|
13
19
|
function log(logger, level, message, error) {
|
|
14
20
|
if (!logger) {
|
|
15
21
|
return;
|
|
@@ -39,8 +45,1745 @@ function registerCustomSerializers() {
|
|
|
39
45
|
}
|
|
40
46
|
}
|
|
41
47
|
__name(registerCustomSerializers, "registerCustomSerializers");
|
|
48
|
+
function getZodErrorMessage(error) {
|
|
49
|
+
if ("_zod" in error) {
|
|
50
|
+
return fromError4(error).toString();
|
|
51
|
+
} else {
|
|
52
|
+
return fromError3(error).toString();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
__name(getZodErrorMessage, "getZodErrorMessage");
|
|
56
|
+
|
|
57
|
+
// src/api/rest/index.ts
|
|
58
|
+
var InvalidValueError = class InvalidValueError2 extends Error {
|
|
59
|
+
static {
|
|
60
|
+
__name(this, "InvalidValueError");
|
|
61
|
+
}
|
|
62
|
+
constructor(message) {
|
|
63
|
+
super(message);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
var DEFAULT_PAGE_SIZE = 100;
|
|
67
|
+
var FilterOperations = [
|
|
68
|
+
"lt",
|
|
69
|
+
"lte",
|
|
70
|
+
"gt",
|
|
71
|
+
"gte",
|
|
72
|
+
"contains",
|
|
73
|
+
"icontains",
|
|
74
|
+
"search",
|
|
75
|
+
"startsWith",
|
|
76
|
+
"endsWith",
|
|
77
|
+
"has",
|
|
78
|
+
"hasEvery",
|
|
79
|
+
"hasSome",
|
|
80
|
+
"isEmpty"
|
|
81
|
+
];
|
|
82
|
+
var DEFAULT_ID_DIVIDER = "_";
|
|
83
|
+
registerCustomSerializers();
|
|
84
|
+
var RestApiHandler = class {
|
|
85
|
+
static {
|
|
86
|
+
__name(this, "RestApiHandler");
|
|
87
|
+
}
|
|
88
|
+
options;
|
|
89
|
+
// resource serializers
|
|
90
|
+
serializers = /* @__PURE__ */ new Map();
|
|
91
|
+
// error responses
|
|
92
|
+
errors = {
|
|
93
|
+
unsupportedModel: {
|
|
94
|
+
status: 404,
|
|
95
|
+
title: "Unsupported model type",
|
|
96
|
+
detail: "The model type is not supported"
|
|
97
|
+
},
|
|
98
|
+
unsupportedRelationship: {
|
|
99
|
+
status: 400,
|
|
100
|
+
title: "Unsupported relationship",
|
|
101
|
+
detail: "The relationship is not supported"
|
|
102
|
+
},
|
|
103
|
+
invalidPath: {
|
|
104
|
+
status: 400,
|
|
105
|
+
title: "The request path is invalid"
|
|
106
|
+
},
|
|
107
|
+
invalidVerb: {
|
|
108
|
+
status: 400,
|
|
109
|
+
title: "The HTTP verb is not supported"
|
|
110
|
+
},
|
|
111
|
+
notFound: {
|
|
112
|
+
status: 404,
|
|
113
|
+
title: "Resource not found"
|
|
114
|
+
},
|
|
115
|
+
noId: {
|
|
116
|
+
status: 400,
|
|
117
|
+
title: "Model without an ID field is not supported"
|
|
118
|
+
},
|
|
119
|
+
invalidId: {
|
|
120
|
+
status: 400,
|
|
121
|
+
title: "Resource ID is invalid"
|
|
122
|
+
},
|
|
123
|
+
invalidPayload: {
|
|
124
|
+
status: 400,
|
|
125
|
+
title: "Invalid payload"
|
|
126
|
+
},
|
|
127
|
+
invalidRelationData: {
|
|
128
|
+
status: 400,
|
|
129
|
+
title: "Invalid relation data",
|
|
130
|
+
detail: "Invalid relationship data"
|
|
131
|
+
},
|
|
132
|
+
invalidRelation: {
|
|
133
|
+
status: 400,
|
|
134
|
+
title: "Invalid relation",
|
|
135
|
+
detail: "Invalid relationship"
|
|
136
|
+
},
|
|
137
|
+
invalidFilter: {
|
|
138
|
+
status: 400,
|
|
139
|
+
title: "Invalid filter"
|
|
140
|
+
},
|
|
141
|
+
invalidSort: {
|
|
142
|
+
status: 400,
|
|
143
|
+
title: "Invalid sort"
|
|
144
|
+
},
|
|
145
|
+
invalidValue: {
|
|
146
|
+
status: 400,
|
|
147
|
+
title: "Invalid value for type"
|
|
148
|
+
},
|
|
149
|
+
duplicatedFieldsParameter: {
|
|
150
|
+
status: 400,
|
|
151
|
+
title: "Fields Parameter Duplicated"
|
|
152
|
+
},
|
|
153
|
+
forbidden: {
|
|
154
|
+
status: 403,
|
|
155
|
+
title: "Operation is forbidden"
|
|
156
|
+
},
|
|
157
|
+
validationError: {
|
|
158
|
+
status: 422,
|
|
159
|
+
title: "Operation is unprocessable due to validation errors"
|
|
160
|
+
},
|
|
161
|
+
queryError: {
|
|
162
|
+
status: 400,
|
|
163
|
+
title: "Error occurred while executing the query"
|
|
164
|
+
},
|
|
165
|
+
unknownError: {
|
|
166
|
+
status: 500,
|
|
167
|
+
title: "Unknown error"
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
filterParamPattern = new RegExp(/^filter(?<match>(\[[^[\]]+\])+)$/);
|
|
171
|
+
// zod schema for payload of creating and updating a resource
|
|
172
|
+
createUpdatePayloadSchema = z.object({
|
|
173
|
+
data: z.object({
|
|
174
|
+
type: z.string(),
|
|
175
|
+
attributes: z.object({}).passthrough().optional(),
|
|
176
|
+
relationships: z.record(z.string(), z.object({
|
|
177
|
+
data: z.union([
|
|
178
|
+
z.object({
|
|
179
|
+
type: z.string(),
|
|
180
|
+
id: z.union([
|
|
181
|
+
z.string(),
|
|
182
|
+
z.number()
|
|
183
|
+
])
|
|
184
|
+
}),
|
|
185
|
+
z.array(z.object({
|
|
186
|
+
type: z.string(),
|
|
187
|
+
id: z.union([
|
|
188
|
+
z.string(),
|
|
189
|
+
z.number()
|
|
190
|
+
])
|
|
191
|
+
}))
|
|
192
|
+
])
|
|
193
|
+
})).optional()
|
|
194
|
+
}),
|
|
195
|
+
meta: z.object({}).passthrough().optional()
|
|
196
|
+
}).strict();
|
|
197
|
+
// zod schema for updating a single relationship
|
|
198
|
+
updateSingleRelationSchema = z.object({
|
|
199
|
+
data: z.object({
|
|
200
|
+
type: z.string(),
|
|
201
|
+
id: z.union([
|
|
202
|
+
z.string(),
|
|
203
|
+
z.number()
|
|
204
|
+
])
|
|
205
|
+
}).nullable()
|
|
206
|
+
});
|
|
207
|
+
// zod schema for updating collection relationship
|
|
208
|
+
updateCollectionRelationSchema = z.object({
|
|
209
|
+
data: z.array(z.object({
|
|
210
|
+
type: z.string(),
|
|
211
|
+
id: z.union([
|
|
212
|
+
z.string(),
|
|
213
|
+
z.number()
|
|
214
|
+
])
|
|
215
|
+
}))
|
|
216
|
+
});
|
|
217
|
+
upsertMetaSchema = z.object({
|
|
218
|
+
meta: z.object({
|
|
219
|
+
operation: z.literal("upsert"),
|
|
220
|
+
matchFields: z.array(z.string()).min(1)
|
|
221
|
+
})
|
|
222
|
+
});
|
|
223
|
+
// all known types and their metadata
|
|
224
|
+
typeMap = {};
|
|
225
|
+
// divider used to separate compound ID fields
|
|
226
|
+
idDivider;
|
|
227
|
+
urlPatternMap;
|
|
228
|
+
modelNameMapping;
|
|
229
|
+
reverseModelNameMapping;
|
|
230
|
+
externalIdMapping;
|
|
231
|
+
constructor(options) {
|
|
232
|
+
this.options = options;
|
|
233
|
+
this.idDivider = options.idDivider ?? DEFAULT_ID_DIVIDER;
|
|
234
|
+
const segmentCharset = options.urlSegmentCharset ?? "a-zA-Z0-9-_~ %";
|
|
235
|
+
this.modelNameMapping = options.modelNameMapping ?? {};
|
|
236
|
+
this.modelNameMapping = Object.fromEntries(Object.entries(this.modelNameMapping).map(([k, v]) => [
|
|
237
|
+
lowerCaseFirst(k),
|
|
238
|
+
v
|
|
239
|
+
]));
|
|
240
|
+
this.reverseModelNameMapping = Object.fromEntries(Object.entries(this.modelNameMapping).map(([k, v]) => [
|
|
241
|
+
v,
|
|
242
|
+
k
|
|
243
|
+
]));
|
|
244
|
+
this.externalIdMapping = options.externalIdMapping ?? {};
|
|
245
|
+
this.externalIdMapping = Object.fromEntries(Object.entries(this.externalIdMapping).map(([k, v]) => [
|
|
246
|
+
lowerCaseFirst(k),
|
|
247
|
+
v
|
|
248
|
+
]));
|
|
249
|
+
this.urlPatternMap = this.buildUrlPatternMap(segmentCharset);
|
|
250
|
+
this.buildTypeMap();
|
|
251
|
+
this.buildSerializers();
|
|
252
|
+
}
|
|
253
|
+
get schema() {
|
|
254
|
+
return this.options.schema;
|
|
255
|
+
}
|
|
256
|
+
get log() {
|
|
257
|
+
return this.options.log;
|
|
258
|
+
}
|
|
259
|
+
buildUrlPatternMap(urlSegmentNameCharset) {
|
|
260
|
+
const options = {
|
|
261
|
+
segmentValueCharset: urlSegmentNameCharset
|
|
262
|
+
};
|
|
263
|
+
const buildPath = /* @__PURE__ */ __name((segments) => {
|
|
264
|
+
return "/" + segments.join("/");
|
|
265
|
+
}, "buildPath");
|
|
266
|
+
return {
|
|
267
|
+
["single"]: new UrlPattern(buildPath([
|
|
268
|
+
":type",
|
|
269
|
+
":id"
|
|
270
|
+
]), options),
|
|
271
|
+
["fetchRelationship"]: new UrlPattern(buildPath([
|
|
272
|
+
":type",
|
|
273
|
+
":id",
|
|
274
|
+
":relationship"
|
|
275
|
+
]), options),
|
|
276
|
+
["relationship"]: new UrlPattern(buildPath([
|
|
277
|
+
":type",
|
|
278
|
+
":id",
|
|
279
|
+
"relationships",
|
|
280
|
+
":relationship"
|
|
281
|
+
]), options),
|
|
282
|
+
["collection"]: new UrlPattern(buildPath([
|
|
283
|
+
":type"
|
|
284
|
+
]), options)
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
mapModelName(modelName) {
|
|
288
|
+
return this.modelNameMapping[modelName] ?? modelName;
|
|
289
|
+
}
|
|
290
|
+
matchUrlPattern(path, routeType) {
|
|
291
|
+
const pattern = this.urlPatternMap[routeType];
|
|
292
|
+
if (!pattern) {
|
|
293
|
+
throw new InvalidValueError(`Unknown route type: ${routeType}`);
|
|
294
|
+
}
|
|
295
|
+
const match2 = pattern.match(path);
|
|
296
|
+
if (!match2) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if (match2.type in this.modelNameMapping) {
|
|
300
|
+
throw new InvalidValueError(`use the mapped model name: ${this.modelNameMapping[match2.type]} and not ${match2.type}`);
|
|
301
|
+
}
|
|
302
|
+
if (match2.type in this.reverseModelNameMapping) {
|
|
303
|
+
match2.type = this.reverseModelNameMapping[match2.type];
|
|
304
|
+
}
|
|
305
|
+
return match2;
|
|
306
|
+
}
|
|
307
|
+
async handleRequest({ client, method, path, query, requestBody }) {
|
|
308
|
+
method = method.toUpperCase();
|
|
309
|
+
if (!path.startsWith("/")) {
|
|
310
|
+
path = "/" + path;
|
|
311
|
+
}
|
|
312
|
+
try {
|
|
313
|
+
switch (method) {
|
|
314
|
+
case "GET": {
|
|
315
|
+
let match2 = this.matchUrlPattern(path, "single");
|
|
316
|
+
if (match2) {
|
|
317
|
+
return await this.processSingleRead(client, match2.type, match2.id, query);
|
|
318
|
+
}
|
|
319
|
+
match2 = this.matchUrlPattern(path, "fetchRelationship");
|
|
320
|
+
if (match2) {
|
|
321
|
+
return await this.processFetchRelated(client, match2.type, match2.id, match2.relationship, query);
|
|
322
|
+
}
|
|
323
|
+
match2 = this.matchUrlPattern(path, "relationship");
|
|
324
|
+
if (match2) {
|
|
325
|
+
return await this.processReadRelationship(client, match2.type, match2.id, match2.relationship, query);
|
|
326
|
+
}
|
|
327
|
+
match2 = this.matchUrlPattern(path, "collection");
|
|
328
|
+
if (match2) {
|
|
329
|
+
return await this.processCollectionRead(client, match2.type, query);
|
|
330
|
+
}
|
|
331
|
+
return this.makeError("invalidPath");
|
|
332
|
+
}
|
|
333
|
+
case "POST": {
|
|
334
|
+
if (!requestBody) {
|
|
335
|
+
return this.makeError("invalidPayload");
|
|
336
|
+
}
|
|
337
|
+
let match2 = this.matchUrlPattern(path, "collection");
|
|
338
|
+
if (match2) {
|
|
339
|
+
const body = requestBody;
|
|
340
|
+
const upsertMeta = this.upsertMetaSchema.safeParse(body);
|
|
341
|
+
if (upsertMeta.success) {
|
|
342
|
+
return await this.processUpsert(client, match2.type, query, requestBody);
|
|
343
|
+
} else {
|
|
344
|
+
return await this.processCreate(client, match2.type, query, requestBody);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
match2 = this.matchUrlPattern(path, "relationship");
|
|
348
|
+
if (match2) {
|
|
349
|
+
return await this.processRelationshipCRUD(client, "create", match2.type, match2.id, match2.relationship, query, requestBody);
|
|
350
|
+
}
|
|
351
|
+
return this.makeError("invalidPath");
|
|
352
|
+
}
|
|
353
|
+
// TODO: PUT for full update
|
|
354
|
+
case "PUT":
|
|
355
|
+
case "PATCH": {
|
|
356
|
+
if (!requestBody) {
|
|
357
|
+
return this.makeError("invalidPayload");
|
|
358
|
+
}
|
|
359
|
+
let match2 = this.matchUrlPattern(path, "single");
|
|
360
|
+
if (match2) {
|
|
361
|
+
return await this.processUpdate(client, match2.type, match2.id, query, requestBody);
|
|
362
|
+
}
|
|
363
|
+
match2 = this.matchUrlPattern(path, "relationship");
|
|
364
|
+
if (match2) {
|
|
365
|
+
return await this.processRelationshipCRUD(client, "update", match2.type, match2.id, match2.relationship, query, requestBody);
|
|
366
|
+
}
|
|
367
|
+
return this.makeError("invalidPath");
|
|
368
|
+
}
|
|
369
|
+
case "DELETE": {
|
|
370
|
+
let match2 = this.matchUrlPattern(path, "single");
|
|
371
|
+
if (match2) {
|
|
372
|
+
return await this.processDelete(client, match2.type, match2.id);
|
|
373
|
+
}
|
|
374
|
+
match2 = this.matchUrlPattern(path, "relationship");
|
|
375
|
+
if (match2) {
|
|
376
|
+
return await this.processRelationshipCRUD(client, "delete", match2.type, match2.id, match2.relationship, query, requestBody);
|
|
377
|
+
}
|
|
378
|
+
return this.makeError("invalidPath");
|
|
379
|
+
}
|
|
380
|
+
default:
|
|
381
|
+
return this.makeError("invalidPath");
|
|
382
|
+
}
|
|
383
|
+
} catch (err) {
|
|
384
|
+
if (err instanceof InvalidValueError) {
|
|
385
|
+
return this.makeError("invalidValue", err.message);
|
|
386
|
+
} else if (err instanceof ZenStackError) {
|
|
387
|
+
return this.handleZenStackError(err);
|
|
388
|
+
} else {
|
|
389
|
+
return this.handleGenericError(err);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
handleGenericError(err) {
|
|
394
|
+
return this.makeError("unknownError", err instanceof Error ? `${err.message}
|
|
395
|
+
${err.stack}` : "Unknown error");
|
|
396
|
+
}
|
|
397
|
+
async processSingleRead(client, type, resourceId, query) {
|
|
398
|
+
const typeInfo = this.getModelInfo(type);
|
|
399
|
+
if (!typeInfo) {
|
|
400
|
+
return this.makeUnsupportedModelError(type);
|
|
401
|
+
}
|
|
402
|
+
const args = {
|
|
403
|
+
where: this.makeIdFilter(typeInfo.idFields, resourceId)
|
|
404
|
+
};
|
|
405
|
+
this.includeRelationshipIds(type, args, "include");
|
|
406
|
+
let include;
|
|
407
|
+
if (query?.["include"]) {
|
|
408
|
+
const { select: select2, error: error2, allIncludes } = this.buildRelationSelect(type, query["include"], query);
|
|
409
|
+
if (error2) {
|
|
410
|
+
return error2;
|
|
411
|
+
}
|
|
412
|
+
if (select2) {
|
|
413
|
+
args.include = {
|
|
414
|
+
...args.include,
|
|
415
|
+
...select2
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
include = allIncludes;
|
|
419
|
+
}
|
|
420
|
+
const { select, error } = this.buildPartialSelect(type, query);
|
|
421
|
+
if (error) return error;
|
|
422
|
+
if (select) {
|
|
423
|
+
args.select = {
|
|
424
|
+
...select,
|
|
425
|
+
...args.select
|
|
426
|
+
};
|
|
427
|
+
if (args.include) {
|
|
428
|
+
args.select = {
|
|
429
|
+
...args.select,
|
|
430
|
+
...args.include
|
|
431
|
+
};
|
|
432
|
+
args.include = void 0;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
const entity = await client[type].findUnique(args);
|
|
436
|
+
if (entity) {
|
|
437
|
+
return {
|
|
438
|
+
status: 200,
|
|
439
|
+
body: await this.serializeItems(type, entity, {
|
|
440
|
+
include
|
|
441
|
+
})
|
|
442
|
+
};
|
|
443
|
+
} else {
|
|
444
|
+
return this.makeError("notFound");
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
async processFetchRelated(client, type, resourceId, relationship, query) {
|
|
448
|
+
const typeInfo = this.getModelInfo(type);
|
|
449
|
+
if (!typeInfo) {
|
|
450
|
+
return this.makeUnsupportedModelError(type);
|
|
451
|
+
}
|
|
452
|
+
const relationInfo = typeInfo.relationships[relationship];
|
|
453
|
+
if (!relationInfo) {
|
|
454
|
+
return this.makeUnsupportedRelationshipError(type, relationship, 404);
|
|
455
|
+
}
|
|
456
|
+
let select;
|
|
457
|
+
let include;
|
|
458
|
+
if (query?.["include"]) {
|
|
459
|
+
const { select: relationSelect, error, allIncludes } = this.buildRelationSelect(type, query["include"], query);
|
|
460
|
+
if (error) {
|
|
461
|
+
return error;
|
|
462
|
+
}
|
|
463
|
+
include = allIncludes.filter((i) => i.startsWith(`${relationship}.`)).map((i) => i.substring(`${relationship}.`.length));
|
|
464
|
+
select = relationSelect;
|
|
465
|
+
}
|
|
466
|
+
if (!select) {
|
|
467
|
+
const { select: partialFields, error } = this.buildPartialSelect(lowerCaseFirst(relationInfo.type), query);
|
|
468
|
+
if (error) return error;
|
|
469
|
+
select = partialFields ? {
|
|
470
|
+
[relationship]: {
|
|
471
|
+
select: {
|
|
472
|
+
...partialFields
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
} : {
|
|
476
|
+
[relationship]: true
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
const args = {
|
|
480
|
+
where: this.makeIdFilter(typeInfo.idFields, resourceId),
|
|
481
|
+
select
|
|
482
|
+
};
|
|
483
|
+
if (relationInfo.isCollection) {
|
|
484
|
+
const error = this.injectRelationQuery(relationInfo.type, select, relationship, query);
|
|
485
|
+
if (error) {
|
|
486
|
+
return error;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
const entity = await client[type].findUnique(args);
|
|
490
|
+
let paginator;
|
|
491
|
+
if (entity?._count?.[relationship] !== void 0) {
|
|
492
|
+
const total = entity?._count?.[relationship];
|
|
493
|
+
const url = this.makeNormalizedUrl(`/${type}/${resourceId}/${relationship}`, query);
|
|
494
|
+
const { offset, limit } = this.getPagination(query);
|
|
495
|
+
paginator = this.makePaginator(url, offset, limit, total);
|
|
496
|
+
}
|
|
497
|
+
if (entity?.[relationship]) {
|
|
498
|
+
const mappedType = this.mapModelName(type);
|
|
499
|
+
return {
|
|
500
|
+
status: 200,
|
|
501
|
+
body: await this.serializeItems(relationInfo.type, entity[relationship], {
|
|
502
|
+
linkers: {
|
|
503
|
+
document: new Linker(() => this.makeLinkUrl(`/${mappedType}/${resourceId}/${relationship}`)),
|
|
504
|
+
paginator
|
|
505
|
+
},
|
|
506
|
+
include
|
|
507
|
+
})
|
|
508
|
+
};
|
|
509
|
+
} else {
|
|
510
|
+
return this.makeError("notFound");
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
async processReadRelationship(client, type, resourceId, relationship, query) {
|
|
514
|
+
const typeInfo = this.getModelInfo(type);
|
|
515
|
+
if (!typeInfo) {
|
|
516
|
+
return this.makeUnsupportedModelError(type);
|
|
517
|
+
}
|
|
518
|
+
const relationInfo = typeInfo.relationships[relationship];
|
|
519
|
+
if (!relationInfo) {
|
|
520
|
+
return this.makeUnsupportedRelationshipError(type, relationship, 404);
|
|
521
|
+
}
|
|
522
|
+
const args = {
|
|
523
|
+
where: this.makeIdFilter(typeInfo.idFields, resourceId),
|
|
524
|
+
select: this.makeIdSelect(typeInfo.idFields)
|
|
525
|
+
};
|
|
526
|
+
args.select = {
|
|
527
|
+
...args.select,
|
|
528
|
+
[relationship]: {
|
|
529
|
+
select: this.makeIdSelect(relationInfo.idFields)
|
|
530
|
+
}
|
|
531
|
+
};
|
|
532
|
+
let paginator;
|
|
533
|
+
if (relationInfo.isCollection) {
|
|
534
|
+
const error = this.injectRelationQuery(relationInfo.type, args.select, relationship, query);
|
|
535
|
+
if (error) {
|
|
536
|
+
return error;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
const entity = await client[type].findUnique(args);
|
|
540
|
+
const mappedType = this.mapModelName(type);
|
|
541
|
+
if (entity?._count?.[relationship] !== void 0) {
|
|
542
|
+
const total = entity?._count?.[relationship];
|
|
543
|
+
const url = this.makeNormalizedUrl(`/${mappedType}/${resourceId}/relationships/${relationship}`, query);
|
|
544
|
+
const { offset, limit } = this.getPagination(query);
|
|
545
|
+
paginator = this.makePaginator(url, offset, limit, total);
|
|
546
|
+
}
|
|
547
|
+
if (entity?.[relationship]) {
|
|
548
|
+
const serialized = await this.serializeItems(relationInfo.type, entity[relationship], {
|
|
549
|
+
linkers: {
|
|
550
|
+
document: new Linker(() => this.makeLinkUrl(`/${mappedType}/${resourceId}/relationships/${relationship}`)),
|
|
551
|
+
paginator
|
|
552
|
+
},
|
|
553
|
+
onlyIdentifier: true
|
|
554
|
+
});
|
|
555
|
+
return {
|
|
556
|
+
status: 200,
|
|
557
|
+
body: serialized
|
|
558
|
+
};
|
|
559
|
+
} else {
|
|
560
|
+
return this.makeError("notFound");
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
async processCollectionRead(client, type, query) {
|
|
564
|
+
const typeInfo = this.getModelInfo(type);
|
|
565
|
+
if (!typeInfo) {
|
|
566
|
+
return this.makeUnsupportedModelError(type);
|
|
567
|
+
}
|
|
568
|
+
const args = {};
|
|
569
|
+
const { filter, error: filterError } = this.buildFilter(type, query);
|
|
570
|
+
if (filterError) {
|
|
571
|
+
return filterError;
|
|
572
|
+
}
|
|
573
|
+
if (filter) {
|
|
574
|
+
args.where = filter;
|
|
575
|
+
}
|
|
576
|
+
const { sort, error: sortError } = this.buildSort(type, query);
|
|
577
|
+
if (sortError) {
|
|
578
|
+
return sortError;
|
|
579
|
+
}
|
|
580
|
+
if (sort) {
|
|
581
|
+
args.orderBy = sort;
|
|
582
|
+
}
|
|
583
|
+
this.includeRelationshipIds(type, args, "include");
|
|
584
|
+
let include;
|
|
585
|
+
if (query?.["include"]) {
|
|
586
|
+
const { select: select2, error: error2, allIncludes } = this.buildRelationSelect(type, query["include"], query);
|
|
587
|
+
if (error2) {
|
|
588
|
+
return error2;
|
|
589
|
+
}
|
|
590
|
+
if (select2) {
|
|
591
|
+
args.include = {
|
|
592
|
+
...args.include,
|
|
593
|
+
...select2
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
include = allIncludes;
|
|
597
|
+
}
|
|
598
|
+
const { select, error } = this.buildPartialSelect(type, query);
|
|
599
|
+
if (error) return error;
|
|
600
|
+
if (select) {
|
|
601
|
+
args.select = {
|
|
602
|
+
...select,
|
|
603
|
+
...args.select
|
|
604
|
+
};
|
|
605
|
+
if (args.include) {
|
|
606
|
+
args.select = {
|
|
607
|
+
...args.select,
|
|
608
|
+
...args.include
|
|
609
|
+
};
|
|
610
|
+
args.include = void 0;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
const { offset, limit } = this.getPagination(query);
|
|
614
|
+
if (offset > 0) {
|
|
615
|
+
args.skip = offset;
|
|
616
|
+
}
|
|
617
|
+
if (limit === Infinity) {
|
|
618
|
+
const entities = await client[type].findMany(args);
|
|
619
|
+
const body = await this.serializeItems(type, entities, {
|
|
620
|
+
include
|
|
621
|
+
});
|
|
622
|
+
const total = entities.length;
|
|
623
|
+
body.meta = this.addTotalCountToMeta(body.meta, total);
|
|
624
|
+
return {
|
|
625
|
+
status: 200,
|
|
626
|
+
body
|
|
627
|
+
};
|
|
628
|
+
} else {
|
|
629
|
+
args.take = limit;
|
|
630
|
+
const [entities, count] = await Promise.all([
|
|
631
|
+
client[type].findMany(args),
|
|
632
|
+
client[type].count({
|
|
633
|
+
where: args.where ?? {}
|
|
634
|
+
})
|
|
635
|
+
]);
|
|
636
|
+
const total = count;
|
|
637
|
+
const mappedType = this.mapModelName(type);
|
|
638
|
+
const url = this.makeNormalizedUrl(`/${mappedType}`, query);
|
|
639
|
+
const options = {
|
|
640
|
+
include,
|
|
641
|
+
linkers: {
|
|
642
|
+
paginator: this.makePaginator(url, offset, limit, total)
|
|
643
|
+
}
|
|
644
|
+
};
|
|
645
|
+
const body = await this.serializeItems(type, entities, options);
|
|
646
|
+
body.meta = this.addTotalCountToMeta(body.meta, total);
|
|
647
|
+
return {
|
|
648
|
+
status: 200,
|
|
649
|
+
body
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
buildPartialSelect(type, query) {
|
|
654
|
+
const selectFieldsQuery = query?.[`fields[${type}]`];
|
|
655
|
+
if (!selectFieldsQuery) {
|
|
656
|
+
return {
|
|
657
|
+
select: void 0,
|
|
658
|
+
error: void 0
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
if (Array.isArray(selectFieldsQuery)) {
|
|
662
|
+
return {
|
|
663
|
+
select: void 0,
|
|
664
|
+
error: this.makeError("duplicatedFieldsParameter", `duplicated fields query for type ${type}`)
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
const typeInfo = this.getModelInfo(type);
|
|
668
|
+
if (!typeInfo) {
|
|
669
|
+
return {
|
|
670
|
+
select: void 0,
|
|
671
|
+
error: this.makeUnsupportedModelError(type)
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
const selectFieldNames = selectFieldsQuery.split(",").filter((i) => i);
|
|
675
|
+
const fields = selectFieldNames.reduce((acc, curr) => ({
|
|
676
|
+
...acc,
|
|
677
|
+
[curr]: true
|
|
678
|
+
}), {});
|
|
679
|
+
return {
|
|
680
|
+
select: {
|
|
681
|
+
...this.makeIdSelect(typeInfo.idFields),
|
|
682
|
+
...fields
|
|
683
|
+
}
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
addTotalCountToMeta(meta, total) {
|
|
687
|
+
return meta ? Object.assign(meta, {
|
|
688
|
+
total
|
|
689
|
+
}) : Object.assign({}, {
|
|
690
|
+
total
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
makePaginator(baseUrl, offset, limit, total) {
|
|
694
|
+
if (limit === Infinity) {
|
|
695
|
+
return void 0;
|
|
696
|
+
}
|
|
697
|
+
const totalPages = Math.ceil(total / limit);
|
|
698
|
+
return new Paginator(() => ({
|
|
699
|
+
first: this.replaceURLSearchParams(baseUrl, {
|
|
700
|
+
"page[limit]": limit
|
|
701
|
+
}),
|
|
702
|
+
last: this.replaceURLSearchParams(baseUrl, {
|
|
703
|
+
"page[offset]": (totalPages - 1) * limit
|
|
704
|
+
}),
|
|
705
|
+
prev: offset - limit >= 0 && offset - limit <= total - 1 ? this.replaceURLSearchParams(baseUrl, {
|
|
706
|
+
"page[offset]": offset - limit,
|
|
707
|
+
"page[limit]": limit
|
|
708
|
+
}) : null,
|
|
709
|
+
next: offset + limit <= total - 1 ? this.replaceURLSearchParams(baseUrl, {
|
|
710
|
+
"page[offset]": offset + limit,
|
|
711
|
+
"page[limit]": limit
|
|
712
|
+
}) : null
|
|
713
|
+
}));
|
|
714
|
+
}
|
|
715
|
+
processRequestBody(requestBody) {
|
|
716
|
+
let body = requestBody;
|
|
717
|
+
if (body.meta?.serialization) {
|
|
718
|
+
body = SuperJSON2.deserialize({
|
|
719
|
+
json: body,
|
|
720
|
+
meta: body.meta.serialization
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
const parseResult = this.createUpdatePayloadSchema.safeParse(body);
|
|
724
|
+
if (!parseResult.success) {
|
|
725
|
+
return {
|
|
726
|
+
attributes: void 0,
|
|
727
|
+
relationships: void 0,
|
|
728
|
+
error: this.makeError("invalidPayload", getZodErrorMessage(parseResult.error))
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
return {
|
|
732
|
+
attributes: parseResult.data.data.attributes,
|
|
733
|
+
relationships: parseResult.data.data.relationships,
|
|
734
|
+
error: void 0
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
async processCreate(client, type, _query, requestBody) {
|
|
738
|
+
const typeInfo = this.getModelInfo(type);
|
|
739
|
+
if (!typeInfo) {
|
|
740
|
+
return this.makeUnsupportedModelError(type);
|
|
741
|
+
}
|
|
742
|
+
const { attributes, relationships, error } = this.processRequestBody(requestBody);
|
|
743
|
+
if (error) {
|
|
744
|
+
return error;
|
|
745
|
+
}
|
|
746
|
+
const createPayload = {
|
|
747
|
+
data: {
|
|
748
|
+
...attributes
|
|
749
|
+
}
|
|
750
|
+
};
|
|
751
|
+
if (relationships) {
|
|
752
|
+
for (const [key, data] of Object.entries(relationships)) {
|
|
753
|
+
if (!data?.data) {
|
|
754
|
+
return this.makeError("invalidRelationData");
|
|
755
|
+
}
|
|
756
|
+
const relationInfo = typeInfo.relationships[key];
|
|
757
|
+
if (!relationInfo) {
|
|
758
|
+
return this.makeUnsupportedRelationshipError(type, key, 400);
|
|
759
|
+
}
|
|
760
|
+
if (relationInfo.isCollection) {
|
|
761
|
+
createPayload.data[key] = {
|
|
762
|
+
connect: enumerate(data.data).map((item) => this.makeIdConnect(relationInfo.idFields, item.id))
|
|
763
|
+
};
|
|
764
|
+
} else {
|
|
765
|
+
if (typeof data.data !== "object") {
|
|
766
|
+
return this.makeError("invalidRelationData");
|
|
767
|
+
}
|
|
768
|
+
createPayload.data[key] = {
|
|
769
|
+
connect: this.makeIdConnect(relationInfo.idFields, data.data.id)
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
createPayload.include = {
|
|
773
|
+
...createPayload.include,
|
|
774
|
+
[key]: {
|
|
775
|
+
select: {
|
|
776
|
+
[this.makeDefaultIdKey(relationInfo.idFields)]: true
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
this.includeRelationshipIds(type, createPayload, "include");
|
|
783
|
+
const entity = await client[type].create(createPayload);
|
|
784
|
+
return {
|
|
785
|
+
status: 201,
|
|
786
|
+
body: await this.serializeItems(type, entity)
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
async processUpsert(client, type, _query, requestBody) {
|
|
790
|
+
const typeInfo = this.getModelInfo(type);
|
|
791
|
+
if (!typeInfo) {
|
|
792
|
+
return this.makeUnsupportedModelError(type);
|
|
793
|
+
}
|
|
794
|
+
const modelName = typeInfo.name;
|
|
795
|
+
const { attributes, relationships, error } = this.processRequestBody(requestBody);
|
|
796
|
+
if (error) {
|
|
797
|
+
return error;
|
|
798
|
+
}
|
|
799
|
+
const parseResult = this.upsertMetaSchema.safeParse(requestBody);
|
|
800
|
+
if (parseResult.error) {
|
|
801
|
+
return this.makeError("invalidPayload", getZodErrorMessage(parseResult.error));
|
|
802
|
+
}
|
|
803
|
+
const matchFields = parseResult.data.meta.matchFields;
|
|
804
|
+
const uniqueFieldSets = this.getUniqueFieldSets(modelName);
|
|
805
|
+
if (!uniqueFieldSets.some((set) => set.every((field) => matchFields.includes(field)))) {
|
|
806
|
+
return this.makeError("invalidPayload", "Match fields must be unique fields", 400);
|
|
807
|
+
}
|
|
808
|
+
const upsertPayload = {
|
|
809
|
+
where: this.makeUpsertWhere(matchFields, attributes, typeInfo),
|
|
810
|
+
create: {
|
|
811
|
+
...attributes
|
|
812
|
+
},
|
|
813
|
+
update: {
|
|
814
|
+
...Object.fromEntries(Object.entries(attributes ?? {}).filter((e) => !matchFields.includes(e[0])))
|
|
815
|
+
}
|
|
816
|
+
};
|
|
817
|
+
if (relationships) {
|
|
818
|
+
for (const [key, data] of Object.entries(relationships)) {
|
|
819
|
+
if (!data?.data) {
|
|
820
|
+
return this.makeError("invalidRelationData");
|
|
821
|
+
}
|
|
822
|
+
const relationInfo = typeInfo.relationships[key];
|
|
823
|
+
if (!relationInfo) {
|
|
824
|
+
return this.makeUnsupportedRelationshipError(modelName, key, 400);
|
|
825
|
+
}
|
|
826
|
+
if (relationInfo.isCollection) {
|
|
827
|
+
upsertPayload.create[key] = {
|
|
828
|
+
connect: enumerate(data.data).map((item) => this.makeIdConnect(relationInfo.idFields, item.id))
|
|
829
|
+
};
|
|
830
|
+
upsertPayload.update[key] = {
|
|
831
|
+
set: enumerate(data.data).map((item) => this.makeIdConnect(relationInfo.idFields, item.id))
|
|
832
|
+
};
|
|
833
|
+
} else {
|
|
834
|
+
if (typeof data.data !== "object") {
|
|
835
|
+
return this.makeError("invalidRelationData");
|
|
836
|
+
}
|
|
837
|
+
upsertPayload.create[key] = {
|
|
838
|
+
connect: this.makeIdConnect(relationInfo.idFields, data.data.id)
|
|
839
|
+
};
|
|
840
|
+
upsertPayload.update[key] = {
|
|
841
|
+
connect: this.makeIdConnect(relationInfo.idFields, data.data.id)
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
this.includeRelationshipIds(modelName, upsertPayload, "include");
|
|
847
|
+
const entity = await client[modelName].upsert(upsertPayload);
|
|
848
|
+
return {
|
|
849
|
+
status: 201,
|
|
850
|
+
body: await this.serializeItems(modelName, entity)
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
getUniqueFieldSets(type) {
|
|
854
|
+
const modelDef = this.requireModel(type);
|
|
855
|
+
return Object.entries(modelDef.uniqueFields).map(([k, v]) => typeof v.type === "string" ? [
|
|
856
|
+
k
|
|
857
|
+
] : Object.keys(v));
|
|
858
|
+
}
|
|
859
|
+
async processRelationshipCRUD(client, mode, type, resourceId, relationship, _query, requestBody) {
|
|
860
|
+
const typeInfo = this.getModelInfo(type);
|
|
861
|
+
if (!typeInfo) {
|
|
862
|
+
return this.makeUnsupportedModelError(type);
|
|
863
|
+
}
|
|
864
|
+
const relationInfo = typeInfo.relationships[relationship];
|
|
865
|
+
if (!relationInfo) {
|
|
866
|
+
return this.makeUnsupportedRelationshipError(type, relationship, 404);
|
|
867
|
+
}
|
|
868
|
+
if (!relationInfo.isCollection && mode !== "update") {
|
|
869
|
+
return this.makeError("invalidVerb");
|
|
870
|
+
}
|
|
871
|
+
const updateArgs = {
|
|
872
|
+
where: this.makeIdFilter(typeInfo.idFields, resourceId),
|
|
873
|
+
select: {
|
|
874
|
+
...typeInfo.idFields.reduce((acc, field) => ({
|
|
875
|
+
...acc,
|
|
876
|
+
[field.name]: true
|
|
877
|
+
}), {}),
|
|
878
|
+
[relationship]: {
|
|
879
|
+
select: this.makeIdSelect(relationInfo.idFields)
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
};
|
|
883
|
+
if (!relationInfo.isCollection) {
|
|
884
|
+
const parsed = this.updateSingleRelationSchema.safeParse(requestBody);
|
|
885
|
+
if (!parsed.success) {
|
|
886
|
+
return this.makeError("invalidPayload", getZodErrorMessage(parsed.error));
|
|
887
|
+
}
|
|
888
|
+
if (parsed.data.data === null) {
|
|
889
|
+
if (!relationInfo.isOptional) {
|
|
890
|
+
return this.makeError("invalidPayload");
|
|
891
|
+
}
|
|
892
|
+
updateArgs.data = {
|
|
893
|
+
[relationship]: {
|
|
894
|
+
disconnect: true
|
|
895
|
+
}
|
|
896
|
+
};
|
|
897
|
+
} else {
|
|
898
|
+
updateArgs.data = {
|
|
899
|
+
[relationship]: {
|
|
900
|
+
connect: this.makeIdConnect(relationInfo.idFields, parsed.data.data.id)
|
|
901
|
+
}
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
} else {
|
|
905
|
+
const parsed = this.updateCollectionRelationSchema.safeParse(requestBody);
|
|
906
|
+
if (!parsed.success) {
|
|
907
|
+
return this.makeError("invalidPayload", getZodErrorMessage(parsed.error));
|
|
908
|
+
}
|
|
909
|
+
const relationVerb = mode === "create" ? "connect" : mode === "delete" ? "disconnect" : "set";
|
|
910
|
+
updateArgs.data = {
|
|
911
|
+
[relationship]: {
|
|
912
|
+
[relationVerb]: enumerate(parsed.data.data).map((item) => this.makeIdFilter(relationInfo.idFields, item.id))
|
|
913
|
+
}
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
const entity = await client[type].update(updateArgs);
|
|
917
|
+
const mappedType = this.mapModelName(type);
|
|
918
|
+
const serialized = await this.serializeItems(relationInfo.type, entity[relationship], {
|
|
919
|
+
linkers: {
|
|
920
|
+
document: new Linker(() => this.makeLinkUrl(`/${mappedType}/${resourceId}/relationships/${relationship}`))
|
|
921
|
+
},
|
|
922
|
+
onlyIdentifier: true
|
|
923
|
+
});
|
|
924
|
+
return {
|
|
925
|
+
status: 200,
|
|
926
|
+
body: serialized
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
async processUpdate(client, type, resourceId, _query, requestBody) {
|
|
930
|
+
const typeInfo = this.getModelInfo(type);
|
|
931
|
+
if (!typeInfo) {
|
|
932
|
+
return this.makeUnsupportedModelError(type);
|
|
933
|
+
}
|
|
934
|
+
const { attributes, relationships, error } = this.processRequestBody(requestBody);
|
|
935
|
+
if (error) {
|
|
936
|
+
return error;
|
|
937
|
+
}
|
|
938
|
+
const updatePayload = {
|
|
939
|
+
where: this.makeIdFilter(typeInfo.idFields, resourceId),
|
|
940
|
+
data: {
|
|
941
|
+
...attributes
|
|
942
|
+
}
|
|
943
|
+
};
|
|
944
|
+
if (relationships) {
|
|
945
|
+
for (const [key, data] of Object.entries(relationships)) {
|
|
946
|
+
if (!data?.data) {
|
|
947
|
+
return this.makeError("invalidRelationData");
|
|
948
|
+
}
|
|
949
|
+
const relationInfo = typeInfo.relationships[key];
|
|
950
|
+
if (!relationInfo) {
|
|
951
|
+
return this.makeUnsupportedRelationshipError(type, key, 400);
|
|
952
|
+
}
|
|
953
|
+
if (relationInfo.isCollection) {
|
|
954
|
+
updatePayload.data[key] = {
|
|
955
|
+
set: enumerate(data.data).map((item) => ({
|
|
956
|
+
[this.makeDefaultIdKey(relationInfo.idFields)]: item.id
|
|
957
|
+
}))
|
|
958
|
+
};
|
|
959
|
+
} else {
|
|
960
|
+
if (typeof data.data !== "object") {
|
|
961
|
+
return this.makeError("invalidRelationData");
|
|
962
|
+
}
|
|
963
|
+
updatePayload.data[key] = {
|
|
964
|
+
connect: {
|
|
965
|
+
[this.makeDefaultIdKey(relationInfo.idFields)]: data.data.id
|
|
966
|
+
}
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
updatePayload.include = {
|
|
970
|
+
...updatePayload.include,
|
|
971
|
+
[key]: {
|
|
972
|
+
select: {
|
|
973
|
+
[this.makeDefaultIdKey(relationInfo.idFields)]: true
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
this.includeRelationshipIds(type, updatePayload, "include");
|
|
980
|
+
const entity = await client[type].update(updatePayload);
|
|
981
|
+
return {
|
|
982
|
+
status: 200,
|
|
983
|
+
body: await this.serializeItems(type, entity)
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
async processDelete(client, type, resourceId) {
|
|
987
|
+
const typeInfo = this.getModelInfo(type);
|
|
988
|
+
if (!typeInfo) {
|
|
989
|
+
return this.makeUnsupportedModelError(type);
|
|
990
|
+
}
|
|
991
|
+
await client[type].delete({
|
|
992
|
+
where: this.makeIdFilter(typeInfo.idFields, resourceId)
|
|
993
|
+
});
|
|
994
|
+
return {
|
|
995
|
+
status: 200,
|
|
996
|
+
body: {
|
|
997
|
+
meta: {}
|
|
998
|
+
}
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
//#region utilities
|
|
1002
|
+
requireModel(model) {
|
|
1003
|
+
const modelDef = this.schema.models[model];
|
|
1004
|
+
if (!modelDef) {
|
|
1005
|
+
throw new Error(`Model ${model} is not defined in the schema`);
|
|
1006
|
+
}
|
|
1007
|
+
return modelDef;
|
|
1008
|
+
}
|
|
1009
|
+
getIdFields(model) {
|
|
1010
|
+
const modelDef = this.requireModel(model);
|
|
1011
|
+
const modelLower = lowerCaseFirst(model);
|
|
1012
|
+
if (!(modelLower in this.externalIdMapping)) {
|
|
1013
|
+
return Object.values(modelDef.fields).filter((f) => modelDef.idFields.includes(f.name));
|
|
1014
|
+
}
|
|
1015
|
+
const externalIdName = this.externalIdMapping[modelLower];
|
|
1016
|
+
for (const [name, info] of Object.entries(modelDef.uniqueFields)) {
|
|
1017
|
+
if (name === externalIdName) {
|
|
1018
|
+
if (typeof info.type === "string") {
|
|
1019
|
+
return [
|
|
1020
|
+
this.requireField(model, info.type)
|
|
1021
|
+
];
|
|
1022
|
+
} else {
|
|
1023
|
+
return Object.keys(info).map((f) => this.requireField(model, f));
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
throw new Error(`Model ${model} does not have unique key ${externalIdName}`);
|
|
1028
|
+
}
|
|
1029
|
+
requireField(model, field) {
|
|
1030
|
+
const modelDef = this.requireModel(model);
|
|
1031
|
+
const fieldDef = modelDef.fields[field];
|
|
1032
|
+
if (!fieldDef) {
|
|
1033
|
+
throw new Error(`Field ${field} is not defined in model ${model}`);
|
|
1034
|
+
}
|
|
1035
|
+
return fieldDef;
|
|
1036
|
+
}
|
|
1037
|
+
buildTypeMap() {
|
|
1038
|
+
this.typeMap = {};
|
|
1039
|
+
for (const [model, { fields }] of Object.entries(this.schema.models)) {
|
|
1040
|
+
const idFields = this.getIdFields(model);
|
|
1041
|
+
if (idFields.length === 0) {
|
|
1042
|
+
log(this.options.log, "warn", `Not including model ${model} in the API because it has no ID field`);
|
|
1043
|
+
continue;
|
|
1044
|
+
}
|
|
1045
|
+
const modelInfo = this.typeMap[lowerCaseFirst(model)] = {
|
|
1046
|
+
name: model,
|
|
1047
|
+
idFields,
|
|
1048
|
+
relationships: {},
|
|
1049
|
+
fields
|
|
1050
|
+
};
|
|
1051
|
+
for (const [field, fieldInfo] of Object.entries(fields)) {
|
|
1052
|
+
if (!fieldInfo.relation) {
|
|
1053
|
+
continue;
|
|
1054
|
+
}
|
|
1055
|
+
const fieldTypeIdFields = this.getIdFields(fieldInfo.type);
|
|
1056
|
+
if (fieldTypeIdFields.length === 0) {
|
|
1057
|
+
log(this.options.log, "warn", `Not including relation ${model}.${field} in the API because it has no ID field`);
|
|
1058
|
+
continue;
|
|
1059
|
+
}
|
|
1060
|
+
modelInfo.relationships[field] = {
|
|
1061
|
+
type: fieldInfo.type,
|
|
1062
|
+
idFields: fieldTypeIdFields,
|
|
1063
|
+
isCollection: !!fieldInfo.array,
|
|
1064
|
+
isOptional: !!fieldInfo.optional
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
getModelInfo(model) {
|
|
1070
|
+
return this.typeMap[lowerCaseFirst(model)];
|
|
1071
|
+
}
|
|
1072
|
+
makeLinkUrl(path) {
|
|
1073
|
+
return `${this.options.endpoint}${path}`;
|
|
1074
|
+
}
|
|
1075
|
+
buildSerializers() {
|
|
1076
|
+
const linkers = {};
|
|
1077
|
+
for (const model of Object.keys(this.schema.models)) {
|
|
1078
|
+
const ids = this.getIdFields(model);
|
|
1079
|
+
const modelLower = lowerCaseFirst(model);
|
|
1080
|
+
const mappedModel = this.mapModelName(modelLower);
|
|
1081
|
+
if (ids.length < 1) {
|
|
1082
|
+
continue;
|
|
1083
|
+
}
|
|
1084
|
+
const linker = new Linker((items) => Array.isArray(items) ? this.makeLinkUrl(`/${mappedModel}`) : this.makeLinkUrl(`/${mappedModel}/${this.getId(model, items)}`));
|
|
1085
|
+
linkers[modelLower] = linker;
|
|
1086
|
+
let projection = {};
|
|
1087
|
+
const modelDef = this.requireModel(model);
|
|
1088
|
+
for (const [field, fieldDef] of Object.entries(modelDef.fields)) {
|
|
1089
|
+
if (fieldDef.relation) {
|
|
1090
|
+
projection[field] = 0;
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
if (Object.keys(projection).length === 0) {
|
|
1094
|
+
projection = null;
|
|
1095
|
+
}
|
|
1096
|
+
const serializer = new Serializer(model, {
|
|
1097
|
+
version: "1.1",
|
|
1098
|
+
idKey: this.makeIdKey(ids),
|
|
1099
|
+
linkers: {
|
|
1100
|
+
resource: linker,
|
|
1101
|
+
document: linker
|
|
1102
|
+
},
|
|
1103
|
+
projection
|
|
1104
|
+
});
|
|
1105
|
+
this.serializers.set(modelLower, serializer);
|
|
1106
|
+
}
|
|
1107
|
+
for (const model of Object.keys(this.schema.models)) {
|
|
1108
|
+
const modelLower = lowerCaseFirst(model);
|
|
1109
|
+
const serializer = this.serializers.get(modelLower);
|
|
1110
|
+
if (!serializer) {
|
|
1111
|
+
continue;
|
|
1112
|
+
}
|
|
1113
|
+
const relators = {};
|
|
1114
|
+
const modelDef = this.requireModel(model);
|
|
1115
|
+
for (const [field, fieldDef] of Object.entries(modelDef.fields)) {
|
|
1116
|
+
if (!fieldDef.relation) {
|
|
1117
|
+
continue;
|
|
1118
|
+
}
|
|
1119
|
+
const fieldSerializer = this.serializers.get(lowerCaseFirst(fieldDef.type));
|
|
1120
|
+
if (!fieldSerializer) {
|
|
1121
|
+
continue;
|
|
1122
|
+
}
|
|
1123
|
+
const fieldIds = this.getIdFields(fieldDef.type);
|
|
1124
|
+
if (fieldIds.length > 0) {
|
|
1125
|
+
const mappedModel = this.mapModelName(modelLower);
|
|
1126
|
+
const relator = new Relator(async (data) => {
|
|
1127
|
+
return data[field];
|
|
1128
|
+
}, fieldSerializer, {
|
|
1129
|
+
relatedName: field,
|
|
1130
|
+
linkers: {
|
|
1131
|
+
related: new Linker((primary) => this.makeLinkUrl(`/${mappedModel}/${this.getId(model, primary)}/${field}`)),
|
|
1132
|
+
relationship: new Linker((primary) => this.makeLinkUrl(`/${mappedModel}/${this.getId(model, primary)}/relationships/${field}`))
|
|
1133
|
+
}
|
|
1134
|
+
});
|
|
1135
|
+
relators[field] = relator;
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
serializer.setRelators(relators);
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
getId(model, data) {
|
|
1142
|
+
if (!data) {
|
|
1143
|
+
return void 0;
|
|
1144
|
+
}
|
|
1145
|
+
const ids = this.getIdFields(model);
|
|
1146
|
+
if (ids.length === 0) {
|
|
1147
|
+
return void 0;
|
|
1148
|
+
} else {
|
|
1149
|
+
return data[this.makeIdKey(ids)];
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
async serializeItems(model, items, options) {
|
|
1153
|
+
model = lowerCaseFirst(model);
|
|
1154
|
+
const serializer = this.serializers.get(model);
|
|
1155
|
+
if (!serializer) {
|
|
1156
|
+
throw new Error(`serializer not found for model ${model}`);
|
|
1157
|
+
}
|
|
1158
|
+
const itemsWithId = clone(items);
|
|
1159
|
+
this.injectCompoundId(model, itemsWithId);
|
|
1160
|
+
const serialized = await serializer.serialize(itemsWithId, options);
|
|
1161
|
+
const plainResult = this.toPlainObject(serialized);
|
|
1162
|
+
const { json, meta } = SuperJSON2.serialize(plainResult);
|
|
1163
|
+
const result = json;
|
|
1164
|
+
if (meta) {
|
|
1165
|
+
result.meta = {
|
|
1166
|
+
...result.meta,
|
|
1167
|
+
serialization: meta
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
return result;
|
|
1171
|
+
}
|
|
1172
|
+
injectCompoundId(model, items) {
|
|
1173
|
+
const typeInfo = this.getModelInfo(model);
|
|
1174
|
+
if (!typeInfo) {
|
|
1175
|
+
return;
|
|
1176
|
+
}
|
|
1177
|
+
enumerate(items).forEach((item) => {
|
|
1178
|
+
if (!item) {
|
|
1179
|
+
return;
|
|
1180
|
+
}
|
|
1181
|
+
if (typeInfo.idFields.length > 1) {
|
|
1182
|
+
item[this.makeIdKey(typeInfo.idFields)] = this.makeCompoundId(typeInfo.idFields, item);
|
|
1183
|
+
}
|
|
1184
|
+
for (const [key, value] of Object.entries(item)) {
|
|
1185
|
+
if (typeInfo.relationships[key]) {
|
|
1186
|
+
this.injectCompoundId(typeInfo.relationships[key].type, value);
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
});
|
|
1190
|
+
}
|
|
1191
|
+
toPlainObject(data) {
|
|
1192
|
+
if (data === void 0 || data === null) {
|
|
1193
|
+
return data;
|
|
1194
|
+
}
|
|
1195
|
+
if (Array.isArray(data)) {
|
|
1196
|
+
return data.map((item) => this.toPlainObject(item));
|
|
1197
|
+
}
|
|
1198
|
+
if (typeof data === "object") {
|
|
1199
|
+
if (typeof data.toJSON === "function") {
|
|
1200
|
+
return data.toJSON();
|
|
1201
|
+
}
|
|
1202
|
+
const result = {};
|
|
1203
|
+
for (const [field, value] of Object.entries(data)) {
|
|
1204
|
+
if (value === void 0 || typeof value === "function") {
|
|
1205
|
+
continue;
|
|
1206
|
+
} else if (field === "attributes") {
|
|
1207
|
+
result[field] = value;
|
|
1208
|
+
} else {
|
|
1209
|
+
result[field] = this.toPlainObject(value);
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
return result;
|
|
1213
|
+
}
|
|
1214
|
+
return data;
|
|
1215
|
+
}
|
|
1216
|
+
replaceURLSearchParams(url, params) {
|
|
1217
|
+
const r = new URL(url);
|
|
1218
|
+
for (const [key, value] of Object.entries(params)) {
|
|
1219
|
+
r.searchParams.set(key, value.toString());
|
|
1220
|
+
}
|
|
1221
|
+
return r.toString();
|
|
1222
|
+
}
|
|
1223
|
+
makeIdFilter(idFields, resourceId, nested = true) {
|
|
1224
|
+
const decodedId = decodeURIComponent(resourceId);
|
|
1225
|
+
if (idFields.length === 1) {
|
|
1226
|
+
return {
|
|
1227
|
+
[idFields[0].name]: this.coerce(idFields[0], decodedId)
|
|
1228
|
+
};
|
|
1229
|
+
} else if (nested) {
|
|
1230
|
+
return {
|
|
1231
|
+
// TODO: support `@@id` with custom name
|
|
1232
|
+
[idFields.map((idf) => idf.name).join(DEFAULT_ID_DIVIDER)]: idFields.reduce((acc, curr, idx) => ({
|
|
1233
|
+
...acc,
|
|
1234
|
+
[curr.name]: this.coerce(curr, decodedId.split(this.idDivider)[idx])
|
|
1235
|
+
}), {})
|
|
1236
|
+
};
|
|
1237
|
+
} else {
|
|
1238
|
+
return idFields.reduce((acc, curr, idx) => ({
|
|
1239
|
+
...acc,
|
|
1240
|
+
[curr.name]: this.coerce(curr, decodedId.split(this.idDivider)[idx])
|
|
1241
|
+
}), {});
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
makeIdSelect(idFields) {
|
|
1245
|
+
if (idFields.length === 0) {
|
|
1246
|
+
throw this.errors["noId"];
|
|
1247
|
+
}
|
|
1248
|
+
return idFields.reduce((acc, curr) => ({
|
|
1249
|
+
...acc,
|
|
1250
|
+
[curr.name]: true
|
|
1251
|
+
}), {});
|
|
1252
|
+
}
|
|
1253
|
+
makeIdConnect(idFields, id) {
|
|
1254
|
+
if (idFields.length === 1) {
|
|
1255
|
+
return {
|
|
1256
|
+
[idFields[0].name]: this.coerce(idFields[0], id)
|
|
1257
|
+
};
|
|
1258
|
+
} else {
|
|
1259
|
+
return {
|
|
1260
|
+
[this.makeDefaultIdKey(idFields)]: idFields.reduce((acc, curr, idx) => ({
|
|
1261
|
+
...acc,
|
|
1262
|
+
[curr.name]: this.coerce(curr, `${id}`.split(this.idDivider)[idx])
|
|
1263
|
+
}), {})
|
|
1264
|
+
};
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
makeIdKey(idFields) {
|
|
1268
|
+
return idFields.map((idf) => idf.name).join(this.idDivider);
|
|
1269
|
+
}
|
|
1270
|
+
makeDefaultIdKey(idFields) {
|
|
1271
|
+
return idFields.map((idf) => idf.name).join(DEFAULT_ID_DIVIDER);
|
|
1272
|
+
}
|
|
1273
|
+
makeCompoundId(idFields, item) {
|
|
1274
|
+
return idFields.map((idf) => item[idf.name]).join(this.idDivider);
|
|
1275
|
+
}
|
|
1276
|
+
makeUpsertWhere(matchFields, attributes, typeInfo) {
|
|
1277
|
+
const where = matchFields.reduce((acc, field) => {
|
|
1278
|
+
acc[field] = attributes[field] ?? null;
|
|
1279
|
+
return acc;
|
|
1280
|
+
}, {});
|
|
1281
|
+
if (typeInfo.idFields.length > 1 && matchFields.some((mf) => typeInfo.idFields.map((idf) => idf.name).includes(mf))) {
|
|
1282
|
+
return {
|
|
1283
|
+
[this.makeDefaultIdKey(typeInfo.idFields)]: where
|
|
1284
|
+
};
|
|
1285
|
+
}
|
|
1286
|
+
return where;
|
|
1287
|
+
}
|
|
1288
|
+
includeRelationshipIds(model, args, mode) {
|
|
1289
|
+
const typeInfo = this.getModelInfo(model);
|
|
1290
|
+
if (!typeInfo) {
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
1293
|
+
for (const [relation, relationInfo] of Object.entries(typeInfo.relationships)) {
|
|
1294
|
+
args[mode] = {
|
|
1295
|
+
...args[mode],
|
|
1296
|
+
[relation]: {
|
|
1297
|
+
select: this.makeIdSelect(relationInfo.idFields)
|
|
1298
|
+
}
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
coerce(fieldDef, value) {
|
|
1303
|
+
if (typeof value === "string") {
|
|
1304
|
+
if (fieldDef.attributes?.some((attr) => attr.name === "@json")) {
|
|
1305
|
+
try {
|
|
1306
|
+
return JSON.parse(value);
|
|
1307
|
+
} catch {
|
|
1308
|
+
throw new InvalidValueError(`invalid JSON value: ${value}`);
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
const type = fieldDef.type;
|
|
1312
|
+
if (type === "Int") {
|
|
1313
|
+
const parsed = parseInt(value);
|
|
1314
|
+
if (isNaN(parsed)) {
|
|
1315
|
+
throw new InvalidValueError(`invalid ${type} value: ${value}`);
|
|
1316
|
+
}
|
|
1317
|
+
return parsed;
|
|
1318
|
+
} else if (type === "BigInt") {
|
|
1319
|
+
try {
|
|
1320
|
+
return BigInt(value);
|
|
1321
|
+
} catch {
|
|
1322
|
+
throw new InvalidValueError(`invalid ${type} value: ${value}`);
|
|
1323
|
+
}
|
|
1324
|
+
} else if (type === "Float") {
|
|
1325
|
+
const parsed = parseFloat(value);
|
|
1326
|
+
if (isNaN(parsed)) {
|
|
1327
|
+
throw new InvalidValueError(`invalid ${type} value: ${value}`);
|
|
1328
|
+
}
|
|
1329
|
+
return parsed;
|
|
1330
|
+
} else if (type === "Decimal") {
|
|
1331
|
+
try {
|
|
1332
|
+
return new Decimal2(value);
|
|
1333
|
+
} catch {
|
|
1334
|
+
throw new InvalidValueError(`invalid ${type} value: ${value}`);
|
|
1335
|
+
}
|
|
1336
|
+
} else if (type === "Boolean") {
|
|
1337
|
+
if (value === "true") {
|
|
1338
|
+
return true;
|
|
1339
|
+
} else if (value === "false") {
|
|
1340
|
+
return false;
|
|
1341
|
+
} else {
|
|
1342
|
+
throw new InvalidValueError(`invalid ${type} value: ${value}`);
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
return value;
|
|
1347
|
+
}
|
|
1348
|
+
makeNormalizedUrl(path, query) {
|
|
1349
|
+
const url = new URL(this.makeLinkUrl(path));
|
|
1350
|
+
for (const [key, value] of Object.entries(query ?? {})) {
|
|
1351
|
+
if (key.startsWith("filter[") || key.startsWith("sort[") || key === "include" || key.startsWith("include[") || key.startsWith("fields[")) {
|
|
1352
|
+
for (const v of enumerate(value)) {
|
|
1353
|
+
url.searchParams.append(key, v);
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
return url.toString();
|
|
1358
|
+
}
|
|
1359
|
+
getPagination(query) {
|
|
1360
|
+
if (!query) {
|
|
1361
|
+
return {
|
|
1362
|
+
offset: 0,
|
|
1363
|
+
limit: this.options.pageSize ?? DEFAULT_PAGE_SIZE
|
|
1364
|
+
};
|
|
1365
|
+
}
|
|
1366
|
+
let offset = 0;
|
|
1367
|
+
if (query["page[offset]"]) {
|
|
1368
|
+
const value = query["page[offset]"];
|
|
1369
|
+
const offsetText = Array.isArray(value) ? value[value.length - 1] : value;
|
|
1370
|
+
offset = parseInt(offsetText);
|
|
1371
|
+
if (isNaN(offset) || offset < 0) {
|
|
1372
|
+
offset = 0;
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
let pageSizeOption = this.options.pageSize ?? DEFAULT_PAGE_SIZE;
|
|
1376
|
+
if (pageSizeOption <= 0) {
|
|
1377
|
+
pageSizeOption = DEFAULT_PAGE_SIZE;
|
|
1378
|
+
}
|
|
1379
|
+
let limit = pageSizeOption;
|
|
1380
|
+
if (query["page[limit]"]) {
|
|
1381
|
+
const value = query["page[limit]"];
|
|
1382
|
+
const limitText = Array.isArray(value) ? value[value.length - 1] : value;
|
|
1383
|
+
limit = parseInt(limitText);
|
|
1384
|
+
if (isNaN(limit) || limit <= 0) {
|
|
1385
|
+
limit = pageSizeOption;
|
|
1386
|
+
}
|
|
1387
|
+
limit = Math.min(pageSizeOption, limit);
|
|
1388
|
+
}
|
|
1389
|
+
return {
|
|
1390
|
+
offset,
|
|
1391
|
+
limit
|
|
1392
|
+
};
|
|
1393
|
+
}
|
|
1394
|
+
buildFilter(type, query) {
|
|
1395
|
+
if (!query) {
|
|
1396
|
+
return {
|
|
1397
|
+
filter: void 0,
|
|
1398
|
+
error: void 0
|
|
1399
|
+
};
|
|
1400
|
+
}
|
|
1401
|
+
const typeInfo = this.getModelInfo(type);
|
|
1402
|
+
if (!typeInfo) {
|
|
1403
|
+
return {
|
|
1404
|
+
filter: void 0,
|
|
1405
|
+
error: this.makeUnsupportedModelError(type)
|
|
1406
|
+
};
|
|
1407
|
+
}
|
|
1408
|
+
const items = [];
|
|
1409
|
+
for (const [key, value] of Object.entries(query)) {
|
|
1410
|
+
if (!value) {
|
|
1411
|
+
continue;
|
|
1412
|
+
}
|
|
1413
|
+
const match2 = key.match(this.filterParamPattern);
|
|
1414
|
+
if (!match2 || !match2.groups || !match2.groups["match"]) {
|
|
1415
|
+
continue;
|
|
1416
|
+
}
|
|
1417
|
+
const filterKeys = match2.groups["match"].replaceAll(/[[\]]/g, " ").split(" ").filter((i) => i);
|
|
1418
|
+
if (!filterKeys.length) {
|
|
1419
|
+
continue;
|
|
1420
|
+
}
|
|
1421
|
+
const item = {};
|
|
1422
|
+
let curr = item;
|
|
1423
|
+
let currType = typeInfo;
|
|
1424
|
+
for (const filterValue of enumerate(value)) {
|
|
1425
|
+
for (let i = 0; i < filterKeys.length; i++) {
|
|
1426
|
+
let filterKey = filterKeys[i];
|
|
1427
|
+
let filterOp;
|
|
1428
|
+
const pos = filterKey.indexOf("$");
|
|
1429
|
+
if (pos > 0) {
|
|
1430
|
+
filterOp = filterKey.substring(pos + 1);
|
|
1431
|
+
filterKey = filterKey.substring(0, pos);
|
|
1432
|
+
}
|
|
1433
|
+
if (!!filterOp && !FilterOperations.includes(filterOp)) {
|
|
1434
|
+
return {
|
|
1435
|
+
filter: void 0,
|
|
1436
|
+
error: this.makeError("invalidFilter", `invalid filter operation: ${filterOp}`)
|
|
1437
|
+
};
|
|
1438
|
+
}
|
|
1439
|
+
const idFields = this.getIdFields(currType.name);
|
|
1440
|
+
const fieldDef = filterKey === "id" ? Object.values(currType.fields).find((f) => idFields.some((idf) => idf.name === f.name)) : currType.fields[filterKey];
|
|
1441
|
+
if (!fieldDef) {
|
|
1442
|
+
return {
|
|
1443
|
+
filter: void 0,
|
|
1444
|
+
error: this.makeError("invalidFilter")
|
|
1445
|
+
};
|
|
1446
|
+
}
|
|
1447
|
+
if (!fieldDef.relation) {
|
|
1448
|
+
if (i !== filterKeys.length - 1) {
|
|
1449
|
+
return {
|
|
1450
|
+
filter: void 0,
|
|
1451
|
+
error: this.makeError("invalidFilter")
|
|
1452
|
+
};
|
|
1453
|
+
}
|
|
1454
|
+
curr[fieldDef.name] = this.makeFilterValue(fieldDef, filterValue, filterOp);
|
|
1455
|
+
} else {
|
|
1456
|
+
if (i === filterKeys.length - 1) {
|
|
1457
|
+
curr[fieldDef.name] = this.makeFilterValue(fieldDef, filterValue, filterOp);
|
|
1458
|
+
} else {
|
|
1459
|
+
if (fieldDef.array) {
|
|
1460
|
+
curr[fieldDef.name] = {
|
|
1461
|
+
some: {}
|
|
1462
|
+
};
|
|
1463
|
+
curr = curr[fieldDef.name].some;
|
|
1464
|
+
} else {
|
|
1465
|
+
curr = curr[fieldDef.name] = {};
|
|
1466
|
+
}
|
|
1467
|
+
currType = this.getModelInfo(fieldDef.type);
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
items.push(item);
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
if (items.length === 0) {
|
|
1475
|
+
return {
|
|
1476
|
+
filter: void 0,
|
|
1477
|
+
error: void 0
|
|
1478
|
+
};
|
|
1479
|
+
} else {
|
|
1480
|
+
return {
|
|
1481
|
+
filter: items.length === 1 ? items[0] : {
|
|
1482
|
+
AND: items
|
|
1483
|
+
},
|
|
1484
|
+
error: void 0
|
|
1485
|
+
};
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
buildSort(type, query) {
|
|
1489
|
+
if (!query?.["sort"]) {
|
|
1490
|
+
return {
|
|
1491
|
+
sort: void 0,
|
|
1492
|
+
error: void 0
|
|
1493
|
+
};
|
|
1494
|
+
}
|
|
1495
|
+
const typeInfo = this.getModelInfo(type);
|
|
1496
|
+
if (!typeInfo) {
|
|
1497
|
+
return {
|
|
1498
|
+
sort: void 0,
|
|
1499
|
+
error: this.makeUnsupportedModelError(type)
|
|
1500
|
+
};
|
|
1501
|
+
}
|
|
1502
|
+
const result = [];
|
|
1503
|
+
for (const sortSpec of enumerate(query["sort"])) {
|
|
1504
|
+
const sortFields = sortSpec.split(",").filter((i) => i);
|
|
1505
|
+
for (const sortField of sortFields) {
|
|
1506
|
+
const dir = sortField.startsWith("-") ? "desc" : "asc";
|
|
1507
|
+
const cleanedSortField = sortField.startsWith("-") ? sortField.substring(1) : sortField;
|
|
1508
|
+
const parts = cleanedSortField.split(".").filter((i) => i);
|
|
1509
|
+
const sortItem = {};
|
|
1510
|
+
let curr = sortItem;
|
|
1511
|
+
let currType = typeInfo;
|
|
1512
|
+
for (let i = 0; i < parts.length; i++) {
|
|
1513
|
+
const part = parts[i];
|
|
1514
|
+
const fieldInfo = currType.fields[part];
|
|
1515
|
+
if (!fieldInfo || fieldInfo.array) {
|
|
1516
|
+
return {
|
|
1517
|
+
sort: void 0,
|
|
1518
|
+
error: this.makeError("invalidSort", "sorting by array field is not supported")
|
|
1519
|
+
};
|
|
1520
|
+
}
|
|
1521
|
+
if (i === parts.length - 1) {
|
|
1522
|
+
if (fieldInfo.relation) {
|
|
1523
|
+
const relationType = this.getModelInfo(fieldInfo.type);
|
|
1524
|
+
if (!relationType) {
|
|
1525
|
+
return {
|
|
1526
|
+
sort: void 0,
|
|
1527
|
+
error: this.makeUnsupportedModelError(fieldInfo.type)
|
|
1528
|
+
};
|
|
1529
|
+
}
|
|
1530
|
+
curr[fieldInfo.name] = relationType.idFields.reduce((acc, idField) => {
|
|
1531
|
+
acc[idField.name] = dir;
|
|
1532
|
+
return acc;
|
|
1533
|
+
}, {});
|
|
1534
|
+
} else {
|
|
1535
|
+
curr[fieldInfo.name] = dir;
|
|
1536
|
+
}
|
|
1537
|
+
} else {
|
|
1538
|
+
if (!fieldInfo.relation) {
|
|
1539
|
+
return {
|
|
1540
|
+
sort: void 0,
|
|
1541
|
+
error: this.makeError("invalidSort", "intermediate sort segments must be relationships")
|
|
1542
|
+
};
|
|
1543
|
+
}
|
|
1544
|
+
curr = curr[fieldInfo.name] = {};
|
|
1545
|
+
currType = this.getModelInfo(fieldInfo.type);
|
|
1546
|
+
if (!currType) {
|
|
1547
|
+
return {
|
|
1548
|
+
sort: void 0,
|
|
1549
|
+
error: this.makeUnsupportedModelError(fieldInfo.type)
|
|
1550
|
+
};
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
result.push(sortItem);
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
return {
|
|
1558
|
+
sort: result,
|
|
1559
|
+
error: void 0
|
|
1560
|
+
};
|
|
1561
|
+
}
|
|
1562
|
+
buildRelationSelect(type, include, query) {
|
|
1563
|
+
const typeInfo = this.getModelInfo(type);
|
|
1564
|
+
if (!typeInfo) {
|
|
1565
|
+
return {
|
|
1566
|
+
select: void 0,
|
|
1567
|
+
error: this.makeUnsupportedModelError(type)
|
|
1568
|
+
};
|
|
1569
|
+
}
|
|
1570
|
+
const result = {};
|
|
1571
|
+
const allIncludes = [];
|
|
1572
|
+
for (const includeItem of enumerate(include)) {
|
|
1573
|
+
const inclusions = includeItem.split(",").filter((i) => i);
|
|
1574
|
+
for (const inclusion of inclusions) {
|
|
1575
|
+
allIncludes.push(inclusion);
|
|
1576
|
+
const parts = inclusion.split(".");
|
|
1577
|
+
let currPayload = result;
|
|
1578
|
+
let currType = typeInfo;
|
|
1579
|
+
for (let i = 0; i < parts.length; i++) {
|
|
1580
|
+
const relation = parts[i];
|
|
1581
|
+
const relationInfo = currType.relationships[relation];
|
|
1582
|
+
if (!relationInfo) {
|
|
1583
|
+
return {
|
|
1584
|
+
select: void 0,
|
|
1585
|
+
error: this.makeUnsupportedRelationshipError(type, relation, 400)
|
|
1586
|
+
};
|
|
1587
|
+
}
|
|
1588
|
+
currType = this.getModelInfo(relationInfo.type);
|
|
1589
|
+
if (!currType) {
|
|
1590
|
+
return {
|
|
1591
|
+
select: void 0,
|
|
1592
|
+
error: this.makeUnsupportedModelError(relationInfo.type)
|
|
1593
|
+
};
|
|
1594
|
+
}
|
|
1595
|
+
const { select, error } = this.buildPartialSelect(lowerCaseFirst(relationInfo.type), query);
|
|
1596
|
+
if (error) return {
|
|
1597
|
+
select: void 0,
|
|
1598
|
+
error
|
|
1599
|
+
};
|
|
1600
|
+
if (i !== parts.length - 1) {
|
|
1601
|
+
if (select) {
|
|
1602
|
+
currPayload[relation] = {
|
|
1603
|
+
select: {
|
|
1604
|
+
...select
|
|
1605
|
+
}
|
|
1606
|
+
};
|
|
1607
|
+
currPayload = currPayload[relation].select;
|
|
1608
|
+
} else {
|
|
1609
|
+
currPayload[relation] = {
|
|
1610
|
+
include: {
|
|
1611
|
+
...currPayload[relation]?.include
|
|
1612
|
+
}
|
|
1613
|
+
};
|
|
1614
|
+
currPayload = currPayload[relation].include;
|
|
1615
|
+
}
|
|
1616
|
+
} else {
|
|
1617
|
+
currPayload[relation] = select ? {
|
|
1618
|
+
select: {
|
|
1619
|
+
...select
|
|
1620
|
+
}
|
|
1621
|
+
} : true;
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
return {
|
|
1627
|
+
select: result,
|
|
1628
|
+
error: void 0,
|
|
1629
|
+
allIncludes
|
|
1630
|
+
};
|
|
1631
|
+
}
|
|
1632
|
+
makeFilterValue(fieldDef, value, op) {
|
|
1633
|
+
if (fieldDef.relation) {
|
|
1634
|
+
const info = this.getModelInfo(fieldDef.type);
|
|
1635
|
+
if (fieldDef.array) {
|
|
1636
|
+
const values = value.split(",").filter((i) => i);
|
|
1637
|
+
const filterValue = values.length > 1 ? {
|
|
1638
|
+
OR: values.map((v) => this.makeIdFilter(info.idFields, v, false))
|
|
1639
|
+
} : this.makeIdFilter(info.idFields, value, false);
|
|
1640
|
+
return {
|
|
1641
|
+
some: filterValue
|
|
1642
|
+
};
|
|
1643
|
+
} else {
|
|
1644
|
+
const values = value.split(",").filter((i) => i);
|
|
1645
|
+
if (values.length > 1) {
|
|
1646
|
+
return {
|
|
1647
|
+
OR: values.map((v) => this.makeIdFilter(info.idFields, v, false))
|
|
1648
|
+
};
|
|
1649
|
+
} else {
|
|
1650
|
+
return {
|
|
1651
|
+
is: this.makeIdFilter(info.idFields, value, false)
|
|
1652
|
+
};
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
} else {
|
|
1656
|
+
const coerced = this.coerce(fieldDef, value);
|
|
1657
|
+
switch (op) {
|
|
1658
|
+
case "icontains":
|
|
1659
|
+
return {
|
|
1660
|
+
contains: coerced,
|
|
1661
|
+
mode: "insensitive"
|
|
1662
|
+
};
|
|
1663
|
+
case "hasSome":
|
|
1664
|
+
case "hasEvery": {
|
|
1665
|
+
const values = value.split(",").filter((i) => i).map((v) => this.coerce(fieldDef, v));
|
|
1666
|
+
return {
|
|
1667
|
+
[op]: values
|
|
1668
|
+
};
|
|
1669
|
+
}
|
|
1670
|
+
case "isEmpty":
|
|
1671
|
+
if (value !== "true" && value !== "false") {
|
|
1672
|
+
throw new InvalidValueError(`Not a boolean: ${value}`);
|
|
1673
|
+
}
|
|
1674
|
+
return {
|
|
1675
|
+
isEmpty: value === "true" ? true : false
|
|
1676
|
+
};
|
|
1677
|
+
default:
|
|
1678
|
+
if (op === void 0) {
|
|
1679
|
+
if (fieldDef.attributes?.some((attr) => attr.name === "@json")) {
|
|
1680
|
+
return {
|
|
1681
|
+
equals: coerced
|
|
1682
|
+
};
|
|
1683
|
+
}
|
|
1684
|
+
const values = value.split(",").filter((i) => i).map((v) => this.coerce(fieldDef, v));
|
|
1685
|
+
return values.length > 1 ? {
|
|
1686
|
+
in: values
|
|
1687
|
+
} : {
|
|
1688
|
+
equals: values[0]
|
|
1689
|
+
};
|
|
1690
|
+
} else {
|
|
1691
|
+
return {
|
|
1692
|
+
[op]: coerced
|
|
1693
|
+
};
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
injectRelationQuery(type, injectTarget, injectKey, query) {
|
|
1699
|
+
const { filter, error: filterError } = this.buildFilter(type, query);
|
|
1700
|
+
if (filterError) {
|
|
1701
|
+
return filterError;
|
|
1702
|
+
}
|
|
1703
|
+
if (filter) {
|
|
1704
|
+
injectTarget[injectKey] = {
|
|
1705
|
+
...injectTarget[injectKey],
|
|
1706
|
+
where: filter
|
|
1707
|
+
};
|
|
1708
|
+
}
|
|
1709
|
+
const { sort, error: sortError } = this.buildSort(type, query);
|
|
1710
|
+
if (sortError) {
|
|
1711
|
+
return sortError;
|
|
1712
|
+
}
|
|
1713
|
+
if (sort) {
|
|
1714
|
+
injectTarget[injectKey] = {
|
|
1715
|
+
...injectTarget[injectKey],
|
|
1716
|
+
orderBy: sort
|
|
1717
|
+
};
|
|
1718
|
+
}
|
|
1719
|
+
const pagination = this.getPagination(query);
|
|
1720
|
+
const offset = pagination.offset;
|
|
1721
|
+
if (offset > 0) {
|
|
1722
|
+
injectTarget[injectKey] = {
|
|
1723
|
+
...injectTarget[injectKey],
|
|
1724
|
+
skip: offset
|
|
1725
|
+
};
|
|
1726
|
+
}
|
|
1727
|
+
const limit = pagination.limit;
|
|
1728
|
+
if (limit !== Infinity) {
|
|
1729
|
+
injectTarget[injectKey] = {
|
|
1730
|
+
...injectTarget[injectKey],
|
|
1731
|
+
take: limit
|
|
1732
|
+
};
|
|
1733
|
+
injectTarget._count = {
|
|
1734
|
+
select: {
|
|
1735
|
+
[injectKey]: true
|
|
1736
|
+
}
|
|
1737
|
+
};
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
handleZenStackError(err) {
|
|
1741
|
+
if (err instanceof InputValidationError) {
|
|
1742
|
+
return this.makeError("validationError", err.message, 422, err.cause instanceof Error ? err.cause.message : void 0);
|
|
1743
|
+
} else if (err instanceof RejectedByPolicyError) {
|
|
1744
|
+
return this.makeError("forbidden", err.message, 403, err.reason);
|
|
1745
|
+
} else if (err instanceof NotFoundError) {
|
|
1746
|
+
return this.makeError("notFound", err.message);
|
|
1747
|
+
} else if (err instanceof QueryError) {
|
|
1748
|
+
return this.makeError("queryError", err.message, 400, err.cause instanceof Error ? err.cause.message : void 0);
|
|
1749
|
+
} else {
|
|
1750
|
+
return this.makeError("unknownError", err.message);
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
makeError(code, detail, status, reason) {
|
|
1754
|
+
status = status ?? this.errors[code]?.status ?? 500;
|
|
1755
|
+
const error = {
|
|
1756
|
+
status,
|
|
1757
|
+
code: paramCase(code),
|
|
1758
|
+
title: this.errors[code]?.title
|
|
1759
|
+
};
|
|
1760
|
+
if (detail) {
|
|
1761
|
+
error.detail = detail;
|
|
1762
|
+
}
|
|
1763
|
+
if (reason) {
|
|
1764
|
+
error.reason = reason;
|
|
1765
|
+
}
|
|
1766
|
+
return {
|
|
1767
|
+
status,
|
|
1768
|
+
body: {
|
|
1769
|
+
errors: [
|
|
1770
|
+
error
|
|
1771
|
+
]
|
|
1772
|
+
}
|
|
1773
|
+
};
|
|
1774
|
+
}
|
|
1775
|
+
makeUnsupportedModelError(model) {
|
|
1776
|
+
return this.makeError("unsupportedModel", `Model ${model} doesn't exist`);
|
|
1777
|
+
}
|
|
1778
|
+
makeUnsupportedRelationshipError(model, relationship, status) {
|
|
1779
|
+
return this.makeError("unsupportedRelationship", `Relationship ${model}.${relationship} doesn't exist`, status);
|
|
1780
|
+
}
|
|
1781
|
+
};
|
|
42
1782
|
|
|
43
1783
|
// src/api/rpc/index.ts
|
|
1784
|
+
import { lowerCaseFirst as lowerCaseFirst2, safeJSONStringify } from "@zenstackhq/common-helpers";
|
|
1785
|
+
import { InputValidationError as InputValidationError2, NotFoundError as NotFoundError2, RejectedByPolicyError as RejectedByPolicyError2, ZenStackError as ZenStackError2 } from "@zenstackhq/orm";
|
|
1786
|
+
import SuperJSON3 from "superjson";
|
|
44
1787
|
registerCustomSerializers();
|
|
45
1788
|
var RPCApiHandler = class {
|
|
46
1789
|
static {
|
|
@@ -53,6 +1796,9 @@ var RPCApiHandler = class {
|
|
|
53
1796
|
get schema() {
|
|
54
1797
|
return this.options.schema;
|
|
55
1798
|
}
|
|
1799
|
+
get log() {
|
|
1800
|
+
return this.options.log;
|
|
1801
|
+
}
|
|
56
1802
|
async handleRequest({ client, method, path, query, requestBody }) {
|
|
57
1803
|
const parts = path.split("/").filter((p) => !!p);
|
|
58
1804
|
const op = parts.pop();
|
|
@@ -60,7 +1806,7 @@ var RPCApiHandler = class {
|
|
|
60
1806
|
if (parts.length !== 0 || !op || !model) {
|
|
61
1807
|
return this.makeBadInputErrorResponse("invalid request path");
|
|
62
1808
|
}
|
|
63
|
-
model =
|
|
1809
|
+
model = lowerCaseFirst2(model);
|
|
64
1810
|
method = method.toUpperCase();
|
|
65
1811
|
let args;
|
|
66
1812
|
let resCode = 200;
|
|
@@ -132,7 +1878,7 @@ var RPCApiHandler = class {
|
|
|
132
1878
|
data: clientResult
|
|
133
1879
|
};
|
|
134
1880
|
if (clientResult) {
|
|
135
|
-
const { json, meta } =
|
|
1881
|
+
const { json, meta } = SuperJSON3.serialize(clientResult);
|
|
136
1882
|
responseBody = {
|
|
137
1883
|
data: json
|
|
138
1884
|
};
|
|
@@ -150,7 +1896,7 @@ var RPCApiHandler = class {
|
|
|
150
1896
|
return response;
|
|
151
1897
|
} catch (err) {
|
|
152
1898
|
log(this.options.log, "error", `error occurred when handling "${model}.${op}" request`, err);
|
|
153
|
-
if (err instanceof
|
|
1899
|
+
if (err instanceof ZenStackError2) {
|
|
154
1900
|
return this.makeZenStackErrorResponse(err);
|
|
155
1901
|
} else {
|
|
156
1902
|
return this.makeGenericErrorResponse(err);
|
|
@@ -158,7 +1904,7 @@ var RPCApiHandler = class {
|
|
|
158
1904
|
}
|
|
159
1905
|
}
|
|
160
1906
|
isValidModel(client, model) {
|
|
161
|
-
return Object.keys(client.$schema.models).some((m) =>
|
|
1907
|
+
return Object.keys(client.$schema.models).some((m) => lowerCaseFirst2(m) === lowerCaseFirst2(model));
|
|
162
1908
|
}
|
|
163
1909
|
makeBadInputErrorResponse(message) {
|
|
164
1910
|
const resp = {
|
|
@@ -177,11 +1923,11 @@ var RPCApiHandler = class {
|
|
|
177
1923
|
status: 500,
|
|
178
1924
|
body: {
|
|
179
1925
|
error: {
|
|
180
|
-
message: err.message
|
|
1926
|
+
message: err instanceof Error ? err.message : "unknown error"
|
|
181
1927
|
}
|
|
182
1928
|
}
|
|
183
1929
|
};
|
|
184
|
-
log(this.options.log, "debug", () => `sending error response: ${safeJSONStringify(resp)}`);
|
|
1930
|
+
log(this.options.log, "debug", () => `sending error response: ${safeJSONStringify(resp)}${err instanceof Error ? "\n" + err.stack : ""}`);
|
|
185
1931
|
return resp;
|
|
186
1932
|
}
|
|
187
1933
|
makeZenStackErrorResponse(err) {
|
|
@@ -192,14 +1938,14 @@ var RPCApiHandler = class {
|
|
|
192
1938
|
if (err.cause && err.cause instanceof Error) {
|
|
193
1939
|
error.cause = err.cause.message;
|
|
194
1940
|
}
|
|
195
|
-
if (err instanceof
|
|
1941
|
+
if (err instanceof NotFoundError2) {
|
|
196
1942
|
status = 404;
|
|
197
1943
|
error.model = err.model;
|
|
198
|
-
} else if (err instanceof
|
|
1944
|
+
} else if (err instanceof InputValidationError2) {
|
|
199
1945
|
status = 422;
|
|
200
1946
|
error.rejectedByValidation = true;
|
|
201
1947
|
error.model = err.model;
|
|
202
|
-
} else if (err instanceof
|
|
1948
|
+
} else if (err instanceof RejectedByPolicyError2) {
|
|
203
1949
|
status = 403;
|
|
204
1950
|
error.rejectedByPolicy = true;
|
|
205
1951
|
error.rejectReason = err.reason;
|
|
@@ -218,7 +1964,7 @@ var RPCApiHandler = class {
|
|
|
218
1964
|
const { meta, ...rest } = args;
|
|
219
1965
|
if (meta?.serialization) {
|
|
220
1966
|
try {
|
|
221
|
-
args =
|
|
1967
|
+
args = SuperJSON3.deserialize({
|
|
222
1968
|
json: rest,
|
|
223
1969
|
meta: meta.serialization
|
|
224
1970
|
});
|
|
@@ -249,7 +1995,7 @@ var RPCApiHandler = class {
|
|
|
249
1995
|
throw new Error('invalid "meta" query parameter');
|
|
250
1996
|
}
|
|
251
1997
|
if (parsedMeta.serialization) {
|
|
252
|
-
return
|
|
1998
|
+
return SuperJSON3.deserialize({
|
|
253
1999
|
json: parsedValue,
|
|
254
2000
|
meta: parsedMeta.serialization
|
|
255
2001
|
});
|
|
@@ -259,6 +2005,7 @@ var RPCApiHandler = class {
|
|
|
259
2005
|
}
|
|
260
2006
|
};
|
|
261
2007
|
export {
|
|
262
|
-
RPCApiHandler
|
|
2008
|
+
RPCApiHandler,
|
|
2009
|
+
RestApiHandler
|
|
263
2010
|
};
|
|
264
2011
|
//# sourceMappingURL=api.js.map
|