@datrix/api 0.1.0
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 +21 -0
- package/README.md +233 -0
- package/dist/index.d.mts +305 -0
- package/dist/index.d.ts +305 -0
- package/dist/index.js +3354 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +3314 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +59 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3354 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
ApiPlugin: () => ApiPlugin,
|
|
24
|
+
ContextBuildError: () => ContextBuildError,
|
|
25
|
+
DatrixApiError: () => DatrixApiError,
|
|
26
|
+
MemorySessionStore: () => MemorySessionStore,
|
|
27
|
+
authenticate: () => authenticate,
|
|
28
|
+
buildRequestContext: () => buildRequestContext,
|
|
29
|
+
checkFieldsForWrite: () => checkFieldsForWrite,
|
|
30
|
+
checkSchemaPermission: () => checkSchemaPermission,
|
|
31
|
+
createAuthHandlers: () => createAuthHandlers,
|
|
32
|
+
createUnifiedAuthHandler: () => createUnifiedAuthHandler,
|
|
33
|
+
datrixErrorResponse: () => datrixErrorResponse,
|
|
34
|
+
evaluatePermissionValue: () => evaluatePermissionValue,
|
|
35
|
+
filterFieldsForRead: () => filterFieldsForRead,
|
|
36
|
+
filterRecordsForRead: () => filterRecordsForRead,
|
|
37
|
+
handleCrudRequest: () => handleCrudRequest,
|
|
38
|
+
handleRequest: () => handleRequest,
|
|
39
|
+
handlerError: () => handlerError,
|
|
40
|
+
jsonResponse: () => jsonResponse,
|
|
41
|
+
methodToAction: () => methodToAction,
|
|
42
|
+
parseFields: () => parseFields,
|
|
43
|
+
parsePopulate: () => parsePopulate,
|
|
44
|
+
parseQuery: () => parseQuery,
|
|
45
|
+
parseWhere: () => parseWhere,
|
|
46
|
+
queryToParams: () => queryToParams,
|
|
47
|
+
serializeQuery: () => serializeQuery
|
|
48
|
+
});
|
|
49
|
+
module.exports = __toCommonJS(index_exports);
|
|
50
|
+
|
|
51
|
+
// src/api.ts
|
|
52
|
+
var import_core18 = require("@datrix/core");
|
|
53
|
+
var import_core19 = require("@datrix/core");
|
|
54
|
+
var import_core20 = require("@datrix/core");
|
|
55
|
+
|
|
56
|
+
// src/auth/password.ts
|
|
57
|
+
var import_node_crypto = require("crypto");
|
|
58
|
+
|
|
59
|
+
// src/auth/error-helper.ts
|
|
60
|
+
var import_core = require("@datrix/core");
|
|
61
|
+
function throwJwtSignError(cause) {
|
|
62
|
+
throw new import_core.DatrixAuthError("Failed to sign JWT token", {
|
|
63
|
+
code: "JWT_SIGN_ERROR",
|
|
64
|
+
strategy: "jwt",
|
|
65
|
+
cause,
|
|
66
|
+
suggestion: "Check your JWT configuration and secret key"
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
function throwJwtVerifyError(cause) {
|
|
70
|
+
throw new import_core.DatrixAuthError("Failed to verify JWT token", {
|
|
71
|
+
code: "JWT_VERIFY_ERROR",
|
|
72
|
+
strategy: "jwt",
|
|
73
|
+
cause,
|
|
74
|
+
suggestion: "Ensure the token is valid and not tampered with"
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
function throwJwtDecodeError(cause) {
|
|
78
|
+
throw new import_core.DatrixAuthError("Failed to decode JWT token", {
|
|
79
|
+
code: "JWT_DECODE_ERROR",
|
|
80
|
+
strategy: "jwt",
|
|
81
|
+
cause,
|
|
82
|
+
suggestion: "Ensure the token format is correct"
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
function throwJwtInvalidFormat() {
|
|
86
|
+
throw new import_core.DatrixAuthError("Invalid JWT format", {
|
|
87
|
+
code: "JWT_INVALID_FORMAT",
|
|
88
|
+
strategy: "jwt",
|
|
89
|
+
suggestion: "JWT must be in format: header.payload.signature",
|
|
90
|
+
expected: "three dot-separated base64url strings"
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
function throwJwtInvalidHeader() {
|
|
94
|
+
throw new import_core.DatrixAuthError("Invalid JWT header", {
|
|
95
|
+
code: "JWT_INVALID_HEADER",
|
|
96
|
+
strategy: "jwt",
|
|
97
|
+
suggestion: "Ensure the JWT header has correct algorithm and type",
|
|
98
|
+
expected: 'header with typ: "JWT" and matching algorithm'
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
function throwJwtInvalidPayload() {
|
|
102
|
+
throw new import_core.DatrixAuthError("Invalid JWT payload", {
|
|
103
|
+
code: "JWT_INVALID_PAYLOAD",
|
|
104
|
+
strategy: "jwt",
|
|
105
|
+
suggestion: "JWT payload must contain userId, role, iat, and exp fields",
|
|
106
|
+
expected: "valid JWT payload with required fields"
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
function throwJwtInvalidSignature() {
|
|
110
|
+
throw new import_core.DatrixAuthError("Invalid JWT signature", {
|
|
111
|
+
code: "JWT_INVALID_SIGNATURE",
|
|
112
|
+
strategy: "jwt",
|
|
113
|
+
suggestion: "Token may have been tampered with or signed with wrong secret",
|
|
114
|
+
expected: "valid HMAC signature"
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
function throwJwtExpired(exp, now) {
|
|
118
|
+
throw new import_core.DatrixAuthError("JWT token expired", {
|
|
119
|
+
code: "JWT_EXPIRED",
|
|
120
|
+
strategy: "jwt",
|
|
121
|
+
context: { exp, now },
|
|
122
|
+
suggestion: "Refresh your token or login again",
|
|
123
|
+
expected: "token with exp > current time"
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
function throwJwtInvalidIat() {
|
|
127
|
+
throw new import_core.DatrixAuthError("JWT token issued in the future", {
|
|
128
|
+
code: "JWT_INVALID_IAT",
|
|
129
|
+
strategy: "jwt",
|
|
130
|
+
suggestion: "Check your server time synchronization",
|
|
131
|
+
expected: "token with iat <= current time (allowing 60s skew)"
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
function throwJwtInvalidIssuer(expected, received) {
|
|
135
|
+
throw new import_core.DatrixAuthError("JWT issuer mismatch", {
|
|
136
|
+
code: "JWT_INVALID_ISSUER",
|
|
137
|
+
strategy: "jwt",
|
|
138
|
+
expected,
|
|
139
|
+
received,
|
|
140
|
+
suggestion: `Token must be issued by: ${expected}`
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
function throwJwtInvalidAudience(expected, received) {
|
|
144
|
+
throw new import_core.DatrixAuthError("JWT audience mismatch", {
|
|
145
|
+
code: "JWT_INVALID_AUDIENCE",
|
|
146
|
+
strategy: "jwt",
|
|
147
|
+
expected,
|
|
148
|
+
received,
|
|
149
|
+
suggestion: `Token must be for audience: ${expected}`
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
function throwSessionCreateError(cause) {
|
|
153
|
+
throw new import_core.DatrixAuthError("Failed to create session", {
|
|
154
|
+
code: "SESSION_CREATE_ERROR",
|
|
155
|
+
strategy: "session",
|
|
156
|
+
cause,
|
|
157
|
+
suggestion: "Check your session store configuration"
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
function throwSessionNotFound(sessionId) {
|
|
161
|
+
throw new import_core.DatrixAuthError("Session not found", {
|
|
162
|
+
code: "AUTH_SESSION_NOT_FOUND",
|
|
163
|
+
strategy: "session",
|
|
164
|
+
context: { sessionId },
|
|
165
|
+
suggestion: "Login again to create a new session"
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
function throwSessionExpired(sessionId) {
|
|
169
|
+
throw new import_core.DatrixAuthError("Session expired", {
|
|
170
|
+
code: "AUTH_SESSION_EXPIRED",
|
|
171
|
+
strategy: "session",
|
|
172
|
+
context: { sessionId },
|
|
173
|
+
suggestion: "Login again to create a new session"
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
function throwSessionNotConfigured() {
|
|
177
|
+
throw new import_core.DatrixAuthError("Session strategy not configured", {
|
|
178
|
+
code: "SESSION_NOT_CONFIGURED",
|
|
179
|
+
strategy: "session",
|
|
180
|
+
suggestion: "Add session configuration to your auth config",
|
|
181
|
+
expected: "AuthConfig.session to be defined"
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
function throwPasswordTooShort(minLength, actualLength) {
|
|
185
|
+
throw new import_core.DatrixAuthError(
|
|
186
|
+
`Password must be at least ${minLength} characters`,
|
|
187
|
+
{
|
|
188
|
+
code: "PASSWORD_TOO_SHORT",
|
|
189
|
+
strategy: "password",
|
|
190
|
+
context: { minLength, actualLength },
|
|
191
|
+
suggestion: `Use a password with at least ${minLength} characters`,
|
|
192
|
+
expected: `password length >= ${minLength}`,
|
|
193
|
+
received: actualLength
|
|
194
|
+
}
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
function throwPasswordHashError(cause) {
|
|
198
|
+
throw new import_core.DatrixAuthError("Failed to hash password", {
|
|
199
|
+
code: "PASSWORD_HASH_ERROR",
|
|
200
|
+
strategy: "password",
|
|
201
|
+
cause,
|
|
202
|
+
suggestion: "Check your password hashing configuration"
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
function throwPasswordVerifyError(cause) {
|
|
206
|
+
throw new import_core.DatrixAuthError("Failed to verify password", {
|
|
207
|
+
code: "PASSWORD_VERIFY_ERROR",
|
|
208
|
+
strategy: "password",
|
|
209
|
+
cause,
|
|
210
|
+
suggestion: "Ensure the password hash and salt are valid"
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// src/auth/password.ts
|
|
215
|
+
var DEFAULT_CONFIG = {
|
|
216
|
+
iterations: 1e5,
|
|
217
|
+
keyLength: 64,
|
|
218
|
+
minLength: 8
|
|
219
|
+
};
|
|
220
|
+
var PasswordManager = class {
|
|
221
|
+
iterations;
|
|
222
|
+
keyLength;
|
|
223
|
+
minLength;
|
|
224
|
+
constructor(config = {}) {
|
|
225
|
+
this.iterations = config.iterations ?? DEFAULT_CONFIG.iterations;
|
|
226
|
+
this.keyLength = config.keyLength ?? DEFAULT_CONFIG.keyLength;
|
|
227
|
+
this.minLength = config.minLength ?? DEFAULT_CONFIG.minLength;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Hash password using PBKDF2
|
|
231
|
+
*/
|
|
232
|
+
async hash(password) {
|
|
233
|
+
if (!password || password.length < this.minLength) {
|
|
234
|
+
throwPasswordTooShort(this.minLength, password?.length ?? 0);
|
|
235
|
+
}
|
|
236
|
+
try {
|
|
237
|
+
const salt = (0, import_node_crypto.randomBytes)(32).toString("hex");
|
|
238
|
+
const hash = (0, import_node_crypto.pbkdf2Sync)(
|
|
239
|
+
password,
|
|
240
|
+
salt,
|
|
241
|
+
this.iterations,
|
|
242
|
+
this.keyLength,
|
|
243
|
+
"sha512"
|
|
244
|
+
).toString("hex");
|
|
245
|
+
return { hash, salt };
|
|
246
|
+
} catch (error) {
|
|
247
|
+
throwPasswordHashError(error instanceof Error ? error : void 0);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Verify password against hash
|
|
252
|
+
*/
|
|
253
|
+
async verify(password, hash, salt) {
|
|
254
|
+
try {
|
|
255
|
+
const computedHash = (0, import_node_crypto.pbkdf2Sync)(
|
|
256
|
+
password,
|
|
257
|
+
salt,
|
|
258
|
+
this.iterations,
|
|
259
|
+
this.keyLength,
|
|
260
|
+
"sha512"
|
|
261
|
+
).toString("hex");
|
|
262
|
+
const isValid = this.constantTimeCompare(computedHash, hash);
|
|
263
|
+
return isValid;
|
|
264
|
+
} catch (error) {
|
|
265
|
+
throwPasswordVerifyError(error instanceof Error ? error : void 0);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Constant-time string comparison (prevent timing attacks)
|
|
270
|
+
*/
|
|
271
|
+
constantTimeCompare(a, b) {
|
|
272
|
+
if (a.length !== b.length) {
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
try {
|
|
276
|
+
const bufA = Buffer.from(a);
|
|
277
|
+
const bufB = Buffer.from(b);
|
|
278
|
+
return (0, import_node_crypto.timingSafeEqual)(bufA, bufB);
|
|
279
|
+
} catch {
|
|
280
|
+
let result = 0;
|
|
281
|
+
for (let i = 0; i < a.length; i++) {
|
|
282
|
+
result |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
283
|
+
}
|
|
284
|
+
return result === 0;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
// src/auth/jwt.ts
|
|
290
|
+
var import_node_crypto2 = require("crypto");
|
|
291
|
+
var import_core2 = require("@datrix/core");
|
|
292
|
+
|
|
293
|
+
// src/auth/types.ts
|
|
294
|
+
function isJwtPayload(value) {
|
|
295
|
+
if (typeof value !== "object" || value === null) {
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
const obj = value;
|
|
299
|
+
return "userId" in obj && "role" in obj && "iat" in obj && "exp" in obj && typeof obj["userId"] === "number" && typeof obj["role"] === "string" && typeof obj["iat"] === "number" && typeof obj["exp"] === "number";
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// src/auth/jwt.ts
|
|
303
|
+
var JwtStrategy = class {
|
|
304
|
+
secret;
|
|
305
|
+
expiresIn;
|
|
306
|
+
// in seconds
|
|
307
|
+
algorithm;
|
|
308
|
+
issuer;
|
|
309
|
+
audience;
|
|
310
|
+
constructor(config) {
|
|
311
|
+
this.secret = config.secret;
|
|
312
|
+
this.expiresIn = this.parseExpiry(
|
|
313
|
+
config.expiresIn ?? import_core2.DEFAULT_API_AUTH_CONFIG.jwt.expiresIn
|
|
314
|
+
);
|
|
315
|
+
this.algorithm = config.algorithm ?? "HS256";
|
|
316
|
+
this.issuer = config.issuer;
|
|
317
|
+
this.audience = config.audience;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Sign a JWT token
|
|
321
|
+
*/
|
|
322
|
+
sign(payload) {
|
|
323
|
+
try {
|
|
324
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
325
|
+
const exp = now + this.expiresIn;
|
|
326
|
+
const basePayload = payload;
|
|
327
|
+
const fullPayload = {
|
|
328
|
+
userId: basePayload["userId"],
|
|
329
|
+
role: basePayload["role"],
|
|
330
|
+
iat: now,
|
|
331
|
+
exp,
|
|
332
|
+
...this.issuer && { iss: this.issuer },
|
|
333
|
+
...this.audience && { aud: this.audience },
|
|
334
|
+
...basePayload
|
|
335
|
+
};
|
|
336
|
+
const token = this.createToken(fullPayload);
|
|
337
|
+
return token;
|
|
338
|
+
} catch (error) {
|
|
339
|
+
throwJwtSignError(error instanceof Error ? error : void 0);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Verify a JWT token
|
|
344
|
+
*/
|
|
345
|
+
verify(token) {
|
|
346
|
+
try {
|
|
347
|
+
const parts = token.split(".");
|
|
348
|
+
if (parts.length !== 3) {
|
|
349
|
+
throwJwtInvalidFormat();
|
|
350
|
+
}
|
|
351
|
+
const encodedHeader = parts[0];
|
|
352
|
+
const encodedPayload = parts[1];
|
|
353
|
+
const signature = parts[2];
|
|
354
|
+
if (!encodedHeader || !encodedPayload || !signature) {
|
|
355
|
+
throwJwtInvalidFormat();
|
|
356
|
+
}
|
|
357
|
+
const expectedSignature = this.signData(
|
|
358
|
+
`${encodedHeader}.${encodedPayload}`
|
|
359
|
+
);
|
|
360
|
+
if (!this.constantTimeCompare(signature, expectedSignature)) {
|
|
361
|
+
throwJwtInvalidSignature();
|
|
362
|
+
}
|
|
363
|
+
const header = this.decodeBase64Url(encodedHeader);
|
|
364
|
+
if (!header || header.typ !== "JWT" || header.alg !== this.algorithm) {
|
|
365
|
+
throwJwtInvalidHeader();
|
|
366
|
+
}
|
|
367
|
+
const payload = this.decodeBase64Url(encodedPayload);
|
|
368
|
+
if (!payload || !isJwtPayload(payload)) {
|
|
369
|
+
throwJwtInvalidPayload();
|
|
370
|
+
}
|
|
371
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
372
|
+
if (payload.exp < now) {
|
|
373
|
+
throwJwtExpired(payload.exp, now);
|
|
374
|
+
}
|
|
375
|
+
if (payload.iat > now + 60) {
|
|
376
|
+
throwJwtInvalidIat();
|
|
377
|
+
}
|
|
378
|
+
if (this.issuer !== void 0 && payload.iss !== this.issuer) {
|
|
379
|
+
throwJwtInvalidIssuer(this.issuer, payload.iss);
|
|
380
|
+
}
|
|
381
|
+
if (this.audience !== void 0 && payload.aud !== this.audience) {
|
|
382
|
+
throwJwtInvalidAudience(this.audience, payload.aud);
|
|
383
|
+
}
|
|
384
|
+
return payload;
|
|
385
|
+
} catch (error) {
|
|
386
|
+
if (error instanceof Error && error.name === "DatrixAuthError") {
|
|
387
|
+
throw error;
|
|
388
|
+
}
|
|
389
|
+
throwJwtVerifyError(error instanceof Error ? error : void 0);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Refresh a JWT token
|
|
394
|
+
*
|
|
395
|
+
* Creates a new token with updated expiration
|
|
396
|
+
*/
|
|
397
|
+
refresh(token) {
|
|
398
|
+
const payload = this.verify(token);
|
|
399
|
+
const { userId, role, ...rest } = payload;
|
|
400
|
+
const { iat: _iat, exp: _exp, iss: _iss, aud: _aud, ...custom } = rest;
|
|
401
|
+
return this.sign({ userId, role, ...custom });
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Decode token without verification (for debugging)
|
|
405
|
+
*/
|
|
406
|
+
decode(token) {
|
|
407
|
+
try {
|
|
408
|
+
const parts = token.split(".");
|
|
409
|
+
if (parts.length !== 3) {
|
|
410
|
+
throwJwtInvalidFormat();
|
|
411
|
+
}
|
|
412
|
+
const encodedPayload = parts[1];
|
|
413
|
+
if (!encodedPayload) {
|
|
414
|
+
throwJwtInvalidFormat();
|
|
415
|
+
}
|
|
416
|
+
const payload = this.decodeBase64Url(encodedPayload);
|
|
417
|
+
if (!payload || !isJwtPayload(payload)) {
|
|
418
|
+
throwJwtInvalidPayload();
|
|
419
|
+
}
|
|
420
|
+
return payload;
|
|
421
|
+
} catch (error) {
|
|
422
|
+
if (error instanceof Error && error.name === "DatrixAuthError") {
|
|
423
|
+
throw error;
|
|
424
|
+
}
|
|
425
|
+
throwJwtDecodeError(error instanceof Error ? error : void 0);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Create a JWT token from payload
|
|
430
|
+
*/
|
|
431
|
+
createToken(payload) {
|
|
432
|
+
const header = {
|
|
433
|
+
alg: this.algorithm,
|
|
434
|
+
typ: "JWT"
|
|
435
|
+
};
|
|
436
|
+
const encodedHeader = this.encodeBase64Url(JSON.stringify(header));
|
|
437
|
+
const encodedPayload = this.encodeBase64Url(JSON.stringify(payload));
|
|
438
|
+
const signature = this.signData(`${encodedHeader}.${encodedPayload}`);
|
|
439
|
+
return `${encodedHeader}.${encodedPayload}.${signature}`;
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Sign data using HMAC
|
|
443
|
+
*/
|
|
444
|
+
signData(data) {
|
|
445
|
+
const algorithm = this.algorithm === "HS256" ? "sha256" : "sha512";
|
|
446
|
+
const hmac = (0, import_node_crypto2.createHmac)(algorithm, this.secret);
|
|
447
|
+
hmac.update(data);
|
|
448
|
+
return this.encodeBase64Url(hmac.digest("base64"));
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Base64 URL encode
|
|
452
|
+
*/
|
|
453
|
+
encodeBase64Url(str) {
|
|
454
|
+
return Buffer.from(str).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Base64 URL decode
|
|
458
|
+
*/
|
|
459
|
+
decodeBase64Url(str) {
|
|
460
|
+
try {
|
|
461
|
+
let padded = str;
|
|
462
|
+
while (padded.length % 4 !== 0) {
|
|
463
|
+
padded += "=";
|
|
464
|
+
}
|
|
465
|
+
const base64 = padded.replace(/-/g, "+").replace(/_/g, "/");
|
|
466
|
+
const decoded = Buffer.from(base64, "base64").toString("utf8");
|
|
467
|
+
return JSON.parse(decoded);
|
|
468
|
+
} catch {
|
|
469
|
+
return void 0;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Constant-time string comparison (prevent timing attacks)
|
|
474
|
+
*/
|
|
475
|
+
constantTimeCompare(a, b) {
|
|
476
|
+
if (a.length !== b.length) {
|
|
477
|
+
return false;
|
|
478
|
+
}
|
|
479
|
+
try {
|
|
480
|
+
const bufferA = Buffer.from(a);
|
|
481
|
+
const bufferB = Buffer.from(b);
|
|
482
|
+
return (0, import_node_crypto2.timingSafeEqual)(bufferA, bufferB);
|
|
483
|
+
} catch {
|
|
484
|
+
return false;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Parse expiry string or number to seconds
|
|
489
|
+
*/
|
|
490
|
+
parseExpiry(expiry) {
|
|
491
|
+
if (typeof expiry === "number") {
|
|
492
|
+
return expiry;
|
|
493
|
+
}
|
|
494
|
+
const match = expiry.match(/^(\d+)([smhd])$/);
|
|
495
|
+
if (!match) {
|
|
496
|
+
return 3600;
|
|
497
|
+
}
|
|
498
|
+
const [, num, unit] = match;
|
|
499
|
+
const value = parseInt(num, 10);
|
|
500
|
+
const multipliers = {
|
|
501
|
+
s: 1,
|
|
502
|
+
m: 60,
|
|
503
|
+
h: 3600,
|
|
504
|
+
d: 86400
|
|
505
|
+
};
|
|
506
|
+
return value * multipliers[unit];
|
|
507
|
+
}
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
// src/auth/session.ts
|
|
511
|
+
var import_node_crypto3 = require("crypto");
|
|
512
|
+
var import_core3 = require("@datrix/core");
|
|
513
|
+
var import_core4 = require("@datrix/core");
|
|
514
|
+
var SessionStrategy = class {
|
|
515
|
+
store;
|
|
516
|
+
maxAge;
|
|
517
|
+
// in seconds
|
|
518
|
+
checkPeriod;
|
|
519
|
+
// cleanup interval in seconds
|
|
520
|
+
cleanupTimer;
|
|
521
|
+
constructor(config) {
|
|
522
|
+
this.maxAge = config.maxAge ?? 86400;
|
|
523
|
+
this.checkPeriod = config.checkPeriod ?? 3600;
|
|
524
|
+
if (config.store && typeof config.store !== "string") {
|
|
525
|
+
this.store = config.store;
|
|
526
|
+
} else {
|
|
527
|
+
this.store = new MemorySessionStore(config.prefix);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* Create a new session
|
|
532
|
+
*/
|
|
533
|
+
async create(userId, role, data) {
|
|
534
|
+
try {
|
|
535
|
+
const sessionId = this.generateSessionId();
|
|
536
|
+
const now = /* @__PURE__ */ new Date();
|
|
537
|
+
const expiresAt = new Date(now.getTime() + this.maxAge * 1e3);
|
|
538
|
+
const sessionData = {
|
|
539
|
+
id: sessionId,
|
|
540
|
+
userId,
|
|
541
|
+
role,
|
|
542
|
+
createdAt: now,
|
|
543
|
+
expiresAt,
|
|
544
|
+
lastAccessedAt: now,
|
|
545
|
+
...data
|
|
546
|
+
};
|
|
547
|
+
await this.store.set(sessionId, sessionData);
|
|
548
|
+
return sessionData;
|
|
549
|
+
} catch (error) {
|
|
550
|
+
if (error instanceof Error && error.name === "DatrixAuthError") {
|
|
551
|
+
throw error;
|
|
552
|
+
}
|
|
553
|
+
throwSessionCreateError(error instanceof Error ? error : void 0);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* Get session by ID
|
|
558
|
+
*/
|
|
559
|
+
async get(sessionId) {
|
|
560
|
+
const session = await this.store.get(sessionId);
|
|
561
|
+
if (session === void 0) {
|
|
562
|
+
throwSessionNotFound(sessionId);
|
|
563
|
+
}
|
|
564
|
+
const now = /* @__PURE__ */ new Date();
|
|
565
|
+
if (session.expiresAt < now) {
|
|
566
|
+
await this.store.delete(sessionId);
|
|
567
|
+
throwSessionExpired(sessionId);
|
|
568
|
+
}
|
|
569
|
+
const updatedSession = {
|
|
570
|
+
...session,
|
|
571
|
+
lastAccessedAt: now
|
|
572
|
+
};
|
|
573
|
+
await this.store.set(sessionId, updatedSession);
|
|
574
|
+
return updatedSession;
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* Update session data
|
|
578
|
+
*/
|
|
579
|
+
async update(sessionId, data) {
|
|
580
|
+
const session = await this.get(sessionId);
|
|
581
|
+
const updatedSession = {
|
|
582
|
+
...session,
|
|
583
|
+
...data,
|
|
584
|
+
id: session.id,
|
|
585
|
+
// Preserve ID
|
|
586
|
+
createdAt: session.createdAt
|
|
587
|
+
// Preserve creation time
|
|
588
|
+
};
|
|
589
|
+
await this.store.set(sessionId, updatedSession);
|
|
590
|
+
return updatedSession;
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* Delete session
|
|
594
|
+
*/
|
|
595
|
+
async delete(sessionId) {
|
|
596
|
+
await this.store.delete(sessionId);
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* Refresh session (extend expiration)
|
|
600
|
+
*/
|
|
601
|
+
async refresh(sessionId) {
|
|
602
|
+
const now = /* @__PURE__ */ new Date();
|
|
603
|
+
const expiresAt = new Date(now.getTime() + this.maxAge * 1e3);
|
|
604
|
+
return this.update(sessionId, { expiresAt, lastAccessedAt: now });
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Validate session (check if exists and not expired)
|
|
608
|
+
*/
|
|
609
|
+
async validate(sessionId) {
|
|
610
|
+
try {
|
|
611
|
+
await this.get(sessionId);
|
|
612
|
+
return true;
|
|
613
|
+
} catch {
|
|
614
|
+
return false;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* Start cleanup timer
|
|
619
|
+
*/
|
|
620
|
+
startCleanup() {
|
|
621
|
+
if (this.cleanupTimer !== void 0) {
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
this.cleanupTimer = setInterval(() => {
|
|
625
|
+
void this.store.cleanup();
|
|
626
|
+
}, this.checkPeriod * 1e3);
|
|
627
|
+
this.cleanupTimer.unref();
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* Stop cleanup timer
|
|
631
|
+
*/
|
|
632
|
+
stopCleanup() {
|
|
633
|
+
if (this.cleanupTimer !== void 0) {
|
|
634
|
+
clearInterval(this.cleanupTimer);
|
|
635
|
+
this.cleanupTimer = void 0;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* Clear all sessions
|
|
640
|
+
*/
|
|
641
|
+
async clear() {
|
|
642
|
+
await this.store.clear();
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* Generate secure session ID
|
|
646
|
+
*/
|
|
647
|
+
generateSessionId() {
|
|
648
|
+
return (0, import_node_crypto3.randomBytes)(32).toString("hex");
|
|
649
|
+
}
|
|
650
|
+
};
|
|
651
|
+
var MemorySessionStore = class {
|
|
652
|
+
name = "memory";
|
|
653
|
+
sessions = /* @__PURE__ */ new Map();
|
|
654
|
+
prefix;
|
|
655
|
+
constructor(prefix = import_core3.DEFAULT_API_AUTH_CONFIG.session.prefix) {
|
|
656
|
+
this.prefix = prefix;
|
|
657
|
+
}
|
|
658
|
+
async get(sessionId) {
|
|
659
|
+
try {
|
|
660
|
+
const key = this.getKey(sessionId);
|
|
661
|
+
const session = this.sessions.get(key);
|
|
662
|
+
return session;
|
|
663
|
+
} catch (error) {
|
|
664
|
+
throw new import_core4.DatrixAuthError("Failed to get session from store", {
|
|
665
|
+
code: "SESSION_CREATE_ERROR",
|
|
666
|
+
strategy: "session",
|
|
667
|
+
cause: error instanceof Error ? error : void 0
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
async set(sessionId, data) {
|
|
672
|
+
const key = this.getKey(sessionId);
|
|
673
|
+
this.sessions.set(key, data);
|
|
674
|
+
}
|
|
675
|
+
async delete(sessionId) {
|
|
676
|
+
const key = this.getKey(sessionId);
|
|
677
|
+
this.sessions.delete(key);
|
|
678
|
+
}
|
|
679
|
+
async cleanup() {
|
|
680
|
+
const now = /* @__PURE__ */ new Date();
|
|
681
|
+
let deletedCount = 0;
|
|
682
|
+
for (const [key, session] of this.sessions.entries()) {
|
|
683
|
+
if (session.expiresAt < now) {
|
|
684
|
+
this.sessions.delete(key);
|
|
685
|
+
deletedCount++;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
return deletedCount;
|
|
689
|
+
}
|
|
690
|
+
async clear() {
|
|
691
|
+
this.sessions.clear();
|
|
692
|
+
}
|
|
693
|
+
getKey(sessionId) {
|
|
694
|
+
return `${this.prefix}${sessionId}`;
|
|
695
|
+
}
|
|
696
|
+
};
|
|
697
|
+
|
|
698
|
+
// src/auth/manager.ts
|
|
699
|
+
var AuthManager = class {
|
|
700
|
+
passwordManager;
|
|
701
|
+
jwtStrategy;
|
|
702
|
+
sessionStrategy;
|
|
703
|
+
config;
|
|
704
|
+
get authConfig() {
|
|
705
|
+
return this.config;
|
|
706
|
+
}
|
|
707
|
+
constructor(config) {
|
|
708
|
+
this.config = config;
|
|
709
|
+
this.passwordManager = new PasswordManager(config.password);
|
|
710
|
+
if (config.jwt) {
|
|
711
|
+
this.jwtStrategy = new JwtStrategy(config.jwt);
|
|
712
|
+
}
|
|
713
|
+
if (config.session) {
|
|
714
|
+
this.sessionStrategy = new SessionStrategy(config.session);
|
|
715
|
+
this.sessionStrategy.startCleanup();
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* Hash password
|
|
720
|
+
*/
|
|
721
|
+
async hashPassword(password) {
|
|
722
|
+
return this.passwordManager.hash(password);
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* Verify password
|
|
726
|
+
*/
|
|
727
|
+
async verifyPassword(password, hash, salt) {
|
|
728
|
+
return this.passwordManager.verify(password, hash, salt);
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* Login user and create token/session
|
|
732
|
+
*/
|
|
733
|
+
async login(user, options = {}) {
|
|
734
|
+
const { createToken = true, createSession = true } = options;
|
|
735
|
+
let token = void 0;
|
|
736
|
+
let sessionId = void 0;
|
|
737
|
+
if (this.jwtStrategy && createToken) {
|
|
738
|
+
token = this.jwtStrategy.sign({
|
|
739
|
+
userId: user.id,
|
|
740
|
+
role: user.role
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
if (this.sessionStrategy && createSession) {
|
|
744
|
+
const sessionData = await this.sessionStrategy.create(user.id, user.role);
|
|
745
|
+
sessionId = sessionData.id;
|
|
746
|
+
}
|
|
747
|
+
const result = {
|
|
748
|
+
user,
|
|
749
|
+
...token !== void 0 && { token },
|
|
750
|
+
...sessionId !== void 0 && { sessionId }
|
|
751
|
+
};
|
|
752
|
+
return result;
|
|
753
|
+
}
|
|
754
|
+
/**
|
|
755
|
+
* Logout user (destroy session)
|
|
756
|
+
*/
|
|
757
|
+
async logout(sessionId) {
|
|
758
|
+
if (!this.sessionStrategy) {
|
|
759
|
+
throwSessionNotConfigured();
|
|
760
|
+
}
|
|
761
|
+
await this.sessionStrategy.delete(sessionId);
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* Authenticate request (extract and verify token/session)
|
|
765
|
+
*/
|
|
766
|
+
async authenticate(request) {
|
|
767
|
+
const token = this.extractToken(request);
|
|
768
|
+
if (token && this.jwtStrategy) {
|
|
769
|
+
try {
|
|
770
|
+
const payload = this.jwtStrategy.verify(token);
|
|
771
|
+
return {
|
|
772
|
+
user: {
|
|
773
|
+
id: payload.userId,
|
|
774
|
+
email: "",
|
|
775
|
+
// Will be fetched from DB if needed
|
|
776
|
+
role: payload.role
|
|
777
|
+
},
|
|
778
|
+
token
|
|
779
|
+
};
|
|
780
|
+
} catch {
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
const sessionId = this.extractSessionId(request);
|
|
784
|
+
if (sessionId && this.sessionStrategy) {
|
|
785
|
+
try {
|
|
786
|
+
const session = await this.sessionStrategy.get(sessionId);
|
|
787
|
+
return {
|
|
788
|
+
user: {
|
|
789
|
+
id: session.userId,
|
|
790
|
+
email: "",
|
|
791
|
+
role: session.role
|
|
792
|
+
},
|
|
793
|
+
sessionId
|
|
794
|
+
};
|
|
795
|
+
} catch {
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
return null;
|
|
799
|
+
}
|
|
800
|
+
/**
|
|
801
|
+
* Extract JWT token from request headers
|
|
802
|
+
*/
|
|
803
|
+
extractToken(request) {
|
|
804
|
+
const authHeader = request.headers.get("authorization");
|
|
805
|
+
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
806
|
+
return null;
|
|
807
|
+
}
|
|
808
|
+
return authHeader.slice(7);
|
|
809
|
+
}
|
|
810
|
+
/**
|
|
811
|
+
* Extract session ID from request cookies
|
|
812
|
+
*/
|
|
813
|
+
extractSessionId(request) {
|
|
814
|
+
const cookieHeader = request.headers.get("cookie");
|
|
815
|
+
if (!cookieHeader) {
|
|
816
|
+
return null;
|
|
817
|
+
}
|
|
818
|
+
const cookies = cookieHeader.split(";").reduce(
|
|
819
|
+
(acc, cookie) => {
|
|
820
|
+
const [key, value] = cookie.trim().split("=");
|
|
821
|
+
if (key && value) {
|
|
822
|
+
acc[key] = value;
|
|
823
|
+
}
|
|
824
|
+
return acc;
|
|
825
|
+
},
|
|
826
|
+
{}
|
|
827
|
+
);
|
|
828
|
+
return cookies["sessionId"] ?? null;
|
|
829
|
+
}
|
|
830
|
+
/**
|
|
831
|
+
* Get JWT strategy (for advanced usage)
|
|
832
|
+
*/
|
|
833
|
+
getJwtStrategy() {
|
|
834
|
+
return this.jwtStrategy;
|
|
835
|
+
}
|
|
836
|
+
/**
|
|
837
|
+
* Get session strategy (for advanced usage)
|
|
838
|
+
*/
|
|
839
|
+
getSessionStrategy() {
|
|
840
|
+
return this.sessionStrategy;
|
|
841
|
+
}
|
|
842
|
+
/**
|
|
843
|
+
* Cleanup resources
|
|
844
|
+
*/
|
|
845
|
+
async destroy() {
|
|
846
|
+
if (this.sessionStrategy) {
|
|
847
|
+
this.sessionStrategy.stopCleanup();
|
|
848
|
+
}
|
|
849
|
+
if (this.sessionStrategy) {
|
|
850
|
+
await this.sessionStrategy.clear();
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
};
|
|
854
|
+
|
|
855
|
+
// src/handler/auth-handler.ts
|
|
856
|
+
var import_core7 = require("@datrix/core");
|
|
857
|
+
|
|
858
|
+
// src/handler/utils.ts
|
|
859
|
+
var import_core6 = require("@datrix/core");
|
|
860
|
+
|
|
861
|
+
// src/errors/api-error.ts
|
|
862
|
+
var import_core5 = require("@datrix/core");
|
|
863
|
+
var DatrixApiError = class extends import_core5.DatrixError {
|
|
864
|
+
/** HTTP status code associated with this error */
|
|
865
|
+
status;
|
|
866
|
+
constructor(message, options) {
|
|
867
|
+
super(message, {
|
|
868
|
+
code: options.code,
|
|
869
|
+
operation: options.operation || "api:handler",
|
|
870
|
+
...options.context && { context: options.context },
|
|
871
|
+
...options.suggestion && { suggestion: options.suggestion },
|
|
872
|
+
...options.expected && { expected: options.expected },
|
|
873
|
+
...options.received !== void 0 && { received: options.received },
|
|
874
|
+
...options.cause && { cause: options.cause }
|
|
875
|
+
});
|
|
876
|
+
this.status = options.status || 500;
|
|
877
|
+
}
|
|
878
|
+
/**
|
|
879
|
+
* Override toJSON to include status
|
|
880
|
+
*/
|
|
881
|
+
toJSON() {
|
|
882
|
+
return {
|
|
883
|
+
...super.toJSON(),
|
|
884
|
+
status: this.status
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
};
|
|
888
|
+
var handlerError = {
|
|
889
|
+
schemaNotFound(tableName, availableModels) {
|
|
890
|
+
return new DatrixApiError(`Model not found for table: ${tableName}`, {
|
|
891
|
+
code: "SCHEMA_NOT_FOUND",
|
|
892
|
+
status: 404,
|
|
893
|
+
context: { tableName, availableModels },
|
|
894
|
+
suggestion: "Check if the table name is correct and the schema is properly defined."
|
|
895
|
+
});
|
|
896
|
+
},
|
|
897
|
+
modelNotSpecified() {
|
|
898
|
+
return new DatrixApiError("Model not specified in the request URL", {
|
|
899
|
+
code: "MODEL_NOT_SPECIFIED",
|
|
900
|
+
status: 400,
|
|
901
|
+
suggestion: "Ensure the URL includes the model name (e.g., /api/users)."
|
|
902
|
+
});
|
|
903
|
+
},
|
|
904
|
+
recordNotFound(modelName, id) {
|
|
905
|
+
return new DatrixApiError(`${modelName} record not found with ID: ${id}`, {
|
|
906
|
+
code: "NOT_FOUND",
|
|
907
|
+
status: 404,
|
|
908
|
+
context: { modelName, id },
|
|
909
|
+
suggestion: "Verify the ID is correct or if the record has been deleted."
|
|
910
|
+
});
|
|
911
|
+
},
|
|
912
|
+
invalidBody(reason) {
|
|
913
|
+
return new DatrixApiError(
|
|
914
|
+
reason ? `Invalid request body: ${reason}` : "Invalid request body",
|
|
915
|
+
{
|
|
916
|
+
code: "INVALID_BODY",
|
|
917
|
+
status: 400,
|
|
918
|
+
context: { reason },
|
|
919
|
+
suggestion: "Ensure the request body is a valid JSON object and contains all required fields."
|
|
920
|
+
}
|
|
921
|
+
);
|
|
922
|
+
},
|
|
923
|
+
missingId(operation) {
|
|
924
|
+
return new DatrixApiError(`ID is required for ${operation}`, {
|
|
925
|
+
code: "MISSING_ID",
|
|
926
|
+
status: 400,
|
|
927
|
+
suggestion: `Provide a valid ID in the URL for the ${operation} operation.`
|
|
928
|
+
});
|
|
929
|
+
},
|
|
930
|
+
methodNotAllowed(method) {
|
|
931
|
+
return new DatrixApiError(
|
|
932
|
+
`HTTP Method ${method} is not allowed for this route`,
|
|
933
|
+
{
|
|
934
|
+
code: "METHOD_NOT_ALLOWED",
|
|
935
|
+
status: 405,
|
|
936
|
+
context: { method },
|
|
937
|
+
suggestion: "Check the API documentation for supported methods on this endpoint."
|
|
938
|
+
}
|
|
939
|
+
);
|
|
940
|
+
},
|
|
941
|
+
permissionDenied(reason, context) {
|
|
942
|
+
return new DatrixApiError("Permission denied", {
|
|
943
|
+
code: "FORBIDDEN",
|
|
944
|
+
status: 403,
|
|
945
|
+
context: { reason, ...context },
|
|
946
|
+
suggestion: "Check your permissions or contact an administrator."
|
|
947
|
+
});
|
|
948
|
+
},
|
|
949
|
+
unauthorized(reason) {
|
|
950
|
+
return new DatrixApiError("Unauthorized access", {
|
|
951
|
+
code: "UNAUTHORIZED",
|
|
952
|
+
status: 401,
|
|
953
|
+
context: { reason },
|
|
954
|
+
suggestion: "Provide valid authentication credentials."
|
|
955
|
+
});
|
|
956
|
+
},
|
|
957
|
+
internalError(message, cause) {
|
|
958
|
+
return new DatrixApiError(message, {
|
|
959
|
+
code: "INTERNAL_ERROR",
|
|
960
|
+
status: 500,
|
|
961
|
+
...cause && { cause }
|
|
962
|
+
});
|
|
963
|
+
},
|
|
964
|
+
conflict(reason, context) {
|
|
965
|
+
return new DatrixApiError(reason, {
|
|
966
|
+
code: "CONFLICT",
|
|
967
|
+
status: 409,
|
|
968
|
+
...context && { context },
|
|
969
|
+
suggestion: "Ensure the resource you are trying to create does not already exist."
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
};
|
|
973
|
+
|
|
974
|
+
// src/handler/utils.ts
|
|
975
|
+
function jsonResponse(data, status = 200) {
|
|
976
|
+
return new Response(JSON.stringify(data), {
|
|
977
|
+
status,
|
|
978
|
+
headers: { "Content-Type": "application/json" }
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
function datrixErrorResponse(error) {
|
|
982
|
+
let status = 400;
|
|
983
|
+
if (error instanceof DatrixApiError) {
|
|
984
|
+
status = error.status;
|
|
985
|
+
} else if (error instanceof import_core6.DatrixValidationError) {
|
|
986
|
+
status = 400;
|
|
987
|
+
}
|
|
988
|
+
const serialized = error.toJSON();
|
|
989
|
+
return jsonResponse(
|
|
990
|
+
{
|
|
991
|
+
error: {
|
|
992
|
+
...serialized,
|
|
993
|
+
type: error.name
|
|
994
|
+
}
|
|
995
|
+
},
|
|
996
|
+
status
|
|
997
|
+
);
|
|
998
|
+
}
|
|
999
|
+
function extractSessionId(request) {
|
|
1000
|
+
const cookieHeader = request.headers.get("cookie");
|
|
1001
|
+
if (!cookieHeader) return null;
|
|
1002
|
+
const match = cookieHeader.match(/sessionId=([^;]+)/);
|
|
1003
|
+
return match ? match[1] : null;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
// src/errors/auth-error.ts
|
|
1007
|
+
var authError = {
|
|
1008
|
+
invalidCredentials() {
|
|
1009
|
+
return new DatrixApiError("Invalid email or password", {
|
|
1010
|
+
code: "INVALID_CREDENTIALS",
|
|
1011
|
+
status: 401,
|
|
1012
|
+
suggestion: "Please check your email and password and try again."
|
|
1013
|
+
});
|
|
1014
|
+
},
|
|
1015
|
+
invalidToken(reason) {
|
|
1016
|
+
return new DatrixApiError("Invalid or expired authentication token", {
|
|
1017
|
+
code: "INVALID_TOKEN",
|
|
1018
|
+
status: 401,
|
|
1019
|
+
context: { reason },
|
|
1020
|
+
suggestion: "Please log in again to obtain a new session."
|
|
1021
|
+
});
|
|
1022
|
+
},
|
|
1023
|
+
missingToken() {
|
|
1024
|
+
return new DatrixApiError("Authentication token is missing", {
|
|
1025
|
+
code: "MISSING_TOKEN",
|
|
1026
|
+
status: 401,
|
|
1027
|
+
suggestion: "Include an Authorization header or a session cookie in your request."
|
|
1028
|
+
});
|
|
1029
|
+
},
|
|
1030
|
+
sessionExpired() {
|
|
1031
|
+
return new DatrixApiError("Your session has expired", {
|
|
1032
|
+
code: "SESSION_EXPIRED",
|
|
1033
|
+
status: 401,
|
|
1034
|
+
suggestion: "Log in again to continue using the application."
|
|
1035
|
+
});
|
|
1036
|
+
},
|
|
1037
|
+
accountLocked(reason) {
|
|
1038
|
+
return new DatrixApiError("This account has been locked", {
|
|
1039
|
+
code: "ACCOUNT_LOCKED",
|
|
1040
|
+
status: 403,
|
|
1041
|
+
context: { reason },
|
|
1042
|
+
suggestion: "Contact support to unlock your account."
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
1045
|
+
};
|
|
1046
|
+
|
|
1047
|
+
// src/handler/auth-handler.ts
|
|
1048
|
+
var import_core8 = require("@datrix/core");
|
|
1049
|
+
function createAuthHandlers(config) {
|
|
1050
|
+
const { datrix, authManager, authConfig } = config;
|
|
1051
|
+
const userSchemaName = authConfig.userSchema?.name ?? "user";
|
|
1052
|
+
const authSchemaName = authConfig.authSchemaName ?? "authentication";
|
|
1053
|
+
const userEmailField = authConfig.userSchema?.email ?? "email";
|
|
1054
|
+
const defaultRole = authConfig.defaultRole;
|
|
1055
|
+
async function register(request) {
|
|
1056
|
+
try {
|
|
1057
|
+
if (authConfig.endpoints?.disableRegister) {
|
|
1058
|
+
throw handlerError.permissionDenied("Registration is disabled");
|
|
1059
|
+
}
|
|
1060
|
+
const body = await request.json();
|
|
1061
|
+
const { email, password, ...extraData } = body;
|
|
1062
|
+
if (!email || typeof email !== "string") {
|
|
1063
|
+
throw handlerError.invalidBody("Email is required");
|
|
1064
|
+
}
|
|
1065
|
+
if (!password || typeof password !== "string") {
|
|
1066
|
+
throw handlerError.invalidBody("Password is required");
|
|
1067
|
+
}
|
|
1068
|
+
const existingAuth = await datrix.raw.findOne(
|
|
1069
|
+
authSchemaName,
|
|
1070
|
+
{ email }
|
|
1071
|
+
);
|
|
1072
|
+
if (existingAuth) {
|
|
1073
|
+
throw handlerError.conflict("User with this email already exists");
|
|
1074
|
+
}
|
|
1075
|
+
const { hash, salt } = await authManager.hashPassword(password);
|
|
1076
|
+
const userData = {
|
|
1077
|
+
[userEmailField]: email,
|
|
1078
|
+
...extraData
|
|
1079
|
+
};
|
|
1080
|
+
let user;
|
|
1081
|
+
try {
|
|
1082
|
+
const createdUser = await datrix.raw.create(userSchemaName, userData);
|
|
1083
|
+
if (!createdUser) {
|
|
1084
|
+
throw handlerError.internalError("Failed to create user record");
|
|
1085
|
+
}
|
|
1086
|
+
user = createdUser;
|
|
1087
|
+
} catch (error) {
|
|
1088
|
+
if (error instanceof import_core8.DatrixError) {
|
|
1089
|
+
throw error;
|
|
1090
|
+
}
|
|
1091
|
+
const message = error instanceof Error ? error.message : "Failed to create user";
|
|
1092
|
+
throw handlerError.invalidBody(message);
|
|
1093
|
+
}
|
|
1094
|
+
const authData = {
|
|
1095
|
+
user: { set: [{ id: user.id }] },
|
|
1096
|
+
email,
|
|
1097
|
+
password: hash,
|
|
1098
|
+
passwordSalt: salt,
|
|
1099
|
+
role: defaultRole
|
|
1100
|
+
};
|
|
1101
|
+
const authRecord = await datrix.raw.create(
|
|
1102
|
+
authSchemaName,
|
|
1103
|
+
authData
|
|
1104
|
+
);
|
|
1105
|
+
if (!authRecord) {
|
|
1106
|
+
await datrix.raw.delete(userSchemaName, user.id);
|
|
1107
|
+
throw handlerError.internalError(
|
|
1108
|
+
"Failed to create authentication record"
|
|
1109
|
+
);
|
|
1110
|
+
}
|
|
1111
|
+
const authUser = {
|
|
1112
|
+
id: authRecord.id,
|
|
1113
|
+
email: authRecord.email,
|
|
1114
|
+
role: authRecord.role
|
|
1115
|
+
};
|
|
1116
|
+
const loginResult = await authManager.login(authUser);
|
|
1117
|
+
const responseBody = {
|
|
1118
|
+
data: {
|
|
1119
|
+
user: authUser,
|
|
1120
|
+
token: loginResult.token,
|
|
1121
|
+
sessionId: loginResult.sessionId
|
|
1122
|
+
}
|
|
1123
|
+
};
|
|
1124
|
+
if (loginResult.sessionId) {
|
|
1125
|
+
return new Response(JSON.stringify(responseBody), {
|
|
1126
|
+
status: 201,
|
|
1127
|
+
headers: {
|
|
1128
|
+
"Content-Type": "application/json",
|
|
1129
|
+
"Set-Cookie": `sessionId=${loginResult.sessionId}; HttpOnly; Path=/; Max-Age=86400; SameSite=Strict`
|
|
1130
|
+
}
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
1133
|
+
return jsonResponse(responseBody, 201);
|
|
1134
|
+
} catch (error) {
|
|
1135
|
+
if (error instanceof import_core8.DatrixError) {
|
|
1136
|
+
return datrixErrorResponse(error);
|
|
1137
|
+
}
|
|
1138
|
+
const message = error instanceof Error ? error.message : "Internal server error";
|
|
1139
|
+
return datrixErrorResponse(
|
|
1140
|
+
handlerError.internalError(
|
|
1141
|
+
message,
|
|
1142
|
+
error instanceof Error ? error : void 0
|
|
1143
|
+
)
|
|
1144
|
+
);
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
async function login(request) {
|
|
1148
|
+
try {
|
|
1149
|
+
const body = await request.json();
|
|
1150
|
+
const { email, password } = body;
|
|
1151
|
+
if (!email || typeof email !== "string") {
|
|
1152
|
+
throw handlerError.invalidBody("Email is required");
|
|
1153
|
+
}
|
|
1154
|
+
if (!password || typeof password !== "string") {
|
|
1155
|
+
throw handlerError.invalidBody("Password is required");
|
|
1156
|
+
}
|
|
1157
|
+
const authRecord = await datrix.raw.findOne(
|
|
1158
|
+
authSchemaName,
|
|
1159
|
+
{ email }
|
|
1160
|
+
);
|
|
1161
|
+
if (!authRecord) {
|
|
1162
|
+
throw authError.invalidCredentials();
|
|
1163
|
+
}
|
|
1164
|
+
const isValid = await authManager.verifyPassword(
|
|
1165
|
+
password,
|
|
1166
|
+
authRecord.password,
|
|
1167
|
+
authRecord.passwordSalt
|
|
1168
|
+
);
|
|
1169
|
+
if (!isValid) {
|
|
1170
|
+
throw authError.invalidCredentials();
|
|
1171
|
+
}
|
|
1172
|
+
const authUser = {
|
|
1173
|
+
id: authRecord.id,
|
|
1174
|
+
email: authRecord.email,
|
|
1175
|
+
role: authRecord.role
|
|
1176
|
+
};
|
|
1177
|
+
const loginResult = await authManager.login(authUser);
|
|
1178
|
+
const responseBody = {
|
|
1179
|
+
data: {
|
|
1180
|
+
user: authUser,
|
|
1181
|
+
token: loginResult.token,
|
|
1182
|
+
sessionId: loginResult.sessionId
|
|
1183
|
+
}
|
|
1184
|
+
};
|
|
1185
|
+
if (loginResult.sessionId) {
|
|
1186
|
+
return new Response(JSON.stringify(responseBody), {
|
|
1187
|
+
status: 200,
|
|
1188
|
+
headers: {
|
|
1189
|
+
"Content-Type": "application/json",
|
|
1190
|
+
"Set-Cookie": `sessionId=${loginResult.sessionId}; HttpOnly; Path=/; Max-Age=86400; SameSite=Strict`
|
|
1191
|
+
}
|
|
1192
|
+
});
|
|
1193
|
+
}
|
|
1194
|
+
return jsonResponse(responseBody);
|
|
1195
|
+
} catch (error) {
|
|
1196
|
+
if (error instanceof import_core8.DatrixError) {
|
|
1197
|
+
return datrixErrorResponse(error);
|
|
1198
|
+
}
|
|
1199
|
+
const message = error instanceof Error ? error.message : "Internal server error";
|
|
1200
|
+
return datrixErrorResponse(
|
|
1201
|
+
handlerError.internalError(
|
|
1202
|
+
message,
|
|
1203
|
+
error instanceof Error ? error : void 0
|
|
1204
|
+
)
|
|
1205
|
+
);
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
async function logout(request) {
|
|
1209
|
+
try {
|
|
1210
|
+
const sessionId = extractSessionId(request);
|
|
1211
|
+
if (!sessionId) {
|
|
1212
|
+
throw handlerError.invalidBody("No session found");
|
|
1213
|
+
}
|
|
1214
|
+
await authManager.logout(sessionId);
|
|
1215
|
+
return new Response(JSON.stringify({ data: { success: true } }), {
|
|
1216
|
+
status: 200,
|
|
1217
|
+
headers: {
|
|
1218
|
+
"Content-Type": "application/json",
|
|
1219
|
+
"Set-Cookie": "sessionId=; HttpOnly; Path=/; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=Strict"
|
|
1220
|
+
}
|
|
1221
|
+
});
|
|
1222
|
+
} catch (error) {
|
|
1223
|
+
if (error instanceof import_core8.DatrixError) {
|
|
1224
|
+
return datrixErrorResponse(error);
|
|
1225
|
+
}
|
|
1226
|
+
const message = error instanceof Error ? error.message : "Internal server error";
|
|
1227
|
+
return datrixErrorResponse(
|
|
1228
|
+
handlerError.internalError(
|
|
1229
|
+
message,
|
|
1230
|
+
error instanceof Error ? error : void 0
|
|
1231
|
+
)
|
|
1232
|
+
);
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
async function me(request) {
|
|
1236
|
+
try {
|
|
1237
|
+
const authContext = await authManager.authenticate(request);
|
|
1238
|
+
if (!authContext || !authContext.user) {
|
|
1239
|
+
throw authError.invalidToken();
|
|
1240
|
+
}
|
|
1241
|
+
const authenticatedUser = await datrix.raw.findById(
|
|
1242
|
+
authSchemaName,
|
|
1243
|
+
authContext.user.id,
|
|
1244
|
+
{ populate: { user: "*" } }
|
|
1245
|
+
);
|
|
1246
|
+
if (!authenticatedUser) {
|
|
1247
|
+
throw handlerError.recordNotFound(
|
|
1248
|
+
userSchemaName,
|
|
1249
|
+
String(authContext.user.id)
|
|
1250
|
+
);
|
|
1251
|
+
}
|
|
1252
|
+
return jsonResponse({ data: authenticatedUser });
|
|
1253
|
+
} catch (error) {
|
|
1254
|
+
if (error instanceof import_core8.DatrixError) {
|
|
1255
|
+
return datrixErrorResponse(error);
|
|
1256
|
+
}
|
|
1257
|
+
const message = error instanceof Error ? error.message : "Internal server error";
|
|
1258
|
+
return datrixErrorResponse(
|
|
1259
|
+
handlerError.internalError(
|
|
1260
|
+
message,
|
|
1261
|
+
error instanceof Error ? error : void 0
|
|
1262
|
+
)
|
|
1263
|
+
);
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
return { register, login, logout, me };
|
|
1267
|
+
}
|
|
1268
|
+
function createUnifiedAuthHandler(config, apiPrefix = "/api") {
|
|
1269
|
+
const handlers = createAuthHandlers(config);
|
|
1270
|
+
const { authConfig } = config;
|
|
1271
|
+
const endpoints = {
|
|
1272
|
+
register: authConfig.endpoints?.register ?? import_core7.DEFAULT_API_AUTH_CONFIG.endpoints.register,
|
|
1273
|
+
login: authConfig.endpoints?.login ?? import_core7.DEFAULT_API_AUTH_CONFIG.endpoints.login,
|
|
1274
|
+
logout: authConfig.endpoints?.logout ?? import_core7.DEFAULT_API_AUTH_CONFIG.endpoints.logout,
|
|
1275
|
+
me: authConfig.endpoints?.me ?? import_core7.DEFAULT_API_AUTH_CONFIG.endpoints.me
|
|
1276
|
+
};
|
|
1277
|
+
return async function authHandler(request) {
|
|
1278
|
+
const url = new URL(request.url);
|
|
1279
|
+
const path = url.pathname.slice(apiPrefix.length);
|
|
1280
|
+
const method = request.method;
|
|
1281
|
+
if (path === endpoints.register && method === "POST") {
|
|
1282
|
+
return handlers.register(request);
|
|
1283
|
+
}
|
|
1284
|
+
if (path === endpoints.login && method === "POST") {
|
|
1285
|
+
return handlers.login(request);
|
|
1286
|
+
}
|
|
1287
|
+
if (path === endpoints.logout && method === "POST") {
|
|
1288
|
+
return handlers.logout(request);
|
|
1289
|
+
}
|
|
1290
|
+
if (path === endpoints.me && method === "GET") {
|
|
1291
|
+
return handlers.me(request);
|
|
1292
|
+
}
|
|
1293
|
+
return datrixErrorResponse(
|
|
1294
|
+
handlerError.recordNotFound("Auth Route", url.pathname)
|
|
1295
|
+
);
|
|
1296
|
+
};
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
// src/middleware/permission.ts
|
|
1300
|
+
var import_core9 = require("@datrix/core");
|
|
1301
|
+
function buildPermCtx(ctx) {
|
|
1302
|
+
const permCtx = {
|
|
1303
|
+
user: ctx.user ?? void 0,
|
|
1304
|
+
id: ctx.id,
|
|
1305
|
+
action: ctx.action,
|
|
1306
|
+
datrix: ctx.datrix
|
|
1307
|
+
};
|
|
1308
|
+
if (ctx.body) {
|
|
1309
|
+
permCtx.input = ctx.body;
|
|
1310
|
+
}
|
|
1311
|
+
return permCtx;
|
|
1312
|
+
}
|
|
1313
|
+
async function evaluatePermissionValue(value, ctx) {
|
|
1314
|
+
if (value === void 0) {
|
|
1315
|
+
return true;
|
|
1316
|
+
}
|
|
1317
|
+
if (typeof value === "boolean") {
|
|
1318
|
+
return value;
|
|
1319
|
+
}
|
|
1320
|
+
if ((0, import_core9.isPermissionFn)(value)) {
|
|
1321
|
+
const permCtx = buildPermCtx(ctx);
|
|
1322
|
+
return await value(permCtx);
|
|
1323
|
+
}
|
|
1324
|
+
if (Array.isArray(value)) {
|
|
1325
|
+
if (!ctx.user) {
|
|
1326
|
+
for (const item of value) {
|
|
1327
|
+
if ((0, import_core9.isPermissionFn)(item)) {
|
|
1328
|
+
const permCtx = buildPermCtx(ctx);
|
|
1329
|
+
const result = await item(permCtx);
|
|
1330
|
+
if (result) return true;
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
return false;
|
|
1334
|
+
}
|
|
1335
|
+
for (const item of value) {
|
|
1336
|
+
if (typeof item === "string") {
|
|
1337
|
+
if (ctx.user.role === item) {
|
|
1338
|
+
return true;
|
|
1339
|
+
}
|
|
1340
|
+
} else if ((0, import_core9.isPermissionFn)(item)) {
|
|
1341
|
+
const permCtx = buildPermCtx(ctx);
|
|
1342
|
+
const result = await item(permCtx);
|
|
1343
|
+
if (result) return true;
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
return false;
|
|
1347
|
+
}
|
|
1348
|
+
return false;
|
|
1349
|
+
}
|
|
1350
|
+
async function checkSchemaPermission(schema, ctx, defaultPermission) {
|
|
1351
|
+
const { action } = ctx;
|
|
1352
|
+
const schemaPermission = schema.permission;
|
|
1353
|
+
let permissionValue;
|
|
1354
|
+
if (schemaPermission && schemaPermission[action] !== void 0) {
|
|
1355
|
+
permissionValue = schemaPermission[action];
|
|
1356
|
+
} else if (defaultPermission && defaultPermission[action] !== void 0) {
|
|
1357
|
+
permissionValue = defaultPermission[action];
|
|
1358
|
+
}
|
|
1359
|
+
const allowed = await evaluatePermissionValue(permissionValue, ctx);
|
|
1360
|
+
return {
|
|
1361
|
+
allowed,
|
|
1362
|
+
reason: allowed ? void 0 : `Permission denied for ${action} on ${schema.name}`
|
|
1363
|
+
};
|
|
1364
|
+
}
|
|
1365
|
+
async function filterFieldsForRead(schema, record, ctx) {
|
|
1366
|
+
const deniedFields = [];
|
|
1367
|
+
const filtered = {};
|
|
1368
|
+
for (const [fieldName, fieldValue] of Object.entries(record)) {
|
|
1369
|
+
const fieldDef = schema.fields[fieldName];
|
|
1370
|
+
if (!fieldDef) {
|
|
1371
|
+
filtered[fieldName] = fieldValue;
|
|
1372
|
+
continue;
|
|
1373
|
+
}
|
|
1374
|
+
const fieldPermission = fieldDef.permission;
|
|
1375
|
+
if (!fieldPermission || fieldPermission.read === void 0) {
|
|
1376
|
+
filtered[fieldName] = fieldValue;
|
|
1377
|
+
continue;
|
|
1378
|
+
}
|
|
1379
|
+
const allowed = await evaluatePermissionValue(fieldPermission.read, ctx);
|
|
1380
|
+
if (allowed) {
|
|
1381
|
+
filtered[fieldName] = fieldValue;
|
|
1382
|
+
} else {
|
|
1383
|
+
deniedFields.push(fieldName);
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
return { data: filtered, deniedFields };
|
|
1387
|
+
}
|
|
1388
|
+
async function checkFieldsForWrite(schema, ctx) {
|
|
1389
|
+
const deniedFields = [];
|
|
1390
|
+
const input = ctx.body ?? {};
|
|
1391
|
+
for (const fieldName of Object.keys(input)) {
|
|
1392
|
+
const fieldDef = schema.fields[fieldName];
|
|
1393
|
+
if (!fieldDef) {
|
|
1394
|
+
continue;
|
|
1395
|
+
}
|
|
1396
|
+
const fieldPermission = fieldDef.permission;
|
|
1397
|
+
if (!fieldPermission || fieldPermission.write === void 0) {
|
|
1398
|
+
continue;
|
|
1399
|
+
}
|
|
1400
|
+
const allowed = await evaluatePermissionValue(fieldPermission.write, ctx);
|
|
1401
|
+
if (!allowed) {
|
|
1402
|
+
deniedFields.push(fieldName);
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
return {
|
|
1406
|
+
allowed: deniedFields.length === 0,
|
|
1407
|
+
deniedFields: deniedFields.length > 0 ? deniedFields : void 0
|
|
1408
|
+
};
|
|
1409
|
+
}
|
|
1410
|
+
function methodToAction(method) {
|
|
1411
|
+
switch (method.toUpperCase()) {
|
|
1412
|
+
case "GET":
|
|
1413
|
+
return "read";
|
|
1414
|
+
case "POST":
|
|
1415
|
+
return "create";
|
|
1416
|
+
case "PATCH":
|
|
1417
|
+
case "PUT":
|
|
1418
|
+
return "update";
|
|
1419
|
+
case "DELETE":
|
|
1420
|
+
return "delete";
|
|
1421
|
+
default:
|
|
1422
|
+
return "read";
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
async function filterRecordsForRead(schema, records, ctx) {
|
|
1426
|
+
const filtered = [];
|
|
1427
|
+
for (const record of records) {
|
|
1428
|
+
const { data } = await filterFieldsForRead(schema, record, ctx);
|
|
1429
|
+
filtered.push(data);
|
|
1430
|
+
}
|
|
1431
|
+
return filtered;
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
// src/parser/query-parser.ts
|
|
1435
|
+
var import_core15 = require("@datrix/core");
|
|
1436
|
+
var import_core16 = require("@datrix/core");
|
|
1437
|
+
|
|
1438
|
+
// src/parser/fields-parser.ts
|
|
1439
|
+
var import_core12 = require("@datrix/core");
|
|
1440
|
+
|
|
1441
|
+
// src/parser/errors.ts
|
|
1442
|
+
var import_core10 = require("@datrix/core");
|
|
1443
|
+
var import_core11 = require("@datrix/core");
|
|
1444
|
+
var whereError = {
|
|
1445
|
+
invalidOperator(operator, path, context) {
|
|
1446
|
+
throw new import_core10.ParserError(`Invalid WHERE operator: ${operator}`, {
|
|
1447
|
+
code: "INVALID_OPERATOR",
|
|
1448
|
+
parser: "where",
|
|
1449
|
+
location: (0, import_core10.buildErrorLocation)(["where", ...path], {
|
|
1450
|
+
queryParam: context?.operatorPath
|
|
1451
|
+
}),
|
|
1452
|
+
received: operator,
|
|
1453
|
+
expected: "One of: $eq, $ne, $gt, $gte, $lt, $lte, $in, $nin, $contains, $startsWith, $endsWith, $like, $ilike, $null, $notNull, $and, $or, $not",
|
|
1454
|
+
suggestion: "Use a valid WHERE operator. See documentation for full list.",
|
|
1455
|
+
context: {
|
|
1456
|
+
operator,
|
|
1457
|
+
...context
|
|
1458
|
+
}
|
|
1459
|
+
});
|
|
1460
|
+
},
|
|
1461
|
+
invalidFieldName(fieldName, path, context) {
|
|
1462
|
+
const reasonDetail = context?.fieldValidationReason ? ` (Reason: ${context.fieldValidationReason})` : "";
|
|
1463
|
+
throw new import_core10.ParserError(
|
|
1464
|
+
`Invalid field name in WHERE clause: ${fieldName}${reasonDetail}`,
|
|
1465
|
+
{
|
|
1466
|
+
code: "INVALID_FIELD_NAME",
|
|
1467
|
+
parser: "where",
|
|
1468
|
+
location: (0, import_core10.buildErrorLocation)(["where", ...path]),
|
|
1469
|
+
received: fieldName,
|
|
1470
|
+
expected: "Field name must start with letter/underscore and contain only alphanumeric characters, underscores, and dots",
|
|
1471
|
+
suggestion: "Use valid field names (e.g., 'name', 'user_id', 'profile.age')",
|
|
1472
|
+
context: {
|
|
1473
|
+
operator: fieldName,
|
|
1474
|
+
...context
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
);
|
|
1478
|
+
},
|
|
1479
|
+
invalidArrayIndex(index, operator, path, context) {
|
|
1480
|
+
throw new import_core10.ParserError(
|
|
1481
|
+
`Array index [${index}] can only follow array operators ($or, $and, $not, $in, $nin), found after: ${context?.previousOperator || "unknown"}`,
|
|
1482
|
+
{
|
|
1483
|
+
code: "ARRAY_INDEX_ERROR",
|
|
1484
|
+
parser: "where",
|
|
1485
|
+
location: (0, import_core10.buildErrorLocation)(["where", ...path], {
|
|
1486
|
+
index: parseInt(index, 10),
|
|
1487
|
+
queryParam: context?.operatorPath
|
|
1488
|
+
}),
|
|
1489
|
+
received: index,
|
|
1490
|
+
expected: "Array index after $or, $and, $not, $in, or $nin",
|
|
1491
|
+
suggestion: "Array indices can only be used with array operators",
|
|
1492
|
+
context: {
|
|
1493
|
+
operator,
|
|
1494
|
+
arrayIndex: parseInt(index, 10),
|
|
1495
|
+
...context
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
);
|
|
1499
|
+
},
|
|
1500
|
+
arrayIndexAtStart(index, _path) {
|
|
1501
|
+
throw new import_core10.ParserError(
|
|
1502
|
+
"Array index cannot appear at the beginning of WHERE clause",
|
|
1503
|
+
{
|
|
1504
|
+
code: "ARRAY_INDEX_ERROR",
|
|
1505
|
+
parser: "where",
|
|
1506
|
+
location: (0, import_core10.buildErrorLocation)(["where"], {
|
|
1507
|
+
index: parseInt(index, 10)
|
|
1508
|
+
}),
|
|
1509
|
+
received: index,
|
|
1510
|
+
expected: "Field name or operator before array index",
|
|
1511
|
+
suggestion: "WHERE clause must start with a field name, not an array index",
|
|
1512
|
+
context: {
|
|
1513
|
+
arrayIndex: parseInt(index, 10)
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
);
|
|
1517
|
+
},
|
|
1518
|
+
invalidArrayIndexFormat(index, operator, path) {
|
|
1519
|
+
throw new import_core10.ParserError(
|
|
1520
|
+
`Invalid array index in ${operator}: ${index} (must be non-negative integer)`,
|
|
1521
|
+
{
|
|
1522
|
+
code: "ARRAY_INDEX_ERROR",
|
|
1523
|
+
parser: "where",
|
|
1524
|
+
location: (0, import_core10.buildErrorLocation)(["where", ...path]),
|
|
1525
|
+
received: index,
|
|
1526
|
+
expected: "Non-negative integer (0, 1, 2, ...)",
|
|
1527
|
+
suggestion: "Use valid array indices starting from 0",
|
|
1528
|
+
context: {
|
|
1529
|
+
operator,
|
|
1530
|
+
arrayIndex: NaN
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
);
|
|
1534
|
+
},
|
|
1535
|
+
arrayIndexNotStartingFromZero(firstIndex, operator, path) {
|
|
1536
|
+
throw new import_core10.ParserError(
|
|
1537
|
+
`Array indices for ${operator} must start from 0, found: ${firstIndex}`,
|
|
1538
|
+
{
|
|
1539
|
+
code: "CONSECUTIVE_INDEX_ERROR",
|
|
1540
|
+
parser: "where",
|
|
1541
|
+
location: (0, import_core10.buildErrorLocation)(["where", ...path], {
|
|
1542
|
+
index: firstIndex
|
|
1543
|
+
}),
|
|
1544
|
+
received: firstIndex,
|
|
1545
|
+
expected: "Array indices starting from 0",
|
|
1546
|
+
suggestion: "Start array indices at 0: use [0], [1], [2], etc.",
|
|
1547
|
+
context: {
|
|
1548
|
+
operator,
|
|
1549
|
+
arrayIndex: firstIndex
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
);
|
|
1553
|
+
},
|
|
1554
|
+
arrayIndexNotConsecutive(missingIndex, operator, path, foundIndices) {
|
|
1555
|
+
const indicesStr = foundIndices ? `. Found: [${foundIndices.join(", ")}]` : "";
|
|
1556
|
+
throw new import_core10.ParserError(
|
|
1557
|
+
`Array indices for ${operator} must be consecutive. Missing index: ${missingIndex}${indicesStr}`,
|
|
1558
|
+
{
|
|
1559
|
+
code: "CONSECUTIVE_INDEX_ERROR",
|
|
1560
|
+
parser: "where",
|
|
1561
|
+
location: (0, import_core10.buildErrorLocation)(["where", ...path], {
|
|
1562
|
+
index: missingIndex
|
|
1563
|
+
}),
|
|
1564
|
+
received: foundIndices ? `Indices: [${foundIndices.join(", ")}]` : `Gap at index ${missingIndex}`,
|
|
1565
|
+
expected: "Consecutive indices: [0, 1, 2, ...]",
|
|
1566
|
+
suggestion: `Add ${operator}[${missingIndex}] to fix the gap`,
|
|
1567
|
+
context: {
|
|
1568
|
+
operator,
|
|
1569
|
+
missingIndex,
|
|
1570
|
+
foundIndices
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
);
|
|
1574
|
+
},
|
|
1575
|
+
maxValueLength(actualLength, path) {
|
|
1576
|
+
throw new import_core10.ParserError(
|
|
1577
|
+
`WHERE value exceeds maximum length of ${import_core11.MAX_WHERE_VALUE_LENGTH} characters`,
|
|
1578
|
+
{
|
|
1579
|
+
code: "MAX_LENGTH_EXCEEDED",
|
|
1580
|
+
parser: "where",
|
|
1581
|
+
location: (0, import_core10.buildErrorLocation)(["where", ...path]),
|
|
1582
|
+
received: `${actualLength} characters`,
|
|
1583
|
+
expected: `Maximum ${import_core11.MAX_WHERE_VALUE_LENGTH} characters`,
|
|
1584
|
+
suggestion: "Reduce the length of your query value or use a different approach",
|
|
1585
|
+
context: {
|
|
1586
|
+
operator: "value_length"
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
);
|
|
1590
|
+
},
|
|
1591
|
+
maxDepthExceeded(depth, path) {
|
|
1592
|
+
const pathStr = path.length > 0 ? ` at path: ${path.join(".")}` : "";
|
|
1593
|
+
throw new import_core10.ParserError(
|
|
1594
|
+
`WHERE clause nesting depth exceeds maximum of ${import_core11.MAX_LOGICAL_NESTING_DEPTH}${pathStr}`,
|
|
1595
|
+
{
|
|
1596
|
+
code: "MAX_DEPTH_EXCEEDED",
|
|
1597
|
+
parser: "where",
|
|
1598
|
+
location: (0, import_core10.buildErrorLocation)(["where", ...path], {
|
|
1599
|
+
depth
|
|
1600
|
+
}),
|
|
1601
|
+
received: `Depth: ${depth}${pathStr}`,
|
|
1602
|
+
expected: `Maximum depth: ${import_core11.MAX_LOGICAL_NESTING_DEPTH}`,
|
|
1603
|
+
suggestion: "Simplify query structure or split into multiple requests",
|
|
1604
|
+
context: {
|
|
1605
|
+
operator: "nesting_depth",
|
|
1606
|
+
currentPath: path.join("."),
|
|
1607
|
+
depth
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
);
|
|
1611
|
+
},
|
|
1612
|
+
emptyLogicalOperator(operator, path) {
|
|
1613
|
+
throw new import_core10.ParserError(
|
|
1614
|
+
`Logical operator ${operator} requires at least one condition`,
|
|
1615
|
+
{
|
|
1616
|
+
code: "EMPTY_VALUE",
|
|
1617
|
+
parser: "where",
|
|
1618
|
+
location: (0, import_core10.buildErrorLocation)(["where", ...path]),
|
|
1619
|
+
received: "empty array",
|
|
1620
|
+
expected: "At least one condition",
|
|
1621
|
+
suggestion: `Add at least one condition to ${operator} operator`,
|
|
1622
|
+
context: {
|
|
1623
|
+
operator
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
);
|
|
1627
|
+
},
|
|
1628
|
+
emptyArrayOperator(operator, path) {
|
|
1629
|
+
throw new import_core10.ParserError(`Operator ${operator} requires a non-empty array`, {
|
|
1630
|
+
code: "EMPTY_VALUE",
|
|
1631
|
+
parser: "where",
|
|
1632
|
+
location: (0, import_core10.buildErrorLocation)(["where", ...path]),
|
|
1633
|
+
received: "empty array",
|
|
1634
|
+
expected: "Non-empty array",
|
|
1635
|
+
suggestion: `Provide at least one value for ${operator} operator`,
|
|
1636
|
+
context: {
|
|
1637
|
+
operator
|
|
1638
|
+
}
|
|
1639
|
+
});
|
|
1640
|
+
},
|
|
1641
|
+
invalidOperatorValue(operator, valueType, path, receivedValue) {
|
|
1642
|
+
const valuePreview = receivedValue !== void 0 ? `: ${JSON.stringify(receivedValue).slice(0, 50)}` : "";
|
|
1643
|
+
throw new import_core10.ParserError(
|
|
1644
|
+
`Operator ${operator} requires array but received ${valueType}${valuePreview}`,
|
|
1645
|
+
{
|
|
1646
|
+
code: "INVALID_VALUE_TYPE",
|
|
1647
|
+
parser: "where",
|
|
1648
|
+
location: (0, import_core10.buildErrorLocation)(["where", ...path]),
|
|
1649
|
+
received: `${valueType}${valuePreview}`,
|
|
1650
|
+
expected: "array (e.g., [1, 2, 3])",
|
|
1651
|
+
suggestion: `Use array format: where[field][${operator}][0]=value1&where[field][${operator}][1]=value2`,
|
|
1652
|
+
context: {
|
|
1653
|
+
operator,
|
|
1654
|
+
receivedType: valueType
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
);
|
|
1658
|
+
}
|
|
1659
|
+
};
|
|
1660
|
+
var populateError = {
|
|
1661
|
+
invalidRelation(relation, path, context) {
|
|
1662
|
+
throw new import_core10.ParserError(`Invalid relation name: ${relation}`, {
|
|
1663
|
+
code: "INVALID_FIELD_NAME",
|
|
1664
|
+
parser: "populate",
|
|
1665
|
+
location: (0, import_core10.buildErrorLocation)(["populate", ...path], {
|
|
1666
|
+
depth: context?.currentDepth
|
|
1667
|
+
}),
|
|
1668
|
+
received: relation,
|
|
1669
|
+
expected: "Relation name must start with letter/underscore and contain only alphanumeric characters and underscores",
|
|
1670
|
+
suggestion: "Use valid relation names (e.g., 'author', 'user_profile')",
|
|
1671
|
+
context: {
|
|
1672
|
+
relation,
|
|
1673
|
+
...context
|
|
1674
|
+
}
|
|
1675
|
+
});
|
|
1676
|
+
},
|
|
1677
|
+
maxDepthExceeded(depth, maxDepth, path, context) {
|
|
1678
|
+
throw new import_core10.ParserError("Maximum populate depth exceeded", {
|
|
1679
|
+
code: "MAX_DEPTH_EXCEEDED",
|
|
1680
|
+
parser: "populate",
|
|
1681
|
+
location: (0, import_core10.buildErrorLocation)(["populate", ...path], {
|
|
1682
|
+
depth
|
|
1683
|
+
}),
|
|
1684
|
+
received: depth,
|
|
1685
|
+
expected: `Maximum depth: ${maxDepth}`,
|
|
1686
|
+
suggestion: "Reduce nesting level or increase maxPopulateDepth in parser options",
|
|
1687
|
+
context: {
|
|
1688
|
+
currentDepth: depth,
|
|
1689
|
+
maxDepth,
|
|
1690
|
+
relationPath: path.join("."),
|
|
1691
|
+
...context
|
|
1692
|
+
}
|
|
1693
|
+
});
|
|
1694
|
+
},
|
|
1695
|
+
emptyValue(path) {
|
|
1696
|
+
throw new import_core10.ParserError("Populate value cannot be empty", {
|
|
1697
|
+
code: "EMPTY_VALUE",
|
|
1698
|
+
parser: "populate",
|
|
1699
|
+
location: (0, import_core10.buildErrorLocation)(["populate", ...path]),
|
|
1700
|
+
received: "empty string",
|
|
1701
|
+
expected: "Relation name or wildcard (*)",
|
|
1702
|
+
suggestion: "Provide a relation name or use * to populate all relations",
|
|
1703
|
+
context: {}
|
|
1704
|
+
});
|
|
1705
|
+
},
|
|
1706
|
+
invalidType(type, path) {
|
|
1707
|
+
throw new import_core10.ParserError("Populate value must be a string or array", {
|
|
1708
|
+
code: "INVALID_VALUE_TYPE",
|
|
1709
|
+
parser: "populate",
|
|
1710
|
+
location: (0, import_core10.buildErrorLocation)(["populate", ...path]),
|
|
1711
|
+
received: type,
|
|
1712
|
+
expected: "string or array",
|
|
1713
|
+
suggestion: "Use a string (e.g., 'author') or array format",
|
|
1714
|
+
context: {}
|
|
1715
|
+
});
|
|
1716
|
+
},
|
|
1717
|
+
invalidFieldName(fieldName, path, context) {
|
|
1718
|
+
const reasonDetail = context?.fieldValidationReason ? ` (Reason: ${context.fieldValidationReason})` : "";
|
|
1719
|
+
throw new import_core10.ParserError(
|
|
1720
|
+
`Invalid field name in populate: ${fieldName}${reasonDetail}`,
|
|
1721
|
+
{
|
|
1722
|
+
code: "INVALID_FIELD_NAME",
|
|
1723
|
+
parser: "populate",
|
|
1724
|
+
location: (0, import_core10.buildErrorLocation)(["populate", ...path]),
|
|
1725
|
+
received: fieldName,
|
|
1726
|
+
expected: "Field name must start with letter/underscore and contain only alphanumeric characters, underscores, and dots",
|
|
1727
|
+
suggestion: "Use valid field names (e.g., 'name', 'user_id', 'profile.age')",
|
|
1728
|
+
context: {
|
|
1729
|
+
fieldName,
|
|
1730
|
+
...context
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
);
|
|
1734
|
+
}
|
|
1735
|
+
};
|
|
1736
|
+
var fieldsError = {
|
|
1737
|
+
invalidFieldNames(invalidFields, path, context) {
|
|
1738
|
+
const reasonDetail = context?.validationReasons && context.validationReasons.length > 0 ? ` (Reasons: ${context.validationReasons.join(", ")})` : "";
|
|
1739
|
+
throw new import_core10.ParserError(
|
|
1740
|
+
`Invalid field names: ${invalidFields.join(", ")}${reasonDetail}`,
|
|
1741
|
+
{
|
|
1742
|
+
code: "INVALID_FIELD_NAME",
|
|
1743
|
+
parser: "fields",
|
|
1744
|
+
location: (0, import_core10.buildErrorLocation)(["fields", ...path]),
|
|
1745
|
+
received: invalidFields,
|
|
1746
|
+
expected: "Field names must start with letter/underscore and contain only alphanumeric characters, underscores, and dots",
|
|
1747
|
+
suggestion: "Use valid field names (e.g., 'name', 'user_id', 'profile.age')",
|
|
1748
|
+
context: {
|
|
1749
|
+
invalidFields,
|
|
1750
|
+
...context
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
);
|
|
1754
|
+
},
|
|
1755
|
+
emptyValue(path) {
|
|
1756
|
+
throw new import_core10.ParserError(
|
|
1757
|
+
"Fields parameter is empty or contains only whitespace",
|
|
1758
|
+
{
|
|
1759
|
+
code: "EMPTY_VALUE",
|
|
1760
|
+
parser: "fields",
|
|
1761
|
+
location: (0, import_core10.buildErrorLocation)(["fields", ...path]),
|
|
1762
|
+
received: "empty string",
|
|
1763
|
+
expected: "Field name(s) or wildcard (*)",
|
|
1764
|
+
suggestion: "Provide field names (e.g., 'name,email') or use * for all fields",
|
|
1765
|
+
context: {}
|
|
1766
|
+
}
|
|
1767
|
+
);
|
|
1768
|
+
},
|
|
1769
|
+
suspiciousParams(params, path) {
|
|
1770
|
+
throw new import_core10.ParserError(`Unknown fields parameters: ${params.join(", ")}`, {
|
|
1771
|
+
code: "UNKNOWN_PARAMETER",
|
|
1772
|
+
parser: "fields",
|
|
1773
|
+
location: (0, import_core10.buildErrorLocation)(["fields", ...path]),
|
|
1774
|
+
received: params,
|
|
1775
|
+
expected: "fields or fields[N] format",
|
|
1776
|
+
suggestion: "Use 'fields=name,email' or 'fields[0]=name&fields[1]=email' format",
|
|
1777
|
+
context: {
|
|
1778
|
+
suspiciousParams: params
|
|
1779
|
+
}
|
|
1780
|
+
});
|
|
1781
|
+
},
|
|
1782
|
+
invalidFormat(path) {
|
|
1783
|
+
throw new import_core10.ParserError("Invalid fields format", {
|
|
1784
|
+
code: "INVALID_SYNTAX",
|
|
1785
|
+
parser: "fields",
|
|
1786
|
+
location: (0, import_core10.buildErrorLocation)(["fields", ...path]),
|
|
1787
|
+
received: "unknown format",
|
|
1788
|
+
expected: "string or array",
|
|
1789
|
+
suggestion: "Use 'fields=name,email' or 'fields[0]=name' format",
|
|
1790
|
+
context: {}
|
|
1791
|
+
});
|
|
1792
|
+
}
|
|
1793
|
+
};
|
|
1794
|
+
var paginationError = {
|
|
1795
|
+
invalidLimit(value, path, context) {
|
|
1796
|
+
throw new import_core10.ParserError(`Invalid limit value: "${value}"`, {
|
|
1797
|
+
code: "INVALID_PAGINATION",
|
|
1798
|
+
parser: "pagination",
|
|
1799
|
+
location: (0, import_core10.buildErrorLocation)(["pagination", ...path]),
|
|
1800
|
+
received: value,
|
|
1801
|
+
expected: "Positive integer",
|
|
1802
|
+
suggestion: "Provide a positive integer for limit (e.g., limit=10)",
|
|
1803
|
+
context: {
|
|
1804
|
+
parameter: "limit",
|
|
1805
|
+
...context
|
|
1806
|
+
}
|
|
1807
|
+
});
|
|
1808
|
+
},
|
|
1809
|
+
invalidOffset(value, path, context) {
|
|
1810
|
+
throw new import_core10.ParserError(`Invalid offset value: "${value}"`, {
|
|
1811
|
+
code: "INVALID_PAGINATION",
|
|
1812
|
+
parser: "pagination",
|
|
1813
|
+
location: (0, import_core10.buildErrorLocation)(["pagination", ...path]),
|
|
1814
|
+
received: value,
|
|
1815
|
+
expected: "Non-negative integer",
|
|
1816
|
+
suggestion: "Provide a non-negative integer for offset (e.g., offset=0)",
|
|
1817
|
+
context: {
|
|
1818
|
+
parameter: "offset",
|
|
1819
|
+
...context
|
|
1820
|
+
}
|
|
1821
|
+
});
|
|
1822
|
+
},
|
|
1823
|
+
invalidPage(value, path, context) {
|
|
1824
|
+
throw new import_core10.ParserError(`Invalid page value: "${value}" (must be >= 1)`, {
|
|
1825
|
+
code: "INVALID_PAGINATION",
|
|
1826
|
+
parser: "pagination",
|
|
1827
|
+
location: (0, import_core10.buildErrorLocation)(["pagination", ...path]),
|
|
1828
|
+
received: value,
|
|
1829
|
+
expected: "Integer >= 1",
|
|
1830
|
+
suggestion: "Provide a positive integer for page (e.g., page=1)",
|
|
1831
|
+
context: {
|
|
1832
|
+
parameter: "page",
|
|
1833
|
+
minValue: 1,
|
|
1834
|
+
...context
|
|
1835
|
+
}
|
|
1836
|
+
});
|
|
1837
|
+
},
|
|
1838
|
+
invalidPageSize(value, path, context) {
|
|
1839
|
+
throw new import_core10.ParserError(`Invalid pageSize value: "${value}" (must be >= 1)`, {
|
|
1840
|
+
code: "INVALID_PAGINATION",
|
|
1841
|
+
parser: "pagination",
|
|
1842
|
+
location: (0, import_core10.buildErrorLocation)(["pagination", ...path]),
|
|
1843
|
+
received: value,
|
|
1844
|
+
expected: "Integer >= 1",
|
|
1845
|
+
suggestion: "Provide a positive integer for pageSize (e.g., pageSize=25)",
|
|
1846
|
+
context: {
|
|
1847
|
+
parameter: "pageSize",
|
|
1848
|
+
minValue: 1,
|
|
1849
|
+
...context
|
|
1850
|
+
}
|
|
1851
|
+
});
|
|
1852
|
+
},
|
|
1853
|
+
maxPageSizeExceeded(value, max, path) {
|
|
1854
|
+
throw new import_core10.ParserError(`Page size exceeds maximum (${max})`, {
|
|
1855
|
+
code: "MAX_VALUE_VIOLATION",
|
|
1856
|
+
parser: "pagination",
|
|
1857
|
+
location: (0, import_core10.buildErrorLocation)(["pagination", ...path]),
|
|
1858
|
+
received: value,
|
|
1859
|
+
expected: `Maximum: ${max}`,
|
|
1860
|
+
suggestion: `Reduce pageSize to ${max} or less`,
|
|
1861
|
+
context: {
|
|
1862
|
+
parameter: "pageSize",
|
|
1863
|
+
maxValue: max
|
|
1864
|
+
}
|
|
1865
|
+
});
|
|
1866
|
+
},
|
|
1867
|
+
maxLimitExceeded(value, max, path) {
|
|
1868
|
+
throw new import_core10.ParserError(`Limit exceeds maximum page size (${max})`, {
|
|
1869
|
+
code: "MAX_VALUE_VIOLATION",
|
|
1870
|
+
parser: "pagination",
|
|
1871
|
+
location: (0, import_core10.buildErrorLocation)(["pagination", ...path]),
|
|
1872
|
+
received: value,
|
|
1873
|
+
expected: `Maximum: ${max}`,
|
|
1874
|
+
suggestion: `Reduce limit to ${max} or less`,
|
|
1875
|
+
context: {
|
|
1876
|
+
parameter: "limit",
|
|
1877
|
+
maxValue: max
|
|
1878
|
+
}
|
|
1879
|
+
});
|
|
1880
|
+
},
|
|
1881
|
+
maxPageNumberExceeded(value, max, path) {
|
|
1882
|
+
throw new import_core10.ParserError(`Page number exceeds maximum (${max})`, {
|
|
1883
|
+
code: "PAGE_OUT_OF_RANGE",
|
|
1884
|
+
parser: "pagination",
|
|
1885
|
+
location: (0, import_core10.buildErrorLocation)(["pagination", ...path]),
|
|
1886
|
+
received: value,
|
|
1887
|
+
expected: `Maximum: ${max}`,
|
|
1888
|
+
suggestion: `Use page number ${max} or less`,
|
|
1889
|
+
context: {
|
|
1890
|
+
parameter: "page",
|
|
1891
|
+
maxValue: max
|
|
1892
|
+
}
|
|
1893
|
+
});
|
|
1894
|
+
}
|
|
1895
|
+
};
|
|
1896
|
+
var sortError = {
|
|
1897
|
+
emptyValue(path) {
|
|
1898
|
+
throw new import_core10.ParserError("Sort value cannot be empty", {
|
|
1899
|
+
code: "EMPTY_VALUE",
|
|
1900
|
+
parser: "sort",
|
|
1901
|
+
location: (0, import_core10.buildErrorLocation)(["sort", ...path]),
|
|
1902
|
+
received: "empty string",
|
|
1903
|
+
expected: "Field name(s) with optional direction",
|
|
1904
|
+
suggestion: "Provide field names (e.g., 'name' or '-createdAt' for descending)",
|
|
1905
|
+
context: {}
|
|
1906
|
+
});
|
|
1907
|
+
},
|
|
1908
|
+
invalidFieldName(field, path, context) {
|
|
1909
|
+
const reasonDetail = context?.fieldValidationReason ? ` (Reason: ${context.fieldValidationReason})` : "";
|
|
1910
|
+
throw new import_core10.ParserError(`Invalid sort field: ${field}${reasonDetail}`, {
|
|
1911
|
+
code: "INVALID_FIELD_NAME",
|
|
1912
|
+
parser: "sort",
|
|
1913
|
+
location: (0, import_core10.buildErrorLocation)(["sort", ...path]),
|
|
1914
|
+
received: field,
|
|
1915
|
+
expected: "Field name must start with letter/underscore and contain only alphanumeric characters, underscores, and dots. Use '-' prefix for descending order.",
|
|
1916
|
+
suggestion: "Use valid field names (e.g., 'name', '-createdAt', 'user.age')",
|
|
1917
|
+
context: {
|
|
1918
|
+
sortField: field,
|
|
1919
|
+
parameter: "sort",
|
|
1920
|
+
...context
|
|
1921
|
+
}
|
|
1922
|
+
});
|
|
1923
|
+
}
|
|
1924
|
+
};
|
|
1925
|
+
|
|
1926
|
+
// src/parser/fields-parser.ts
|
|
1927
|
+
function parseFields(params) {
|
|
1928
|
+
const suspiciousParams = Object.keys(params).filter(
|
|
1929
|
+
(key) => key.startsWith("fields") && key !== "fields" && !key.match(/^fields\[\d+\]$/)
|
|
1930
|
+
// Allow fields[0], fields[1], etc.
|
|
1931
|
+
);
|
|
1932
|
+
if (suspiciousParams.length > 0) {
|
|
1933
|
+
fieldsError.suspiciousParams(suspiciousParams, []);
|
|
1934
|
+
}
|
|
1935
|
+
const arrayFields = extractArrayFields(params);
|
|
1936
|
+
if (arrayFields.length > 0) {
|
|
1937
|
+
return validateAndReturn(arrayFields);
|
|
1938
|
+
}
|
|
1939
|
+
const fieldsParam = params["fields"];
|
|
1940
|
+
if (fieldsParam === void 0) {
|
|
1941
|
+
return "*";
|
|
1942
|
+
}
|
|
1943
|
+
if (fieldsParam === "*") {
|
|
1944
|
+
return "*";
|
|
1945
|
+
}
|
|
1946
|
+
if (typeof fieldsParam === "string") {
|
|
1947
|
+
const fields = fieldsParam.split(",").map((f) => f.trim()).filter(Boolean);
|
|
1948
|
+
if (fields.length === 0) {
|
|
1949
|
+
fieldsError.emptyValue([]);
|
|
1950
|
+
}
|
|
1951
|
+
return validateAndReturn(fields);
|
|
1952
|
+
}
|
|
1953
|
+
if (Array.isArray(fieldsParam)) {
|
|
1954
|
+
const fields = fieldsParam.map((f) => String(f).trim()).filter(Boolean);
|
|
1955
|
+
if (fields.length === 0) {
|
|
1956
|
+
fieldsError.emptyValue([]);
|
|
1957
|
+
}
|
|
1958
|
+
return validateAndReturn(fields);
|
|
1959
|
+
}
|
|
1960
|
+
fieldsError.invalidFormat([]);
|
|
1961
|
+
return void 0;
|
|
1962
|
+
}
|
|
1963
|
+
function extractArrayFields(params) {
|
|
1964
|
+
const fields = [];
|
|
1965
|
+
for (const key in params) {
|
|
1966
|
+
const match = key.match(/^fields\[(\d+)\]$/);
|
|
1967
|
+
if (!match) continue;
|
|
1968
|
+
const index = parseInt(match[1], 10);
|
|
1969
|
+
if (index >= import_core12.MAX_ARRAY_INDEX) {
|
|
1970
|
+
continue;
|
|
1971
|
+
}
|
|
1972
|
+
const value = params[key];
|
|
1973
|
+
if (typeof value === "string") {
|
|
1974
|
+
fields.push(value.trim());
|
|
1975
|
+
} else if (Array.isArray(value)) {
|
|
1976
|
+
fields.push(...value.map((v) => String(v).trim()));
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
return fields;
|
|
1980
|
+
}
|
|
1981
|
+
function validateAndReturn(fields) {
|
|
1982
|
+
if (fields.length === 0) {
|
|
1983
|
+
return "*";
|
|
1984
|
+
}
|
|
1985
|
+
const invalidFieldsWithReasons = [];
|
|
1986
|
+
for (const field of fields) {
|
|
1987
|
+
const validation = (0, import_core12.validateFieldName)(field);
|
|
1988
|
+
if (!validation.valid) {
|
|
1989
|
+
invalidFieldsWithReasons.push({ field, reason: validation.reason });
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
if (invalidFieldsWithReasons.length > 0) {
|
|
1993
|
+
const invalidFields = invalidFieldsWithReasons.map((item) => item.field);
|
|
1994
|
+
const reasons = invalidFieldsWithReasons.map((item) => item.reason);
|
|
1995
|
+
fieldsError.invalidFieldNames(invalidFields, [], {
|
|
1996
|
+
validationReasons: reasons
|
|
1997
|
+
});
|
|
1998
|
+
}
|
|
1999
|
+
return fields;
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
// src/parser/where-parser.ts
|
|
2003
|
+
var import_core13 = require("@datrix/core");
|
|
2004
|
+
function parseWhere(params) {
|
|
2005
|
+
const whereClause = {};
|
|
2006
|
+
for (const [key, value] of Object.entries(params)) {
|
|
2007
|
+
if (!key.startsWith("where[")) {
|
|
2008
|
+
continue;
|
|
2009
|
+
}
|
|
2010
|
+
const parts = key.slice(5).split("]").filter((p) => p.startsWith("[")).map((p) => p.slice(1));
|
|
2011
|
+
if (parts.length === 0) continue;
|
|
2012
|
+
for (let i = 0; i < parts.length; i++) {
|
|
2013
|
+
const part = parts[i];
|
|
2014
|
+
if (part.startsWith("$")) {
|
|
2015
|
+
if (!(0, import_core13.isValidWhereOperator)(part)) {
|
|
2016
|
+
whereError.invalidOperator(part, parts.slice(0, i), {
|
|
2017
|
+
operatorPath: key
|
|
2018
|
+
});
|
|
2019
|
+
}
|
|
2020
|
+
if (i === 0 && !(0, import_core13.isLogicalOperator)(part)) {
|
|
2021
|
+
whereError.invalidFieldName(part, [], {
|
|
2022
|
+
fieldValidationReason: "INVALID_FORMAT"
|
|
2023
|
+
});
|
|
2024
|
+
}
|
|
2025
|
+
} else if (/^\d+$/.test(part)) {
|
|
2026
|
+
if (i === 0) {
|
|
2027
|
+
whereError.arrayIndexAtStart(part, []);
|
|
2028
|
+
}
|
|
2029
|
+
const previousPart = parts[i - 1];
|
|
2030
|
+
if (!["$or", "$and", "$in", "$nin"].includes(previousPart)) {
|
|
2031
|
+
whereError.invalidArrayIndex(part, previousPart, parts.slice(0, i), {
|
|
2032
|
+
previousOperator: previousPart,
|
|
2033
|
+
operatorPath: key
|
|
2034
|
+
});
|
|
2035
|
+
}
|
|
2036
|
+
} else {
|
|
2037
|
+
const validation = (0, import_core13.validateFieldName)(part);
|
|
2038
|
+
if (!validation.valid) {
|
|
2039
|
+
whereError.invalidFieldName(part, parts.slice(0, i), {
|
|
2040
|
+
fieldValidationReason: validation.reason
|
|
2041
|
+
});
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
let current = whereClause;
|
|
2046
|
+
const pathParts = [...parts];
|
|
2047
|
+
for (let i = 0; i < pathParts.length; i++) {
|
|
2048
|
+
const part = pathParts[i];
|
|
2049
|
+
const isLast = i === pathParts.length - 1;
|
|
2050
|
+
if (isLast) {
|
|
2051
|
+
let operatorContext;
|
|
2052
|
+
const isArrayIndex = /^\d+$/.test(part);
|
|
2053
|
+
if (part.startsWith("$")) {
|
|
2054
|
+
const expectedType = (0, import_core13.getOperatorValueType)(part);
|
|
2055
|
+
if (expectedType === "string") {
|
|
2056
|
+
operatorContext = part;
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
const parsedValue = parseValue(value, operatorContext);
|
|
2060
|
+
if (part.startsWith("$") && !isArrayIndex) {
|
|
2061
|
+
validateOperatorValue(part, parsedValue, pathParts);
|
|
2062
|
+
}
|
|
2063
|
+
current[part] = parsedValue;
|
|
2064
|
+
} else {
|
|
2065
|
+
if (current[part] === void 0) {
|
|
2066
|
+
current[part] = {};
|
|
2067
|
+
}
|
|
2068
|
+
current = current[part];
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
const transformResult = transformToFinalWhere(whereClause);
|
|
2073
|
+
const finalClause = transformResult;
|
|
2074
|
+
if (Object.keys(finalClause).length === 0) {
|
|
2075
|
+
return void 0;
|
|
2076
|
+
}
|
|
2077
|
+
validateNestingDepth(finalClause);
|
|
2078
|
+
return finalClause;
|
|
2079
|
+
}
|
|
2080
|
+
function transformToFinalWhere(obj) {
|
|
2081
|
+
if (obj === null || typeof obj !== "object" || Array.isArray(obj)) {
|
|
2082
|
+
return obj;
|
|
2083
|
+
}
|
|
2084
|
+
const typedObj = obj;
|
|
2085
|
+
const result = {};
|
|
2086
|
+
for (const [key, value] of Object.entries(typedObj)) {
|
|
2087
|
+
const arrayOperators = ["$or", "$and", "$in", "$nin"];
|
|
2088
|
+
if (arrayOperators.includes(key)) {
|
|
2089
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
2090
|
+
const valueObj = value;
|
|
2091
|
+
const keys = Object.keys(valueObj);
|
|
2092
|
+
const numericKeys = [];
|
|
2093
|
+
for (const k of keys) {
|
|
2094
|
+
const num = Number(k);
|
|
2095
|
+
if (isNaN(num) || !Number.isInteger(num) || num < 0) {
|
|
2096
|
+
whereError.invalidArrayIndexFormat(k, key, [key]);
|
|
2097
|
+
}
|
|
2098
|
+
numericKeys.push(num);
|
|
2099
|
+
}
|
|
2100
|
+
const sortedKeys = numericKeys.sort((a, b) => a - b);
|
|
2101
|
+
if (sortedKeys.length > 0 && sortedKeys[0] !== 0) {
|
|
2102
|
+
whereError.arrayIndexNotStartingFromZero(sortedKeys[0], key, [key]);
|
|
2103
|
+
}
|
|
2104
|
+
for (let i = 0; i < sortedKeys.length; i++) {
|
|
2105
|
+
if (sortedKeys[i] !== i) {
|
|
2106
|
+
whereError.arrayIndexNotConsecutive(i, key, [key], sortedKeys);
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
if (["$in", "$nin"].includes(key)) {
|
|
2110
|
+
result[key] = sortedKeys.map((idx) => valueObj[String(idx)]);
|
|
2111
|
+
} else {
|
|
2112
|
+
const transformed = [];
|
|
2113
|
+
for (const idx of sortedKeys) {
|
|
2114
|
+
const transformResult = transformToFinalWhere(
|
|
2115
|
+
valueObj[String(idx)]
|
|
2116
|
+
);
|
|
2117
|
+
transformed.push(transformResult);
|
|
2118
|
+
}
|
|
2119
|
+
result[key] = transformed;
|
|
2120
|
+
}
|
|
2121
|
+
} else {
|
|
2122
|
+
const transformResult = transformToFinalWhere(value);
|
|
2123
|
+
result[key] = transformResult;
|
|
2124
|
+
}
|
|
2125
|
+
} else {
|
|
2126
|
+
const transformResult = transformToFinalWhere(value);
|
|
2127
|
+
result[key] = transformResult;
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
return result;
|
|
2131
|
+
}
|
|
2132
|
+
function parseValue(value, operator) {
|
|
2133
|
+
if (value === void 0) {
|
|
2134
|
+
return void 0;
|
|
2135
|
+
}
|
|
2136
|
+
if (Array.isArray(value)) {
|
|
2137
|
+
const parsed = [];
|
|
2138
|
+
for (const v of value) {
|
|
2139
|
+
if (typeof v === "string") {
|
|
2140
|
+
const result = parseSingleValue(v, operator);
|
|
2141
|
+
parsed.push(result);
|
|
2142
|
+
} else {
|
|
2143
|
+
parsed.push(v);
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
return parsed;
|
|
2147
|
+
}
|
|
2148
|
+
if (typeof value === "string") {
|
|
2149
|
+
return parseSingleValue(value, operator);
|
|
2150
|
+
}
|
|
2151
|
+
return value;
|
|
2152
|
+
}
|
|
2153
|
+
function parseSingleValue(value, operator) {
|
|
2154
|
+
const MAX_WHERE_VALUE_LENGTH2 = 1e3;
|
|
2155
|
+
if (value.length > MAX_WHERE_VALUE_LENGTH2) {
|
|
2156
|
+
whereError.maxValueLength(value.length, []);
|
|
2157
|
+
}
|
|
2158
|
+
if (operator) {
|
|
2159
|
+
const expectedType = (0, import_core13.getOperatorValueType)(operator);
|
|
2160
|
+
if (expectedType === "string") {
|
|
2161
|
+
return value;
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
if (value === "null") {
|
|
2165
|
+
return null;
|
|
2166
|
+
}
|
|
2167
|
+
if (value === "true") {
|
|
2168
|
+
return true;
|
|
2169
|
+
}
|
|
2170
|
+
if (value === "false") {
|
|
2171
|
+
return false;
|
|
2172
|
+
}
|
|
2173
|
+
return value;
|
|
2174
|
+
}
|
|
2175
|
+
function validateOperatorValue(operator, value, path) {
|
|
2176
|
+
const expectedType = (0, import_core13.getOperatorValueType)(operator);
|
|
2177
|
+
if (!expectedType) {
|
|
2178
|
+
return;
|
|
2179
|
+
}
|
|
2180
|
+
if (expectedType === "array") {
|
|
2181
|
+
if (!Array.isArray(value)) {
|
|
2182
|
+
whereError.invalidOperatorValue(operator, typeof value, path, value);
|
|
2183
|
+
}
|
|
2184
|
+
if (value.length === 0) {
|
|
2185
|
+
whereError.emptyArrayOperator(operator, path);
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
function validateNestingDepth(clause, depth = 0, path = []) {
|
|
2190
|
+
const MAX_LOGICAL_NESTING_DEPTH2 = 10;
|
|
2191
|
+
if (depth > MAX_LOGICAL_NESTING_DEPTH2) {
|
|
2192
|
+
whereError.maxDepthExceeded(depth, path);
|
|
2193
|
+
}
|
|
2194
|
+
for (const [key, value] of Object.entries(clause)) {
|
|
2195
|
+
if ((0, import_core13.isLogicalOperator)(key) && Array.isArray(value)) {
|
|
2196
|
+
if (value.length === 0) {
|
|
2197
|
+
whereError.emptyLogicalOperator(key, [...path, key]);
|
|
2198
|
+
}
|
|
2199
|
+
for (const condition of value) {
|
|
2200
|
+
if (typeof condition === "object" && condition !== null) {
|
|
2201
|
+
validateNestingDepth(condition, depth + 1, [
|
|
2202
|
+
...path,
|
|
2203
|
+
key
|
|
2204
|
+
]);
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
} else if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
2208
|
+
validateNestingDepth(value, depth, [...path, key]);
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
|
|
2213
|
+
// src/parser/populate-parser.ts
|
|
2214
|
+
var import_core14 = require("@datrix/core");
|
|
2215
|
+
var DEFAULT_MAX_DEPTH = 5;
|
|
2216
|
+
function parsePopulate(params, maxDepth = DEFAULT_MAX_DEPTH) {
|
|
2217
|
+
if (maxDepth <= 0) {
|
|
2218
|
+
populateError.maxDepthExceeded(maxDepth, maxDepth, ["config"], {
|
|
2219
|
+
maxDepth
|
|
2220
|
+
});
|
|
2221
|
+
}
|
|
2222
|
+
const populateClause = {};
|
|
2223
|
+
const mainPopulate = params["populate"];
|
|
2224
|
+
if (mainPopulate !== void 0) {
|
|
2225
|
+
if (mainPopulate === "*") {
|
|
2226
|
+
return "*";
|
|
2227
|
+
}
|
|
2228
|
+
if (mainPopulate === "true") {
|
|
2229
|
+
return true;
|
|
2230
|
+
}
|
|
2231
|
+
if (typeof mainPopulate === "string") {
|
|
2232
|
+
const trimmed = mainPopulate.trim();
|
|
2233
|
+
if (trimmed === "") {
|
|
2234
|
+
populateError.emptyValue([]);
|
|
2235
|
+
}
|
|
2236
|
+
const validation = (0, import_core14.validateFieldName)(trimmed);
|
|
2237
|
+
if (!validation.valid) {
|
|
2238
|
+
populateError.invalidRelation(trimmed, [trimmed], {
|
|
2239
|
+
fieldValidationReason: validation.reason
|
|
2240
|
+
});
|
|
2241
|
+
}
|
|
2242
|
+
populateClause[trimmed] = "*";
|
|
2243
|
+
} else if (Array.isArray(mainPopulate)) {
|
|
2244
|
+
for (const rel of mainPopulate) {
|
|
2245
|
+
if (rel && typeof rel === "string") {
|
|
2246
|
+
const trimmed = rel.trim();
|
|
2247
|
+
const validation = (0, import_core14.validateFieldName)(trimmed);
|
|
2248
|
+
if (!validation.valid) {
|
|
2249
|
+
populateError.invalidRelation(trimmed, [trimmed], {
|
|
2250
|
+
fieldValidationReason: validation.reason
|
|
2251
|
+
});
|
|
2252
|
+
}
|
|
2253
|
+
populateClause[trimmed] = "*";
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
} else {
|
|
2257
|
+
populateError.invalidType(typeof mainPopulate, []);
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
const populateParams = extractPopulateParams(params);
|
|
2261
|
+
const indexedArrayRelations = [];
|
|
2262
|
+
for (const [key, value] of Object.entries(params)) {
|
|
2263
|
+
const indexMatch = key.match(/^populate\[(\d+)\]$/);
|
|
2264
|
+
if (indexMatch && typeof value === "string") {
|
|
2265
|
+
const index = Number(indexMatch[1]);
|
|
2266
|
+
const relationName = value.trim();
|
|
2267
|
+
const validation = (0, import_core14.validateFieldName)(relationName);
|
|
2268
|
+
if (!validation.valid) {
|
|
2269
|
+
populateError.invalidRelation(relationName, [relationName], {
|
|
2270
|
+
fieldValidationReason: validation.reason
|
|
2271
|
+
});
|
|
2272
|
+
}
|
|
2273
|
+
indexedArrayRelations[index] = relationName;
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
if (indexedArrayRelations.length > 0) {
|
|
2277
|
+
return indexedArrayRelations.filter(
|
|
2278
|
+
Boolean
|
|
2279
|
+
);
|
|
2280
|
+
}
|
|
2281
|
+
for (const [relation, relationParams] of Object.entries(populateParams)) {
|
|
2282
|
+
const parseResult = parseRelation(relation, relationParams, 1, maxDepth);
|
|
2283
|
+
populateClause[relation] = parseResult;
|
|
2284
|
+
}
|
|
2285
|
+
if (Object.keys(populateClause).length === 0) {
|
|
2286
|
+
return void 0;
|
|
2287
|
+
}
|
|
2288
|
+
return populateClause;
|
|
2289
|
+
}
|
|
2290
|
+
function extractPopulateParams(params) {
|
|
2291
|
+
const relations = {};
|
|
2292
|
+
for (const [key, value] of Object.entries(params)) {
|
|
2293
|
+
if (!key.startsWith("populate[")) {
|
|
2294
|
+
continue;
|
|
2295
|
+
}
|
|
2296
|
+
const parts = key.match(/populate\[([^\]]+)\](.*)$/);
|
|
2297
|
+
if (!parts) {
|
|
2298
|
+
continue;
|
|
2299
|
+
}
|
|
2300
|
+
const relation = parts[1];
|
|
2301
|
+
const rest = parts[2];
|
|
2302
|
+
if (!relation) {
|
|
2303
|
+
continue;
|
|
2304
|
+
}
|
|
2305
|
+
if (relations[relation] === void 0) {
|
|
2306
|
+
relations[relation] = {};
|
|
2307
|
+
}
|
|
2308
|
+
if (rest === "" && value === "*") {
|
|
2309
|
+
relations[relation]["isWildcard"] = true;
|
|
2310
|
+
continue;
|
|
2311
|
+
}
|
|
2312
|
+
if (rest !== void 0) {
|
|
2313
|
+
parseRelationPath(relations[relation], rest, value);
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
return relations;
|
|
2317
|
+
}
|
|
2318
|
+
function parseRelationPath(relationData, path, value) {
|
|
2319
|
+
if (path === "") {
|
|
2320
|
+
return;
|
|
2321
|
+
}
|
|
2322
|
+
const match = path.match(/^\[([^\]]+)\](.*)$/);
|
|
2323
|
+
if (!match) {
|
|
2324
|
+
return;
|
|
2325
|
+
}
|
|
2326
|
+
const key = match[1];
|
|
2327
|
+
const rest = match[2];
|
|
2328
|
+
if (key === "fields") {
|
|
2329
|
+
if (relationData["fields"] === void 0) {
|
|
2330
|
+
relationData["fields"] = [];
|
|
2331
|
+
}
|
|
2332
|
+
const fieldsArray = Array.isArray(relationData["fields"]) ? relationData["fields"] : [];
|
|
2333
|
+
if (rest === "") {
|
|
2334
|
+
if (value === "*") {
|
|
2335
|
+
relationData["fields"] = "*";
|
|
2336
|
+
} else if (typeof value === "string") {
|
|
2337
|
+
const fields = value.split(",").map((f) => f.trim());
|
|
2338
|
+
for (const field of fields) {
|
|
2339
|
+
const validation = (0, import_core14.validateFieldName)(field);
|
|
2340
|
+
if (!validation.valid) {
|
|
2341
|
+
populateError.invalidFieldName(field, ["fields"], {
|
|
2342
|
+
fieldValidationReason: validation.reason
|
|
2343
|
+
});
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
fieldsArray.push(...fields);
|
|
2347
|
+
}
|
|
2348
|
+
} else if (rest !== void 0) {
|
|
2349
|
+
const indexMatch = rest.match(/^\[(\d+)\]$/);
|
|
2350
|
+
if (indexMatch && typeof value === "string") {
|
|
2351
|
+
const field = value.trim();
|
|
2352
|
+
const validation = (0, import_core14.validateFieldName)(field);
|
|
2353
|
+
if (!validation.valid) {
|
|
2354
|
+
populateError.invalidFieldName(field, ["fields"], {
|
|
2355
|
+
fieldValidationReason: validation.reason
|
|
2356
|
+
});
|
|
2357
|
+
}
|
|
2358
|
+
fieldsArray.push(field);
|
|
2359
|
+
}
|
|
2360
|
+
}
|
|
2361
|
+
} else if (key === "populate") {
|
|
2362
|
+
if (relationData["populate"] === void 0) {
|
|
2363
|
+
relationData["populate"] = {};
|
|
2364
|
+
}
|
|
2365
|
+
const populateObj = typeof relationData["populate"] === "object" && !Array.isArray(relationData["populate"]) ? relationData["populate"] : {};
|
|
2366
|
+
relationData["populate"] = populateObj;
|
|
2367
|
+
if (rest === "") {
|
|
2368
|
+
if (value === "*") {
|
|
2369
|
+
relationData["isWildcard"] = true;
|
|
2370
|
+
} else if (typeof value === "string") {
|
|
2371
|
+
const relations = value.split(",").map((r) => r.trim()).filter(Boolean);
|
|
2372
|
+
for (const rel of relations) {
|
|
2373
|
+
if (rel === "*") {
|
|
2374
|
+
relationData["isWildcard"] = true;
|
|
2375
|
+
} else if (populateObj[rel] === void 0) {
|
|
2376
|
+
populateObj[rel] = { isWildcard: true };
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
}
|
|
2380
|
+
} else if (rest !== void 0) {
|
|
2381
|
+
const nestedMatch = rest.match(/^\[([^\]]+)\](.*)$/);
|
|
2382
|
+
if (nestedMatch) {
|
|
2383
|
+
const nestedRelation = nestedMatch[1];
|
|
2384
|
+
const nestedRest = nestedMatch[2];
|
|
2385
|
+
if (nestedRelation) {
|
|
2386
|
+
if (populateObj[nestedRelation] === void 0) {
|
|
2387
|
+
populateObj[nestedRelation] = {};
|
|
2388
|
+
}
|
|
2389
|
+
if (nestedRest === "" && value === "*") {
|
|
2390
|
+
populateObj[nestedRelation]["isWildcard"] = true;
|
|
2391
|
+
} else if (nestedRest !== void 0) {
|
|
2392
|
+
parseRelationPath(populateObj[nestedRelation], nestedRest, value);
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
function parseRelation(relation, params, currentDepth, maxDepth, path = []) {
|
|
2400
|
+
const validation = (0, import_core14.validateFieldName)(relation);
|
|
2401
|
+
if (!validation.valid) {
|
|
2402
|
+
populateError.invalidRelation(relation, [...path, relation], {
|
|
2403
|
+
relationPath: [...path, relation].join("."),
|
|
2404
|
+
fieldValidationReason: validation.reason
|
|
2405
|
+
});
|
|
2406
|
+
}
|
|
2407
|
+
if (currentDepth > maxDepth) {
|
|
2408
|
+
populateError.maxDepthExceeded(
|
|
2409
|
+
currentDepth,
|
|
2410
|
+
maxDepth,
|
|
2411
|
+
[...path, relation],
|
|
2412
|
+
{
|
|
2413
|
+
relation,
|
|
2414
|
+
relationPath: [...path, relation].join("."),
|
|
2415
|
+
currentDepth,
|
|
2416
|
+
nestedRelations: [...path, relation]
|
|
2417
|
+
}
|
|
2418
|
+
);
|
|
2419
|
+
}
|
|
2420
|
+
if (params.isWildcard) {
|
|
2421
|
+
return "*";
|
|
2422
|
+
}
|
|
2423
|
+
const options = {};
|
|
2424
|
+
if (params.fields !== void 0) {
|
|
2425
|
+
if (typeof params.fields === "string" && params.fields === "*") {
|
|
2426
|
+
options["select"] = "*";
|
|
2427
|
+
} else if (Array.isArray(params.fields) && params.fields.length > 0) {
|
|
2428
|
+
options["select"] = params.fields;
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
if (params.populate !== void 0) {
|
|
2432
|
+
const nestedPopulate = {};
|
|
2433
|
+
for (const [nestedRelation, nestedParams] of Object.entries(
|
|
2434
|
+
params.populate
|
|
2435
|
+
)) {
|
|
2436
|
+
nestedPopulate[nestedRelation] = parseRelation(
|
|
2437
|
+
nestedRelation,
|
|
2438
|
+
nestedParams,
|
|
2439
|
+
currentDepth + 1,
|
|
2440
|
+
maxDepth,
|
|
2441
|
+
[...path, relation]
|
|
2442
|
+
);
|
|
2443
|
+
}
|
|
2444
|
+
if (Object.keys(nestedPopulate).length > 0) {
|
|
2445
|
+
options["populate"] = nestedPopulate;
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
if (Object.keys(options).length === 0) {
|
|
2449
|
+
return "*";
|
|
2450
|
+
}
|
|
2451
|
+
return options;
|
|
2452
|
+
}
|
|
2453
|
+
|
|
2454
|
+
// src/parser/query-parser.ts
|
|
2455
|
+
var DEFAULT_OPTIONS = {
|
|
2456
|
+
maxPageSize: 100,
|
|
2457
|
+
defaultPageSize: 25,
|
|
2458
|
+
maxPopulateDepth: 5,
|
|
2459
|
+
allowedOperators: [],
|
|
2460
|
+
strictMode: false
|
|
2461
|
+
};
|
|
2462
|
+
function parseQuery(params, options) {
|
|
2463
|
+
const opts = {
|
|
2464
|
+
...DEFAULT_OPTIONS,
|
|
2465
|
+
...options
|
|
2466
|
+
};
|
|
2467
|
+
const fields = parseFields(params);
|
|
2468
|
+
const where = parseWhere(params);
|
|
2469
|
+
const populate = parsePopulate(params, opts.maxPopulateDepth);
|
|
2470
|
+
const pagination = parsePagination(params, opts);
|
|
2471
|
+
const sort = parseSort(params);
|
|
2472
|
+
const unknownParams = detectUnknownParams(params);
|
|
2473
|
+
if (unknownParams.length > 0) {
|
|
2474
|
+
throw new import_core15.ParserError(
|
|
2475
|
+
`Unknown query parameters: ${unknownParams.join(", ")}`,
|
|
2476
|
+
{
|
|
2477
|
+
code: "UNKNOWN_PARAMETER",
|
|
2478
|
+
parser: "query",
|
|
2479
|
+
location: (0, import_core15.buildErrorLocation)(unknownParams),
|
|
2480
|
+
received: unknownParams,
|
|
2481
|
+
expected: "Known parameters: fields, where, populate, page, pageSize, sort",
|
|
2482
|
+
suggestion: "Check for typos. Common mistake: use 'where' instead of 'filters'."
|
|
2483
|
+
}
|
|
2484
|
+
);
|
|
2485
|
+
}
|
|
2486
|
+
const result = {
|
|
2487
|
+
...fields !== void 0 && fields !== "*" && { select: fields },
|
|
2488
|
+
...where !== void 0 && { where },
|
|
2489
|
+
...populate !== void 0 && { populate },
|
|
2490
|
+
...pagination !== void 0 && {
|
|
2491
|
+
page: pagination.page ?? 1,
|
|
2492
|
+
pageSize: pagination.pageSize ?? opts.defaultPageSize
|
|
2493
|
+
},
|
|
2494
|
+
...sort !== void 0 && Array.isArray(sort) && sort.length > 0 && { orderBy: sort }
|
|
2495
|
+
};
|
|
2496
|
+
return result;
|
|
2497
|
+
}
|
|
2498
|
+
function parsePagination(params, options) {
|
|
2499
|
+
const { page, pageSize } = params;
|
|
2500
|
+
const parsedPage = page !== void 0 ? parseInt(String(page), 10) : 1;
|
|
2501
|
+
const parsedPageSize = pageSize !== void 0 ? parseInt(String(pageSize), 10) : options.defaultPageSize;
|
|
2502
|
+
const MAX_PAGE_NUMBER = 1e6;
|
|
2503
|
+
if (isNaN(parsedPage) || parsedPage < 1) {
|
|
2504
|
+
paginationError.invalidPage(page ?? "", ["page"]);
|
|
2505
|
+
}
|
|
2506
|
+
if (parsedPage > MAX_PAGE_NUMBER) {
|
|
2507
|
+
paginationError.maxPageNumberExceeded(parsedPage, MAX_PAGE_NUMBER, [
|
|
2508
|
+
"page"
|
|
2509
|
+
]);
|
|
2510
|
+
}
|
|
2511
|
+
if (isNaN(parsedPageSize) || parsedPageSize < 1) {
|
|
2512
|
+
paginationError.invalidPageSize(pageSize ?? "", ["pageSize"]);
|
|
2513
|
+
}
|
|
2514
|
+
if (parsedPageSize > options.maxPageSize) {
|
|
2515
|
+
paginationError.maxPageSizeExceeded(parsedPageSize, options.maxPageSize, [
|
|
2516
|
+
"pageSize"
|
|
2517
|
+
]);
|
|
2518
|
+
}
|
|
2519
|
+
return {
|
|
2520
|
+
page: parsedPage,
|
|
2521
|
+
pageSize: parsedPageSize
|
|
2522
|
+
};
|
|
2523
|
+
}
|
|
2524
|
+
function parseSort(params) {
|
|
2525
|
+
const sortParam = params["sort"];
|
|
2526
|
+
if (sortParam === void 0) {
|
|
2527
|
+
return void 0;
|
|
2528
|
+
}
|
|
2529
|
+
if (typeof sortParam === "string" && sortParam.trim() === "") {
|
|
2530
|
+
sortError.emptyValue([]);
|
|
2531
|
+
}
|
|
2532
|
+
const sorts = [];
|
|
2533
|
+
const sortStrings = typeof sortParam === "string" ? sortParam.split(",").map((s) => s.trim()) : Array.isArray(sortParam) ? sortParam.map((s) => String(s).trim()) : [String(sortParam).trim()];
|
|
2534
|
+
for (const sortStr of sortStrings) {
|
|
2535
|
+
if (!sortStr) {
|
|
2536
|
+
continue;
|
|
2537
|
+
}
|
|
2538
|
+
const isDescending = sortStr.startsWith("-");
|
|
2539
|
+
const field = isDescending ? sortStr.slice(1) : sortStr;
|
|
2540
|
+
if (!field) {
|
|
2541
|
+
sortError.invalidFieldName(sortStr, [sortStr]);
|
|
2542
|
+
}
|
|
2543
|
+
const validation = (0, import_core16.validateFieldName)(field);
|
|
2544
|
+
if (!validation.valid) {
|
|
2545
|
+
sortError.invalidFieldName(sortStr, [sortStr], {
|
|
2546
|
+
fieldValidationReason: validation.reason
|
|
2547
|
+
});
|
|
2548
|
+
}
|
|
2549
|
+
const direction = isDescending ? "desc" : "asc";
|
|
2550
|
+
sorts.push({ field, direction });
|
|
2551
|
+
}
|
|
2552
|
+
return sorts.length > 0 ? sorts : void 0;
|
|
2553
|
+
}
|
|
2554
|
+
var KNOWN_PARAM_PREFIXES = [
|
|
2555
|
+
"fields",
|
|
2556
|
+
"where",
|
|
2557
|
+
"populate",
|
|
2558
|
+
"page",
|
|
2559
|
+
"pageSize",
|
|
2560
|
+
"sort"
|
|
2561
|
+
];
|
|
2562
|
+
function detectUnknownParams(params) {
|
|
2563
|
+
const unknownKeys = [];
|
|
2564
|
+
for (const key of Object.keys(params)) {
|
|
2565
|
+
const isKnown = KNOWN_PARAM_PREFIXES.some(
|
|
2566
|
+
(prefix) => key === prefix || key.startsWith(`${prefix}[`)
|
|
2567
|
+
);
|
|
2568
|
+
if (!isKnown) {
|
|
2569
|
+
unknownKeys.push(key);
|
|
2570
|
+
}
|
|
2571
|
+
}
|
|
2572
|
+
return unknownKeys;
|
|
2573
|
+
}
|
|
2574
|
+
|
|
2575
|
+
// src/middleware/context.ts
|
|
2576
|
+
function extractTableNameFromPath(pathname, prefix) {
|
|
2577
|
+
const segments = pathname.split("/").filter(Boolean);
|
|
2578
|
+
const prefixSegments = prefix.split("/").filter(Boolean);
|
|
2579
|
+
const pathSegments = segments.slice(prefixSegments.length);
|
|
2580
|
+
if (pathSegments.length === 0) {
|
|
2581
|
+
return null;
|
|
2582
|
+
}
|
|
2583
|
+
return pathSegments[0] ?? null;
|
|
2584
|
+
}
|
|
2585
|
+
function extractIdFromPath(pathname, prefix) {
|
|
2586
|
+
const segments = pathname.split("/").filter(Boolean);
|
|
2587
|
+
const prefixSegments = prefix.split("/").filter(Boolean);
|
|
2588
|
+
const pathSegments = segments.slice(prefixSegments.length);
|
|
2589
|
+
if (pathSegments.length < 2) {
|
|
2590
|
+
return null;
|
|
2591
|
+
}
|
|
2592
|
+
const val = parseInt(pathSegments[1], 10);
|
|
2593
|
+
return isNaN(val) ? null : val;
|
|
2594
|
+
}
|
|
2595
|
+
var ContextBuildError = class extends Error {
|
|
2596
|
+
parserError;
|
|
2597
|
+
constructor(parserError) {
|
|
2598
|
+
super(parserError.message);
|
|
2599
|
+
this.name = "ContextBuildError";
|
|
2600
|
+
this.parserError = parserError;
|
|
2601
|
+
}
|
|
2602
|
+
};
|
|
2603
|
+
async function buildRequestContext(request, datrix, api, options = {}) {
|
|
2604
|
+
const apiPrefix = options.apiPrefix ?? "/api";
|
|
2605
|
+
const url = new URL(request.url);
|
|
2606
|
+
const method = request.method;
|
|
2607
|
+
const authEnabled = api.isAuthEnabled();
|
|
2608
|
+
const tableName = extractTableNameFromPath(url.pathname, apiPrefix);
|
|
2609
|
+
const modelName = tableName === "upload" && api.upload ? api.upload.getModelName() : datrix.getSchemas().findModelByTableName(tableName);
|
|
2610
|
+
const schema = modelName ? datrix.getSchema(modelName) ?? null : null;
|
|
2611
|
+
const action = methodToAction(method);
|
|
2612
|
+
const id = extractIdFromPath(url.pathname, apiPrefix);
|
|
2613
|
+
let user = null;
|
|
2614
|
+
if (authEnabled && api.authManager) {
|
|
2615
|
+
const authResult = await api.authManager.authenticate(request);
|
|
2616
|
+
user = authResult?.user ?? null;
|
|
2617
|
+
}
|
|
2618
|
+
let query = null;
|
|
2619
|
+
const queryParams = {};
|
|
2620
|
+
url.searchParams.forEach((value, key) => {
|
|
2621
|
+
const existing = queryParams[key];
|
|
2622
|
+
if (existing !== void 0) {
|
|
2623
|
+
if (Array.isArray(existing)) {
|
|
2624
|
+
existing.push(value);
|
|
2625
|
+
} else {
|
|
2626
|
+
queryParams[key] = [existing, value];
|
|
2627
|
+
}
|
|
2628
|
+
} else {
|
|
2629
|
+
queryParams[key] = value;
|
|
2630
|
+
}
|
|
2631
|
+
});
|
|
2632
|
+
if (Object.keys(queryParams).length > 0) {
|
|
2633
|
+
query = parseQuery(queryParams);
|
|
2634
|
+
}
|
|
2635
|
+
let body = null;
|
|
2636
|
+
if (["POST", "PATCH", "PUT"].includes(method)) {
|
|
2637
|
+
try {
|
|
2638
|
+
const contentType = request.headers.get("content-type");
|
|
2639
|
+
if (contentType?.includes("application/json")) {
|
|
2640
|
+
body = await request.json();
|
|
2641
|
+
}
|
|
2642
|
+
} catch {
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
2645
|
+
const headers = {};
|
|
2646
|
+
request.headers.forEach((value, key) => {
|
|
2647
|
+
headers[key] = value;
|
|
2648
|
+
});
|
|
2649
|
+
return {
|
|
2650
|
+
schema,
|
|
2651
|
+
action,
|
|
2652
|
+
id,
|
|
2653
|
+
method,
|
|
2654
|
+
query,
|
|
2655
|
+
body,
|
|
2656
|
+
headers,
|
|
2657
|
+
url,
|
|
2658
|
+
request,
|
|
2659
|
+
user,
|
|
2660
|
+
datrix,
|
|
2661
|
+
api,
|
|
2662
|
+
authEnabled
|
|
2663
|
+
};
|
|
2664
|
+
}
|
|
2665
|
+
|
|
2666
|
+
// src/handler/unified.ts
|
|
2667
|
+
var import_core17 = require("@datrix/core");
|
|
2668
|
+
async function handleGet(ctx) {
|
|
2669
|
+
const { datrix, schema, authEnabled } = ctx;
|
|
2670
|
+
if (!schema) {
|
|
2671
|
+
throw handlerError.schemaNotFound(ctx.url.pathname);
|
|
2672
|
+
}
|
|
2673
|
+
const { upload } = ctx.api;
|
|
2674
|
+
if (ctx.id) {
|
|
2675
|
+
const result = await datrix.findById(schema.name, ctx.id, {
|
|
2676
|
+
select: ctx.query?.select,
|
|
2677
|
+
populate: ctx.query?.populate
|
|
2678
|
+
});
|
|
2679
|
+
if (!result) {
|
|
2680
|
+
throw handlerError.recordNotFound(schema.name, ctx.id);
|
|
2681
|
+
}
|
|
2682
|
+
if (authEnabled) {
|
|
2683
|
+
const { data: filteredResult } = await filterFieldsForRead(
|
|
2684
|
+
schema,
|
|
2685
|
+
result,
|
|
2686
|
+
ctx
|
|
2687
|
+
);
|
|
2688
|
+
const data2 = upload ? await upload.injectUrls(filteredResult) : filteredResult;
|
|
2689
|
+
return jsonResponse({ data: data2 });
|
|
2690
|
+
}
|
|
2691
|
+
const data = upload ? await upload.injectUrls(result) : result;
|
|
2692
|
+
return jsonResponse({ data });
|
|
2693
|
+
} else {
|
|
2694
|
+
const page = ctx.query?.page ?? 1;
|
|
2695
|
+
const pageSize = ctx.query?.pageSize ?? 25;
|
|
2696
|
+
const limit = pageSize;
|
|
2697
|
+
const offset = (page - 1) * pageSize;
|
|
2698
|
+
const result = await datrix.findMany(schema.name, {
|
|
2699
|
+
where: ctx.query?.where,
|
|
2700
|
+
select: ctx.query?.select,
|
|
2701
|
+
populate: ctx.query?.populate,
|
|
2702
|
+
orderBy: ctx.query?.orderBy,
|
|
2703
|
+
limit,
|
|
2704
|
+
offset
|
|
2705
|
+
});
|
|
2706
|
+
const total = await datrix.count(schema.name, ctx.query?.where);
|
|
2707
|
+
const totalPages = Math.ceil(total / pageSize);
|
|
2708
|
+
if (authEnabled) {
|
|
2709
|
+
const filteredResults = await filterRecordsForRead(schema, result, ctx);
|
|
2710
|
+
const data2 = upload ? await upload.injectUrls(filteredResults) : filteredResults;
|
|
2711
|
+
const response2 = {
|
|
2712
|
+
data: data2,
|
|
2713
|
+
meta: { total, page, pageSize, totalPages }
|
|
2714
|
+
};
|
|
2715
|
+
return jsonResponse(response2);
|
|
2716
|
+
}
|
|
2717
|
+
const data = upload ? await upload.injectUrls(result) : result;
|
|
2718
|
+
const response = {
|
|
2719
|
+
data,
|
|
2720
|
+
meta: { total, page, pageSize, totalPages }
|
|
2721
|
+
};
|
|
2722
|
+
return jsonResponse(response);
|
|
2723
|
+
}
|
|
2724
|
+
}
|
|
2725
|
+
async function handlePost(ctx) {
|
|
2726
|
+
const { datrix, schema, authEnabled, body, query } = ctx;
|
|
2727
|
+
if (!schema) {
|
|
2728
|
+
throw handlerError.schemaNotFound(ctx.url.pathname);
|
|
2729
|
+
}
|
|
2730
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
2731
|
+
throw handlerError.invalidBody();
|
|
2732
|
+
}
|
|
2733
|
+
if (authEnabled) {
|
|
2734
|
+
const fieldCheck = await checkFieldsForWrite(schema, ctx);
|
|
2735
|
+
if (!fieldCheck.allowed) {
|
|
2736
|
+
throw handlerError.permissionDenied(
|
|
2737
|
+
`Permission denied for fields: ${fieldCheck.deniedFields?.join(", ")}`,
|
|
2738
|
+
{ deniedFields: fieldCheck.deniedFields }
|
|
2739
|
+
);
|
|
2740
|
+
}
|
|
2741
|
+
}
|
|
2742
|
+
const { upload } = ctx.api;
|
|
2743
|
+
const result = await datrix.create(schema.name, body, {
|
|
2744
|
+
select: query?.select,
|
|
2745
|
+
populate: query?.populate
|
|
2746
|
+
});
|
|
2747
|
+
if (authEnabled) {
|
|
2748
|
+
const { data: filteredResult } = await filterFieldsForRead(
|
|
2749
|
+
schema,
|
|
2750
|
+
result,
|
|
2751
|
+
ctx
|
|
2752
|
+
);
|
|
2753
|
+
const data2 = upload ? await upload.injectUrls(filteredResult) : filteredResult;
|
|
2754
|
+
return jsonResponse({ data: data2 }, 201);
|
|
2755
|
+
}
|
|
2756
|
+
const data = upload ? await upload.injectUrls(result) : result;
|
|
2757
|
+
return jsonResponse({ data }, 201);
|
|
2758
|
+
}
|
|
2759
|
+
async function handleUpdate(ctx) {
|
|
2760
|
+
const { datrix, schema, authEnabled, body, id } = ctx;
|
|
2761
|
+
if (!schema) {
|
|
2762
|
+
throw handlerError.schemaNotFound(ctx.url.pathname);
|
|
2763
|
+
}
|
|
2764
|
+
if (!id) {
|
|
2765
|
+
throw handlerError.missingId("update");
|
|
2766
|
+
}
|
|
2767
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
2768
|
+
throw handlerError.invalidBody();
|
|
2769
|
+
}
|
|
2770
|
+
const existingRecord = await datrix.findById(schema.name, id);
|
|
2771
|
+
if (!existingRecord) {
|
|
2772
|
+
throw handlerError.recordNotFound(schema.name, id);
|
|
2773
|
+
}
|
|
2774
|
+
if (authEnabled) {
|
|
2775
|
+
const fieldCheck = await checkFieldsForWrite(schema, ctx);
|
|
2776
|
+
if (!fieldCheck.allowed) {
|
|
2777
|
+
throw handlerError.permissionDenied(
|
|
2778
|
+
`Permission denied for fields: ${fieldCheck.deniedFields?.join(", ")}`,
|
|
2779
|
+
{ deniedFields: fieldCheck.deniedFields }
|
|
2780
|
+
);
|
|
2781
|
+
}
|
|
2782
|
+
}
|
|
2783
|
+
const result = await datrix.update(schema.name, id, body, {
|
|
2784
|
+
select: ctx.query?.select,
|
|
2785
|
+
populate: ctx.query?.populate
|
|
2786
|
+
});
|
|
2787
|
+
if (!result) {
|
|
2788
|
+
throw handlerError.recordNotFound(schema.name, id);
|
|
2789
|
+
}
|
|
2790
|
+
const { upload } = ctx.api;
|
|
2791
|
+
if (authEnabled) {
|
|
2792
|
+
const { data: filteredResult } = await filterFieldsForRead(
|
|
2793
|
+
schema,
|
|
2794
|
+
result,
|
|
2795
|
+
ctx
|
|
2796
|
+
);
|
|
2797
|
+
const data2 = upload ? await upload.injectUrls(filteredResult) : filteredResult;
|
|
2798
|
+
return jsonResponse({ data: data2 });
|
|
2799
|
+
}
|
|
2800
|
+
const data = upload ? await upload.injectUrls(result) : result;
|
|
2801
|
+
return jsonResponse({ data });
|
|
2802
|
+
}
|
|
2803
|
+
async function handleDelete(ctx) {
|
|
2804
|
+
const { datrix, schema, id } = ctx;
|
|
2805
|
+
if (!schema) {
|
|
2806
|
+
throw handlerError.schemaNotFound(ctx.url.pathname);
|
|
2807
|
+
}
|
|
2808
|
+
if (!id) {
|
|
2809
|
+
throw handlerError.missingId("delete");
|
|
2810
|
+
}
|
|
2811
|
+
const deleted = await datrix.delete(schema.name, id);
|
|
2812
|
+
if (!deleted) {
|
|
2813
|
+
throw handlerError.recordNotFound(schema.name, id);
|
|
2814
|
+
}
|
|
2815
|
+
return jsonResponse({ data: { id, deleted: true } });
|
|
2816
|
+
}
|
|
2817
|
+
async function handleCrudRequest(request, datrix, api, options) {
|
|
2818
|
+
try {
|
|
2819
|
+
const ctx = await buildRequestContext(request, datrix, api, options);
|
|
2820
|
+
if (!ctx.schema) {
|
|
2821
|
+
throw handlerError.modelNotSpecified();
|
|
2822
|
+
}
|
|
2823
|
+
if (api.excludeSchemas.includes(ctx.schema.name)) {
|
|
2824
|
+
throw handlerError.schemaNotFound(ctx.url.pathname);
|
|
2825
|
+
}
|
|
2826
|
+
if (ctx.authEnabled) {
|
|
2827
|
+
const permissionResult = await checkSchemaPermission(
|
|
2828
|
+
ctx.schema,
|
|
2829
|
+
ctx,
|
|
2830
|
+
api.authDefaultPermission
|
|
2831
|
+
);
|
|
2832
|
+
if (!permissionResult.allowed) {
|
|
2833
|
+
throw ctx.user ? handlerError.permissionDenied("Schema scope permission denied") : handlerError.unauthorized();
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2836
|
+
api.setUser(ctx.user);
|
|
2837
|
+
switch (ctx.method) {
|
|
2838
|
+
case "GET":
|
|
2839
|
+
return await handleGet(ctx);
|
|
2840
|
+
case "POST":
|
|
2841
|
+
return await handlePost(ctx);
|
|
2842
|
+
case "PATCH":
|
|
2843
|
+
case "PUT":
|
|
2844
|
+
return await handleUpdate(ctx);
|
|
2845
|
+
case "DELETE":
|
|
2846
|
+
return await handleDelete(ctx);
|
|
2847
|
+
default: {
|
|
2848
|
+
throw handlerError.methodNotAllowed(ctx.method);
|
|
2849
|
+
}
|
|
2850
|
+
}
|
|
2851
|
+
} catch (error) {
|
|
2852
|
+
if (error instanceof import_core17.DatrixValidationError || error instanceof import_core17.DatrixError) {
|
|
2853
|
+
return datrixErrorResponse(error);
|
|
2854
|
+
}
|
|
2855
|
+
console.error("Unified Handler Error:", error);
|
|
2856
|
+
const message = error instanceof Error ? error.message : "Internal server error";
|
|
2857
|
+
return datrixErrorResponse(
|
|
2858
|
+
handlerError.internalError(
|
|
2859
|
+
message,
|
|
2860
|
+
error instanceof Error ? error : void 0
|
|
2861
|
+
)
|
|
2862
|
+
);
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2865
|
+
|
|
2866
|
+
// src/api.ts
|
|
2867
|
+
var ApiPlugin = class extends import_core18.BasePlugin {
|
|
2868
|
+
name = "api";
|
|
2869
|
+
version = "1.0.0";
|
|
2870
|
+
authManager;
|
|
2871
|
+
user = null;
|
|
2872
|
+
datrixInstance;
|
|
2873
|
+
get datrix() {
|
|
2874
|
+
return this.datrixInstance;
|
|
2875
|
+
}
|
|
2876
|
+
get upload() {
|
|
2877
|
+
return this.options.upload;
|
|
2878
|
+
}
|
|
2879
|
+
setUser(user) {
|
|
2880
|
+
this.user = user;
|
|
2881
|
+
}
|
|
2882
|
+
get authConfig() {
|
|
2883
|
+
return this.options.auth;
|
|
2884
|
+
}
|
|
2885
|
+
get apiConfig() {
|
|
2886
|
+
return this.options;
|
|
2887
|
+
}
|
|
2888
|
+
get authSchemaName() {
|
|
2889
|
+
return this.authConfig?.authSchemaName ?? "authentication";
|
|
2890
|
+
}
|
|
2891
|
+
get userSchemaName() {
|
|
2892
|
+
return this.authConfig?.userSchema?.name ?? "user";
|
|
2893
|
+
}
|
|
2894
|
+
get userSchemaEmailField() {
|
|
2895
|
+
return this.authConfig?.userSchema?.email ?? "email";
|
|
2896
|
+
}
|
|
2897
|
+
get authDefaultPermission() {
|
|
2898
|
+
return this.authConfig?.defaultPermission;
|
|
2899
|
+
}
|
|
2900
|
+
get authDefaultRole() {
|
|
2901
|
+
return this.authConfig?.defaultRole;
|
|
2902
|
+
}
|
|
2903
|
+
get excludeSchemas() {
|
|
2904
|
+
return [
|
|
2905
|
+
...this.apiConfig.excludeSchemas ?? [],
|
|
2906
|
+
"_datrix",
|
|
2907
|
+
"_datrix_migrations"
|
|
2908
|
+
];
|
|
2909
|
+
}
|
|
2910
|
+
getTableName(schemaName) {
|
|
2911
|
+
const schema = this.datrix.getSchema(schemaName);
|
|
2912
|
+
return schema?.tableName || `${schemaName.toLowerCase()}s`;
|
|
2913
|
+
}
|
|
2914
|
+
async onCreateQueryContext(context) {
|
|
2915
|
+
if (this.user) {
|
|
2916
|
+
context.user = this.user;
|
|
2917
|
+
}
|
|
2918
|
+
return context;
|
|
2919
|
+
}
|
|
2920
|
+
async init(context) {
|
|
2921
|
+
this.context = context;
|
|
2922
|
+
if (!this.authConfig) {
|
|
2923
|
+
return;
|
|
2924
|
+
}
|
|
2925
|
+
if (context.schemas.has("auth")) {
|
|
2926
|
+
throw this.createError(
|
|
2927
|
+
"Schema name 'auth' is reserved for API authentication routes",
|
|
2928
|
+
"RESERVED_SCHEMA_NAME"
|
|
2929
|
+
);
|
|
2930
|
+
}
|
|
2931
|
+
if (!context.schemas.has(this.userSchemaName)) {
|
|
2932
|
+
throw this.createError(
|
|
2933
|
+
`User schema '${this.userSchemaName}' not found. Create it before enabling auth.`,
|
|
2934
|
+
"USER_SCHEMA_NOT_FOUND"
|
|
2935
|
+
);
|
|
2936
|
+
}
|
|
2937
|
+
const userSchema = context.schemas.get(this.userSchemaName);
|
|
2938
|
+
const emailField = this.userSchemaEmailField;
|
|
2939
|
+
if (!userSchema?.fields[emailField]) {
|
|
2940
|
+
throw this.createError(
|
|
2941
|
+
`User schema must have an '${emailField}' field`,
|
|
2942
|
+
"MISSING_EMAIL_FIELD"
|
|
2943
|
+
);
|
|
2944
|
+
}
|
|
2945
|
+
if (this.authConfig.jwt) {
|
|
2946
|
+
if (this.authConfig.jwt.secret.length < 32) {
|
|
2947
|
+
throw this.createError(
|
|
2948
|
+
"JWT secret must be at least 32 characters long for security",
|
|
2949
|
+
"WEAK_JWT_SECRET"
|
|
2950
|
+
);
|
|
2951
|
+
}
|
|
2952
|
+
}
|
|
2953
|
+
this.authManager = new AuthManager(this.authConfig);
|
|
2954
|
+
}
|
|
2955
|
+
async destroy() {
|
|
2956
|
+
}
|
|
2957
|
+
async getSchemas() {
|
|
2958
|
+
const schemas = [];
|
|
2959
|
+
if (this.options.upload) {
|
|
2960
|
+
const uploadSchemas = await this.options.upload.getSchemas();
|
|
2961
|
+
schemas.push(...uploadSchemas);
|
|
2962
|
+
}
|
|
2963
|
+
if (!this.authConfig) {
|
|
2964
|
+
return schemas;
|
|
2965
|
+
}
|
|
2966
|
+
const authSchema = (0, import_core19.defineSchema)({
|
|
2967
|
+
name: this.authSchemaName,
|
|
2968
|
+
fields: {
|
|
2969
|
+
user: {
|
|
2970
|
+
type: "relation",
|
|
2971
|
+
required: true,
|
|
2972
|
+
kind: "belongsTo",
|
|
2973
|
+
model: this.userSchemaName
|
|
2974
|
+
},
|
|
2975
|
+
email: {
|
|
2976
|
+
type: "string",
|
|
2977
|
+
required: true
|
|
2978
|
+
},
|
|
2979
|
+
password: {
|
|
2980
|
+
type: "string",
|
|
2981
|
+
required: true
|
|
2982
|
+
},
|
|
2983
|
+
passwordSalt: {
|
|
2984
|
+
type: "string",
|
|
2985
|
+
required: true
|
|
2986
|
+
},
|
|
2987
|
+
role: {
|
|
2988
|
+
type: "string",
|
|
2989
|
+
required: true,
|
|
2990
|
+
default: this.authDefaultRole ?? "user"
|
|
2991
|
+
}
|
|
2992
|
+
},
|
|
2993
|
+
indexes: [
|
|
2994
|
+
{
|
|
2995
|
+
name: `${this.authSchemaName}_email_idx`,
|
|
2996
|
+
fields: ["email"],
|
|
2997
|
+
unique: true
|
|
2998
|
+
},
|
|
2999
|
+
{
|
|
3000
|
+
name: `${this.authSchemaName}_userId_idx`,
|
|
3001
|
+
fields: ["user"],
|
|
3002
|
+
unique: true
|
|
3003
|
+
}
|
|
3004
|
+
]
|
|
3005
|
+
});
|
|
3006
|
+
schemas.push(authSchema);
|
|
3007
|
+
return schemas;
|
|
3008
|
+
}
|
|
3009
|
+
async onBeforeQuery(query, context) {
|
|
3010
|
+
if (!this.authConfig) {
|
|
3011
|
+
return query;
|
|
3012
|
+
}
|
|
3013
|
+
const userTable = this.getTableName(this.userSchemaName);
|
|
3014
|
+
if (query.type === "insert" && query.table === userTable) {
|
|
3015
|
+
context.metadata["api:createAuth"] = true;
|
|
3016
|
+
context.metadata["api:userData"] = query.data[0];
|
|
3017
|
+
}
|
|
3018
|
+
if (query.type === "update" && query.table === userTable) {
|
|
3019
|
+
const data = query.data;
|
|
3020
|
+
const emailField = this.userSchemaEmailField;
|
|
3021
|
+
if (data && emailField in data) {
|
|
3022
|
+
context.metadata["api:syncEmail"] = query.data[emailField];
|
|
3023
|
+
context.metadata["api:userId"] = query.where?.["id"];
|
|
3024
|
+
}
|
|
3025
|
+
}
|
|
3026
|
+
return query;
|
|
3027
|
+
}
|
|
3028
|
+
async onAfterQuery(result, context) {
|
|
3029
|
+
if (!this.authConfig) {
|
|
3030
|
+
return result;
|
|
3031
|
+
}
|
|
3032
|
+
const pluginContext = this.getContext();
|
|
3033
|
+
if (context.metadata["api:createAuth"]) {
|
|
3034
|
+
const { id: userId } = Array.isArray(result) ? result[0] : result;
|
|
3035
|
+
if (typeof userId === "number") {
|
|
3036
|
+
const user = {
|
|
3037
|
+
...context.metadata["api:userData"],
|
|
3038
|
+
userId
|
|
3039
|
+
};
|
|
3040
|
+
await this.createAuthenticationRecord(user, pluginContext);
|
|
3041
|
+
}
|
|
3042
|
+
}
|
|
3043
|
+
if (context.metadata["api:syncEmail"] && context.metadata["api:userId"]) {
|
|
3044
|
+
const newEmail = context.metadata["api:syncEmail"];
|
|
3045
|
+
const userId = context.metadata["api:userId"];
|
|
3046
|
+
await this.syncAuthenticationEmail(userId, newEmail, pluginContext);
|
|
3047
|
+
}
|
|
3048
|
+
return result;
|
|
3049
|
+
}
|
|
3050
|
+
async createAuthenticationRecord(_user, _context) {
|
|
3051
|
+
const emailField = this.userSchemaEmailField;
|
|
3052
|
+
const user = _user;
|
|
3053
|
+
const authData = {
|
|
3054
|
+
user: user["userId"],
|
|
3055
|
+
email: user[emailField],
|
|
3056
|
+
password: user["password"] || "",
|
|
3057
|
+
passwordSalt: user["passwordSalt"] || "",
|
|
3058
|
+
role: user["role"] || this.authConfig?.defaultRole || "user"
|
|
3059
|
+
};
|
|
3060
|
+
await this.datrixInstance.raw.create(this.authSchemaName, authData);
|
|
3061
|
+
}
|
|
3062
|
+
async syncAuthenticationEmail(userId, newEmail, _context) {
|
|
3063
|
+
await this.datrix.raw.updateMany(
|
|
3064
|
+
this.authSchemaName,
|
|
3065
|
+
{ user: { id: { $eq: userId } } },
|
|
3066
|
+
{ email: newEmail }
|
|
3067
|
+
);
|
|
3068
|
+
}
|
|
3069
|
+
/**
|
|
3070
|
+
* Handle HTTP request
|
|
3071
|
+
*
|
|
3072
|
+
* Main entry point for all API requests.
|
|
3073
|
+
* Routes to auth handlers or CRUD handlers.
|
|
3074
|
+
*/
|
|
3075
|
+
async handleRequest(request, datrix) {
|
|
3076
|
+
if (!this.isInitialized()) {
|
|
3077
|
+
return datrixErrorResponse(
|
|
3078
|
+
handlerError.internalError("API plugin not initialized")
|
|
3079
|
+
);
|
|
3080
|
+
}
|
|
3081
|
+
this.datrixInstance = datrix;
|
|
3082
|
+
const url = new URL(request.url);
|
|
3083
|
+
const prefix = this.apiConfig.prefix ?? "/api";
|
|
3084
|
+
if (!url.pathname.startsWith(prefix)) {
|
|
3085
|
+
return datrixErrorResponse(
|
|
3086
|
+
handlerError.internalError("Invalid API prefix")
|
|
3087
|
+
);
|
|
3088
|
+
}
|
|
3089
|
+
const pathAfterPrefix = url.pathname.slice(prefix.length);
|
|
3090
|
+
const segments = pathAfterPrefix.split("/").filter(Boolean);
|
|
3091
|
+
const model = segments[0];
|
|
3092
|
+
if (this.authConfig && this.isAuthPath(pathAfterPrefix)) {
|
|
3093
|
+
return this.handleAuthRequest(request, datrix);
|
|
3094
|
+
}
|
|
3095
|
+
if (model === "upload" && this.apiConfig.upload && request.method !== "GET") {
|
|
3096
|
+
return this.apiConfig.upload.handleRequest(request, datrix);
|
|
3097
|
+
}
|
|
3098
|
+
return handleCrudRequest(request, datrix, this, {
|
|
3099
|
+
apiPrefix: prefix
|
|
3100
|
+
});
|
|
3101
|
+
}
|
|
3102
|
+
isAuthPath(pathname) {
|
|
3103
|
+
const e = this.authConfig?.endpoints;
|
|
3104
|
+
const d = import_core20.DEFAULT_API_AUTH_CONFIG.endpoints;
|
|
3105
|
+
const login = e?.login ?? d.login;
|
|
3106
|
+
const register = e?.register ?? d.register;
|
|
3107
|
+
const logout = e?.logout ?? d.logout;
|
|
3108
|
+
const me = e?.me ?? d.me;
|
|
3109
|
+
const authPrefix = [login, register, logout, me].map((p) => p.split("/")[1]).find(Boolean) ?? "auth";
|
|
3110
|
+
return pathname.startsWith(`/${authPrefix}/`) || pathname === `/${authPrefix}`;
|
|
3111
|
+
}
|
|
3112
|
+
/**
|
|
3113
|
+
* Handle authentication requests
|
|
3114
|
+
*/
|
|
3115
|
+
async handleAuthRequest(request, datrix) {
|
|
3116
|
+
if (!this.authManager) {
|
|
3117
|
+
return datrixErrorResponse(
|
|
3118
|
+
handlerError.internalError("Authentication not configured")
|
|
3119
|
+
);
|
|
3120
|
+
}
|
|
3121
|
+
const handler = createUnifiedAuthHandler(
|
|
3122
|
+
{
|
|
3123
|
+
datrix,
|
|
3124
|
+
authManager: this.authManager,
|
|
3125
|
+
authConfig: this.authConfig
|
|
3126
|
+
},
|
|
3127
|
+
this.apiConfig.prefix ?? "/api"
|
|
3128
|
+
);
|
|
3129
|
+
return handler(request);
|
|
3130
|
+
}
|
|
3131
|
+
/**
|
|
3132
|
+
* Check if API is enabled
|
|
3133
|
+
*/
|
|
3134
|
+
isEnabled() {
|
|
3135
|
+
return !(this.apiConfig.disabled ?? false);
|
|
3136
|
+
}
|
|
3137
|
+
/**
|
|
3138
|
+
* Check if authentication is enabled
|
|
3139
|
+
*/
|
|
3140
|
+
isAuthEnabled() {
|
|
3141
|
+
return this.authConfig !== void 0;
|
|
3142
|
+
}
|
|
3143
|
+
/**
|
|
3144
|
+
* Get auth manager (for external use)
|
|
3145
|
+
*/
|
|
3146
|
+
getAuthManager() {
|
|
3147
|
+
return this.authManager;
|
|
3148
|
+
}
|
|
3149
|
+
};
|
|
3150
|
+
|
|
3151
|
+
// src/helper/index.ts
|
|
3152
|
+
var import_core21 = require("@datrix/core");
|
|
3153
|
+
async function handleRequest(datrix, request) {
|
|
3154
|
+
try {
|
|
3155
|
+
const api = datrix.getPlugin("api");
|
|
3156
|
+
if (!api || !(api instanceof ApiPlugin)) {
|
|
3157
|
+
const errRes = handlerError.internalError(
|
|
3158
|
+
'API is not configured in datrix.config.ts. Add "api: new DatrixApi({ ... })" to your configuration.'
|
|
3159
|
+
);
|
|
3160
|
+
return datrixErrorResponse(errRes);
|
|
3161
|
+
}
|
|
3162
|
+
if (!api.isEnabled()) {
|
|
3163
|
+
const errRes = handlerError.internalError(
|
|
3164
|
+
'API is disabled. Remove "disabled: true" from ApiPlugin configuration.'
|
|
3165
|
+
);
|
|
3166
|
+
return datrixErrorResponse(errRes);
|
|
3167
|
+
}
|
|
3168
|
+
return await api.handleRequest(request, datrix);
|
|
3169
|
+
} catch (error) {
|
|
3170
|
+
if (error instanceof import_core21.DatrixError) {
|
|
3171
|
+
return datrixErrorResponse(error);
|
|
3172
|
+
}
|
|
3173
|
+
console.error("[Datrix API] Unexpected error:", error);
|
|
3174
|
+
const message = error instanceof Error ? error.message : "Internal server error";
|
|
3175
|
+
const errRes = handlerError.internalError(
|
|
3176
|
+
message,
|
|
3177
|
+
error instanceof Error ? error : void 0
|
|
3178
|
+
);
|
|
3179
|
+
return datrixErrorResponse(errRes);
|
|
3180
|
+
}
|
|
3181
|
+
}
|
|
3182
|
+
|
|
3183
|
+
// src/middleware/auth.ts
|
|
3184
|
+
async function authenticate(request, authManager) {
|
|
3185
|
+
if (!authManager) {
|
|
3186
|
+
return null;
|
|
3187
|
+
}
|
|
3188
|
+
const authContext = await authManager.authenticate(request);
|
|
3189
|
+
if (!authContext || !authContext.user) {
|
|
3190
|
+
return null;
|
|
3191
|
+
}
|
|
3192
|
+
return authContext.user;
|
|
3193
|
+
}
|
|
3194
|
+
|
|
3195
|
+
// src/serializer/query.ts
|
|
3196
|
+
function queryToParams(query) {
|
|
3197
|
+
if (!query) return "";
|
|
3198
|
+
const serialized = serializeQuery(query);
|
|
3199
|
+
const parts = [];
|
|
3200
|
+
Object.entries(serialized).forEach(([key, value]) => {
|
|
3201
|
+
if (Array.isArray(value)) {
|
|
3202
|
+
value.forEach((v) => {
|
|
3203
|
+
parts.push(`${key}=${encodeURIComponent(String(v))}`);
|
|
3204
|
+
});
|
|
3205
|
+
} else if (value !== void 0) {
|
|
3206
|
+
parts.push(`${key}=${encodeURIComponent(String(value))}`);
|
|
3207
|
+
}
|
|
3208
|
+
});
|
|
3209
|
+
return parts.join("&");
|
|
3210
|
+
}
|
|
3211
|
+
function serializeQuery(query) {
|
|
3212
|
+
const params = {};
|
|
3213
|
+
const validKeys = [
|
|
3214
|
+
"select",
|
|
3215
|
+
"where",
|
|
3216
|
+
"populate",
|
|
3217
|
+
"orderBy",
|
|
3218
|
+
"page",
|
|
3219
|
+
"pageSize"
|
|
3220
|
+
];
|
|
3221
|
+
const queryKeys = Object.keys(query);
|
|
3222
|
+
const unknownKeys = queryKeys.filter((key) => !validKeys.includes(key));
|
|
3223
|
+
if (unknownKeys.length > 0) {
|
|
3224
|
+
throw new Error(
|
|
3225
|
+
`Unknown query keys: ${unknownKeys.join(", ")}. Valid keys are: ${validKeys.join(", ")}`
|
|
3226
|
+
);
|
|
3227
|
+
}
|
|
3228
|
+
if (query.select) {
|
|
3229
|
+
if (query.select === "*") {
|
|
3230
|
+
params["fields"] = "*";
|
|
3231
|
+
} else if (Array.isArray(query.select)) {
|
|
3232
|
+
params["fields"] = query.select.join(",");
|
|
3233
|
+
}
|
|
3234
|
+
}
|
|
3235
|
+
if (query.where) {
|
|
3236
|
+
serializeWhere(query.where, "where", params);
|
|
3237
|
+
}
|
|
3238
|
+
if (query.populate) {
|
|
3239
|
+
serializePopulate(query.populate, "populate", params);
|
|
3240
|
+
}
|
|
3241
|
+
if (query.orderBy) {
|
|
3242
|
+
if (typeof query.orderBy === "string") {
|
|
3243
|
+
params["sort"] = query.orderBy;
|
|
3244
|
+
} else if (Array.isArray(query.orderBy)) {
|
|
3245
|
+
const sortStrings = query.orderBy.map((item) => {
|
|
3246
|
+
if (typeof item === "string") {
|
|
3247
|
+
return item;
|
|
3248
|
+
} else {
|
|
3249
|
+
return item.direction === "desc" ? `-${item.field}` : item.field;
|
|
3250
|
+
}
|
|
3251
|
+
});
|
|
3252
|
+
if (sortStrings.length > 0) {
|
|
3253
|
+
params["sort"] = sortStrings.join(",");
|
|
3254
|
+
}
|
|
3255
|
+
}
|
|
3256
|
+
}
|
|
3257
|
+
if (query.page !== void 0) params["page"] = String(query.page);
|
|
3258
|
+
if (query.pageSize !== void 0) params["pageSize"] = String(query.pageSize);
|
|
3259
|
+
return params;
|
|
3260
|
+
}
|
|
3261
|
+
function serializeWhere(where, prefix, params) {
|
|
3262
|
+
if (where === null || typeof where !== "object") {
|
|
3263
|
+
params[prefix] = String(where);
|
|
3264
|
+
return;
|
|
3265
|
+
}
|
|
3266
|
+
for (const [key, value] of Object.entries(where)) {
|
|
3267
|
+
const newPrefix = `${prefix}[${key}]`;
|
|
3268
|
+
if (Array.isArray(value)) {
|
|
3269
|
+
if (["$or", "$and", "$not"].includes(key)) {
|
|
3270
|
+
value.forEach((item, index) => {
|
|
3271
|
+
serializeWhere(item, `${newPrefix}[${index}]`, params);
|
|
3272
|
+
});
|
|
3273
|
+
} else {
|
|
3274
|
+
value.forEach((item, index) => {
|
|
3275
|
+
params[`${newPrefix}[${index}]`] = String(item);
|
|
3276
|
+
});
|
|
3277
|
+
}
|
|
3278
|
+
} else if (value !== null && typeof value === "object") {
|
|
3279
|
+
serializeWhere(value, newPrefix, params);
|
|
3280
|
+
} else {
|
|
3281
|
+
params[newPrefix] = String(value);
|
|
3282
|
+
}
|
|
3283
|
+
}
|
|
3284
|
+
}
|
|
3285
|
+
function serializePopulate(populate, prefix, params) {
|
|
3286
|
+
if (populate === "*") {
|
|
3287
|
+
params[prefix] = "*";
|
|
3288
|
+
return;
|
|
3289
|
+
}
|
|
3290
|
+
if (populate === "true") {
|
|
3291
|
+
params[prefix] = true;
|
|
3292
|
+
return;
|
|
3293
|
+
}
|
|
3294
|
+
if (populate === true) {
|
|
3295
|
+
params[prefix] = true;
|
|
3296
|
+
return;
|
|
3297
|
+
}
|
|
3298
|
+
if (Array.isArray(populate)) {
|
|
3299
|
+
populate.forEach((relation, index) => {
|
|
3300
|
+
params[`${prefix}[${index}]`] = String(relation);
|
|
3301
|
+
});
|
|
3302
|
+
return;
|
|
3303
|
+
}
|
|
3304
|
+
if (typeof populate !== "object") return;
|
|
3305
|
+
for (const [relation, options] of Object.entries(populate)) {
|
|
3306
|
+
const relPrefix = `${prefix}[${relation}]`;
|
|
3307
|
+
if (options === "*" || options === true) {
|
|
3308
|
+
params[relPrefix] = options;
|
|
3309
|
+
} else if (typeof options === "object") {
|
|
3310
|
+
const opts = options;
|
|
3311
|
+
if (opts.select) {
|
|
3312
|
+
if (opts.select === "*") {
|
|
3313
|
+
params[`${relPrefix}[fields]`] = "*";
|
|
3314
|
+
} else if (Array.isArray(opts.select)) {
|
|
3315
|
+
opts.select.forEach((field, index) => {
|
|
3316
|
+
params[`${relPrefix}[fields][${index}]`] = field;
|
|
3317
|
+
});
|
|
3318
|
+
}
|
|
3319
|
+
}
|
|
3320
|
+
if (opts.populate) {
|
|
3321
|
+
serializePopulate(opts.populate, `${relPrefix}[populate]`, params);
|
|
3322
|
+
}
|
|
3323
|
+
}
|
|
3324
|
+
}
|
|
3325
|
+
}
|
|
3326
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
3327
|
+
0 && (module.exports = {
|
|
3328
|
+
ApiPlugin,
|
|
3329
|
+
ContextBuildError,
|
|
3330
|
+
DatrixApiError,
|
|
3331
|
+
MemorySessionStore,
|
|
3332
|
+
authenticate,
|
|
3333
|
+
buildRequestContext,
|
|
3334
|
+
checkFieldsForWrite,
|
|
3335
|
+
checkSchemaPermission,
|
|
3336
|
+
createAuthHandlers,
|
|
3337
|
+
createUnifiedAuthHandler,
|
|
3338
|
+
datrixErrorResponse,
|
|
3339
|
+
evaluatePermissionValue,
|
|
3340
|
+
filterFieldsForRead,
|
|
3341
|
+
filterRecordsForRead,
|
|
3342
|
+
handleCrudRequest,
|
|
3343
|
+
handleRequest,
|
|
3344
|
+
handlerError,
|
|
3345
|
+
jsonResponse,
|
|
3346
|
+
methodToAction,
|
|
3347
|
+
parseFields,
|
|
3348
|
+
parsePopulate,
|
|
3349
|
+
parseQuery,
|
|
3350
|
+
parseWhere,
|
|
3351
|
+
queryToParams,
|
|
3352
|
+
serializeQuery
|
|
3353
|
+
});
|
|
3354
|
+
//# sourceMappingURL=index.js.map
|