@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/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