@getstrata/core 0.5.100 → 0.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/CHANGELOG.md +67 -32
  2. package/README.md +16 -35
  3. package/dist/core/auth/abilityCatalog.d.ts +2 -2
  4. package/dist/core/auth/basicAuthGuard.d.ts +9 -0
  5. package/dist/core/auth/guard.d.ts +6 -0
  6. package/dist/core/auth/jwt.d.ts +19 -0
  7. package/dist/core/auth/jwtGuard.d.ts +14 -0
  8. package/dist/core/auth/tokenAbilityChecker.d.ts +5 -0
  9. package/dist/core/cache/tags.d.ts +6 -0
  10. package/dist/core/contracts/authUserDirectory.d.ts +4 -0
  11. package/dist/core/database/baseRepository.d.ts +5 -1
  12. package/dist/core/database/dialect.d.ts +18 -0
  13. package/dist/core/database/factory.d.ts +1 -0
  14. package/dist/core/database/index.d.ts +11 -3
  15. package/dist/core/database/model.d.ts +21 -2
  16. package/dist/core/database/mysqlConnection.d.ts +12 -0
  17. package/dist/core/database/namedConnections.d.ts +15 -0
  18. package/dist/core/database/relationQuery.d.ts +22 -3
  19. package/dist/core/database/relationships.d.ts +22 -2
  20. package/dist/core/database/repositoryQuery.d.ts +5 -1
  21. package/dist/core/database/sqliteConnection.d.ts +7 -0
  22. package/dist/core/http/loginThrottleMiddleware.d.ts +5 -2
  23. package/dist/core/http/resources.d.ts +2 -2
  24. package/dist/core/http/response.d.ts +2 -1
  25. package/dist/core/http/statelessAuth.d.ts +8 -0
  26. package/dist/core/http/throttleResponse.d.ts +2 -0
  27. package/dist/core/runtime/frontendMode.d.ts +10 -2
  28. package/dist/entries/auth/basicAuthGuard.js +137 -0
  29. package/dist/entries/auth/jwt.js +135 -0
  30. package/dist/entries/auth/jwtGuard.js +203 -0
  31. package/dist/entries/auth/sessionGuard.js +3 -21
  32. package/dist/entries/auth/tokenAbilityChecker.js +24 -0
  33. package/dist/entries/cache/tags.js +7 -1
  34. package/dist/entries/database/connectionContext.js +1 -0
  35. package/dist/entries/database/dialect.js +1 -0
  36. package/dist/entries/database/factory.js +5 -4
  37. package/dist/entries/database/model.js +189 -33
  38. package/dist/entries/database/mysqlConnection.js +35 -0
  39. package/dist/entries/database/namedConnections.js +1 -0
  40. package/dist/entries/database/query.js +28 -15
  41. package/dist/entries/database/relationships.js +43 -6
  42. package/dist/entries/database/repositoryQuery.js +142 -73
  43. package/dist/entries/database/schema.js +28 -15
  44. package/dist/entries/database/sqliteConnection.js +34 -0
  45. package/dist/entries/facades.js +1 -1
  46. package/dist/entries/http/contentNegotiation.js +5 -2
  47. package/dist/entries/http/csrfMiddleware.js +45 -0
  48. package/dist/entries/http/loginThrottleMiddleware.js +246 -7
  49. package/dist/entries/http/memoryThrottleMiddleware.js +208 -6
  50. package/dist/entries/http/requireAbilityMiddleware.js +35 -11
  51. package/dist/entries/http/requirePasswordConfirmMiddleware.js +5 -2
  52. package/dist/entries/http/requireVerifiedMiddleware.js +5 -2
  53. package/dist/entries/http/requireWebAuthMiddleware.js +12 -3
  54. package/dist/entries/http/resources.js +4 -1
  55. package/dist/entries/http/response.js +46 -12
  56. package/dist/entries/http/statelessAuth.js +48 -0
  57. package/dist/entries/http/throttleMiddleware.js +208 -6
  58. package/dist/entries/http/webErrorResponse.js +35 -11
  59. package/dist/entries/http/webFormRequest.js +5 -2
  60. package/dist/entries/mail/mailer.js +1 -1
  61. package/dist/entries/openapi/generator.js +48 -5
  62. package/dist/entries/runtime/frontendMode.js +39 -10
  63. package/dist/framework/public-api.d.ts +11 -1
  64. package/dist/index.js +1068 -250
  65. package/package.json +56 -5
package/dist/index.js CHANGED
@@ -211,37 +211,101 @@ function resolveAuthUserDirectory(container) {
211
211
  return null;
212
212
  }
213
213
 
214
+ // ../../src/core/http/statelessAuth.ts
215
+ function authorizationScheme(request) {
216
+ const header = request.headers.get("authorization")?.trim() ?? "";
217
+ const scheme = header.split(/\s+/, 1)[0];
218
+ return scheme ? scheme.toLowerCase() : "";
219
+ }
220
+ function requestUsesHeaderCredentials(request) {
221
+ const scheme = authorizationScheme(request);
222
+ return scheme === "bearer" || scheme === "basic";
223
+ }
224
+ function readBearerToken(request) {
225
+ const header = request.headers.get("authorization")?.trim() ?? "";
226
+ if (!header.toLowerCase().startsWith("bearer ")) {
227
+ return null;
228
+ }
229
+ const token = header.slice("Bearer ".length).trim();
230
+ return token.length > 0 ? token : null;
231
+ }
232
+ function readBasicCredentials(request) {
233
+ const header = request.headers.get("authorization")?.trim() ?? "";
234
+ if (!header.toLowerCase().startsWith("basic ")) {
235
+ return null;
236
+ }
237
+ const encoded = header.slice("Basic ".length).trim();
238
+ if (!encoded) {
239
+ return null;
240
+ }
241
+ try {
242
+ const decoded = Buffer.from(encoded, "base64").toString("utf8");
243
+ const separator = decoded.indexOf(":");
244
+ if (separator < 0) {
245
+ return null;
246
+ }
247
+ return {
248
+ username: decoded.slice(0, separator),
249
+ password: decoded.slice(separator + 1)
250
+ };
251
+ } catch {
252
+ return null;
253
+ }
254
+ }
255
+
256
+ // ../../src/core/auth/password.ts
257
+ async function verifyPassword(password, passwordHash) {
258
+ return await Bun.password.verify(password, passwordHash);
259
+ }
260
+
261
+ // ../../src/core/auth/basicAuthGuard.ts
262
+ class BasicAuthGuard {
263
+ container;
264
+ constructor(container) {
265
+ this.container = container;
266
+ }
267
+ async resolve(request) {
268
+ const credentials = readBasicCredentials(request);
269
+ if (!credentials) {
270
+ return null;
271
+ }
272
+ const directory = resolveAuthUserDirectory(this.container);
273
+ if (!directory) {
274
+ return null;
275
+ }
276
+ if (typeof directory.verifyCredentials === "function") {
277
+ return await directory.verifyCredentials(credentials.username, credentials.password);
278
+ }
279
+ if (typeof directory.findByEmail !== "function") {
280
+ return null;
281
+ }
282
+ const record = await directory.findByEmail(credentials.username);
283
+ if (!record?.password) {
284
+ return null;
285
+ }
286
+ if (!await verifyPassword(credentials.password, record.password)) {
287
+ return null;
288
+ }
289
+ return {
290
+ id: record.id,
291
+ role: record.role,
292
+ emailVerifiedAt: record.email_verified_at ?? null
293
+ };
294
+ }
295
+ }
214
296
  // ../../src/core/auth/abilityCatalog.ts
215
297
  var MEMBER_ABILITIES = [
216
- "organizations:read",
217
- "organizations:create",
218
- "projects:read",
219
- "projects:create",
220
- "tasks:read",
221
- "tasks:create",
222
- "comments:read",
223
- "comments:create",
224
- "attachments:read",
225
- "attachments:create",
298
+ "profile:read",
226
299
  "auth:tokens:read",
227
300
  "auth:tokens:write",
228
301
  "auth:tokens:delete"
229
302
  ];
230
303
  var ADMIN_ABILITIES = [
231
304
  ...MEMBER_ABILITIES,
232
- "organizations:create",
233
- "organizations:update",
234
- "organizations:delete",
235
- "projects:update",
236
- "projects:delete",
237
- "tasks:update",
238
- "tasks:delete",
239
- "comments:update",
240
- "comments:delete",
241
- "attachments:delete",
242
305
  "webhooks:read",
243
306
  "webhooks:write",
244
- "audit:read"
307
+ "audit:read",
308
+ "audit:export"
245
309
  ];
246
310
  var PLATFORM_ADMIN_ABILITIES = ["*"];
247
311
  function resolveAbilitiesForRole(role) {
@@ -292,12 +356,8 @@ class ApiTokenGuard {
292
356
  this.options = options;
293
357
  }
294
358
  resolve(request) {
295
- const authorization = request.headers.get("authorization");
296
- if (!authorization?.startsWith("Bearer ")) {
297
- return null;
298
- }
299
- const token = authorization.slice("Bearer ".length).trim();
300
- if (token !== this.options.token) {
359
+ const token = readBearerToken(request);
360
+ if (!token || token !== this.options.token) {
301
361
  return null;
302
362
  }
303
363
  return this.options.user;
@@ -310,12 +370,8 @@ class DatabaseTokenGuard {
310
370
  this.container = container;
311
371
  }
312
372
  async resolve(request) {
313
- const authorization = request.headers.get("authorization");
314
- if (!authorization?.startsWith("Bearer ")) {
315
- return null;
316
- }
317
- const token = authorization.slice("Bearer ".length).trim();
318
- if (!token) {
373
+ const token = readBearerToken(request);
374
+ if (!token || token.split(".").length === 3) {
319
375
  return null;
320
376
  }
321
377
  const tokenService = resolveAuthUserDirectory(this.container);
@@ -341,15 +397,38 @@ class CompositeGuard {
341
397
  return null;
342
398
  }
343
399
  }
400
+ var BEARER_GUARD_NAMES = ["api", "access_token", "token", "jwt"];
401
+ var BASIC_GUARD_NAMES = ["basic"];
402
+ var SESSION_GUARD_NAMES = ["web", "session", "default"];
344
403
 
345
404
  class AuthManager {
346
405
  guard;
406
+ namedGuards = new Map;
347
407
  constructor(guard) {
348
408
  this.guard = guard;
409
+ this.namedGuards.set("default", guard);
410
+ }
411
+ registerGuard(name, next) {
412
+ const trimmed = name.trim();
413
+ if (!trimmed) {
414
+ throw new Error("Auth guard name must not be empty.");
415
+ }
416
+ this.namedGuards.set(trimmed, next);
417
+ return this;
418
+ }
419
+ use(name = "default") {
420
+ const found = this.namedGuards.get(name);
421
+ if (!found) {
422
+ throw new Error(`Unknown auth guard "${name}".`);
423
+ }
424
+ return found;
425
+ }
426
+ guardNames() {
427
+ return [...this.namedGuards.keys()];
349
428
  }
350
429
  async resolve(request) {
351
430
  if (request) {
352
- return await Promise.resolve(this.guard.resolve(request));
431
+ return await this.authenticateRequest(request);
353
432
  }
354
433
  return currentAuthUser();
355
434
  }
@@ -366,6 +445,167 @@ class AuthManager {
366
445
  }
367
446
  return user;
368
447
  }
448
+ async authenticateRequest(request) {
449
+ const scheme = authorizationScheme(request);
450
+ if (scheme === "bearer") {
451
+ const fromBearer = await this.tryNamedGuards(request, BEARER_GUARD_NAMES);
452
+ if (fromBearer) {
453
+ return fromBearer;
454
+ }
455
+ } else if (scheme === "basic") {
456
+ const fromBasic = await this.tryNamedGuards(request, BASIC_GUARD_NAMES);
457
+ if (fromBasic) {
458
+ return fromBasic;
459
+ }
460
+ } else {
461
+ const fromSession = await this.tryNamedGuards(request, SESSION_GUARD_NAMES);
462
+ if (fromSession) {
463
+ return fromSession;
464
+ }
465
+ }
466
+ return await Promise.resolve(this.guard.resolve(request));
467
+ }
468
+ async tryNamedGuards(request, names) {
469
+ const seen = new Set;
470
+ for (const name of names) {
471
+ const named = this.namedGuards.get(name);
472
+ if (!named || seen.has(named)) {
473
+ continue;
474
+ }
475
+ seen.add(named);
476
+ const user = await Promise.resolve(named.resolve(request));
477
+ if (user) {
478
+ return user;
479
+ }
480
+ }
481
+ return null;
482
+ }
483
+ }
484
+ // ../../src/core/auth/jwt.ts
485
+ import { createHmac, timingSafeEqual } from "crypto";
486
+
487
+ // ../../src/core/runtime/appKeyPrefix.ts
488
+ function appKeyPrefix() {
489
+ return process.env.APP_KEY_PREFIX?.trim() || "strata";
490
+ }
491
+ function appCookieName(kind) {
492
+ return `${appKeyPrefix()}_${kind}`;
493
+ }
494
+ function appDevSecret(kind) {
495
+ return `${appKeyPrefix()}-dev-${kind}`;
496
+ }
497
+ function namespacedRedisKey(kind) {
498
+ return `${appKeyPrefix()}:${kind}`;
499
+ }
500
+ function smtpEhloHost() {
501
+ const raw = process.env.MAIL_EHLO?.trim() || `${appKeyPrefix()}.local`;
502
+ const safe = raw.replace(/[^a-zA-Z0-9.-]/g, "");
503
+ return safe || "strata.local";
504
+ }
505
+ function otelServiceName() {
506
+ return process.env.OTEL_SERVICE_NAME?.trim() || `${appKeyPrefix()}-api`;
507
+ }
508
+ function appEnv() {
509
+ return process.env.APP_ENV?.trim() || "local";
510
+ }
511
+
512
+ // ../../src/core/auth/jwt.ts
513
+ function resolveJwtSecret(secret) {
514
+ return secret?.trim() || process.env.JWT_SECRET?.trim() || process.env.SESSION_SECRET?.trim() || appDevSecret("jwt-secret");
515
+ }
516
+ function jwtTtlSeconds(override) {
517
+ if (typeof override === "number" && Number.isInteger(override) && override > 0) {
518
+ return override;
519
+ }
520
+ const parsed = Number.parseInt(process.env.JWT_TTL_SECONDS ?? "", 10);
521
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : 3600;
522
+ }
523
+ function encodeJson(value) {
524
+ return Buffer.from(JSON.stringify(value)).toString("base64url");
525
+ }
526
+ function decodeJson(value) {
527
+ try {
528
+ return JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
529
+ } catch {
530
+ return null;
531
+ }
532
+ }
533
+ function signPart(headerAndPayload, secret) {
534
+ return createHmac("sha256", secret).update(headerAndPayload).digest("base64url");
535
+ }
536
+ function signaturesMatch(left, right) {
537
+ const leftBuffer = Buffer.from(left);
538
+ const rightBuffer = Buffer.from(right);
539
+ if (leftBuffer.length !== rightBuffer.length) {
540
+ return false;
541
+ }
542
+ return timingSafeEqual(leftBuffer, rightBuffer);
543
+ }
544
+ function signJwt(payload, options = {}) {
545
+ const now = Math.floor(Date.now() / 1000);
546
+ const body = {
547
+ ...payload,
548
+ iat: now,
549
+ exp: now + jwtTtlSeconds(options.ttlSeconds)
550
+ };
551
+ const header = encodeJson({ alg: "HS256", typ: "JWT" });
552
+ const data = encodeJson(body);
553
+ const unsigned = `${header}.${data}`;
554
+ const signature = signPart(unsigned, resolveJwtSecret(options.secret));
555
+ return `${unsigned}.${signature}`;
556
+ }
557
+ function verifyJwt(token, secret) {
558
+ const parts = token.split(".");
559
+ if (parts.length !== 3) {
560
+ return null;
561
+ }
562
+ const [header, data, signature] = parts;
563
+ if (!header || !data || !signature) {
564
+ return null;
565
+ }
566
+ const expected = signPart(`${header}.${data}`, resolveJwtSecret(secret));
567
+ if (!signaturesMatch(signature, expected)) {
568
+ return null;
569
+ }
570
+ const parsedHeader = decodeJson(header);
571
+ if (parsedHeader?.alg !== "HS256") {
572
+ return null;
573
+ }
574
+ const payload = decodeJson(data);
575
+ if (!payload || payload.sub === undefined || payload.sub === null) {
576
+ return null;
577
+ }
578
+ if (typeof payload.exp === "number" && payload.exp * 1000 <= Date.now()) {
579
+ return null;
580
+ }
581
+ return payload;
582
+ }
583
+ // ../../src/core/auth/jwtGuard.ts
584
+ function authUserFromJwt(payload) {
585
+ return {
586
+ id: payload.sub,
587
+ ...payload.role ? { role: String(payload.role) } : {},
588
+ ...Array.isArray(payload.abilities) ? { abilities: payload.abilities.map(String) } : {},
589
+ ...payload.emailVerifiedAt !== undefined ? { emailVerifiedAt: payload.emailVerifiedAt } : {}
590
+ };
591
+ }
592
+
593
+ class JwtGuard {
594
+ options;
595
+ constructor(options = {}) {
596
+ this.options = options;
597
+ }
598
+ resolve(request) {
599
+ const token = readBearerToken(request);
600
+ if (token?.split(".").length !== 3) {
601
+ return null;
602
+ }
603
+ const payload = verifyJwt(token, this.options.secret);
604
+ if (!payload) {
605
+ return null;
606
+ }
607
+ return authUserFromJwt(payload);
608
+ }
369
609
  }
370
610
  // ../../src/core/auth/membershipContext.ts
371
611
  var membershipContext = createAsyncContextStore("@getstrata/membershipContext");
@@ -841,14 +1081,14 @@ function getDefaultDatabaseQuery() {
841
1081
  }
842
1082
 
843
1083
  // ../../src/core/security/timingSafeCompare.ts
844
- import { timingSafeEqual } from "crypto";
1084
+ import { timingSafeEqual as timingSafeEqual2 } from "crypto";
845
1085
  function timingSafeCompareString(left, right) {
846
1086
  const leftBuffer = Buffer.from(left);
847
1087
  const rightBuffer = Buffer.from(right);
848
1088
  if (leftBuffer.length !== rightBuffer.length) {
849
1089
  return false;
850
1090
  }
851
- return timingSafeEqual(leftBuffer, rightBuffer);
1091
+ return timingSafeEqual2(leftBuffer, rightBuffer);
852
1092
  }
853
1093
 
854
1094
  // ../../src/core/security/scimTenantTokens.ts
@@ -988,35 +1228,26 @@ function jsonScimError(detail, status) {
988
1228
  headers: { "content-type": "application/scim+json" }
989
1229
  });
990
1230
  }
991
- // ../../src/core/cache/redisCacheStore.ts
992
- var {RedisClient } = globalThis.Bun;
993
-
994
- // ../../src/core/runtime/appKeyPrefix.ts
995
- function appKeyPrefix() {
996
- return process.env.APP_KEY_PREFIX?.trim() || "strata";
997
- }
998
- function appCookieName(kind) {
999
- return `${appKeyPrefix()}_${kind}`;
1000
- }
1001
- function appDevSecret(kind) {
1002
- return `${appKeyPrefix()}-dev-${kind}`;
1003
- }
1004
- function namespacedRedisKey(kind) {
1005
- return `${appKeyPrefix()}:${kind}`;
1006
- }
1007
- function smtpEhloHost() {
1008
- const raw = process.env.MAIL_EHLO?.trim() || `${appKeyPrefix()}.local`;
1009
- const safe = raw.replace(/[^a-zA-Z0-9.-]/g, "");
1010
- return safe || "strata.local";
1011
- }
1012
- function otelServiceName() {
1013
- return process.env.OTEL_SERVICE_NAME?.trim() || `${appKeyPrefix()}-api`;
1231
+ // ../../src/core/auth/tokenAbilityChecker.ts
1232
+ function tokenCan(user, ability) {
1233
+ if (!user) {
1234
+ return false;
1235
+ }
1236
+ const abilities = user.abilities ?? [];
1237
+ return abilities.includes("*") || abilities.includes(ability);
1014
1238
  }
1015
- function appEnv() {
1016
- return process.env.APP_ENV?.trim() || "local";
1239
+ function createTokenAbilityChecker() {
1240
+ return {
1241
+ tokenCan,
1242
+ requireAbility(user, ability) {
1243
+ if (!tokenCan(user, ability)) {
1244
+ throw new ForbiddenError(`Missing ability: ${ability}`);
1245
+ }
1246
+ }
1247
+ };
1017
1248
  }
1018
-
1019
1249
  // ../../src/core/cache/redisCacheStore.ts
1250
+ var {RedisClient } = globalThis.Bun;
1020
1251
  function cacheKeyPrefix() {
1021
1252
  return namespacedRedisKey("cache:");
1022
1253
  }
@@ -1455,7 +1686,13 @@ var CACHE_TAGS = {
1455
1686
  tasks: "tasks",
1456
1687
  comments: "comments",
1457
1688
  attachments: "attachments",
1458
- reports: "reports"
1689
+ reports: "reports",
1690
+ users: "users",
1691
+ departments: "departments",
1692
+ positions: "positions",
1693
+ applications: "applications",
1694
+ careers: "careers",
1695
+ offers: "offers"
1459
1696
  };
1460
1697
  // ../../src/core/contracts/container.ts
1461
1698
  class ServiceContainer {
@@ -1644,12 +1881,155 @@ async function withDatabaseErrorHandling(operation) {
1644
1881
  }
1645
1882
  }
1646
1883
 
1647
- // ../../src/core/database/query.ts
1648
- function quoteIdentifier(identifier) {
1884
+ // ../../src/core/database/schema/driver.ts
1885
+ function normalizeConnectionName(connection) {
1886
+ const normalized = connection.trim().toLowerCase();
1887
+ if (normalized === "pgsql" || normalized === "postgres" || normalized === "postgresql") {
1888
+ return "pgsql";
1889
+ }
1890
+ if (normalized === "mysql" || normalized === "mariadb") {
1891
+ return "mysql";
1892
+ }
1893
+ if (normalized === "sqlite") {
1894
+ return "sqlite";
1895
+ }
1896
+ throw new Error(`Unsupported DB_CONNECTION: ${connection}`);
1897
+ }
1898
+ function resolveDriverFromUrl(url) {
1899
+ const normalized = url.trim().toLowerCase();
1900
+ if (normalized.startsWith("postgres://") || normalized.startsWith("postgresql://") || normalized.startsWith("postgres:")) {
1901
+ return "pgsql";
1902
+ }
1903
+ if (normalized.startsWith("mysql://") || normalized.startsWith("mysql:")) {
1904
+ return "mysql";
1905
+ }
1906
+ if (normalized.startsWith("sqlite:")) {
1907
+ return "sqlite";
1908
+ }
1909
+ return null;
1910
+ }
1911
+ function resolveDatabaseDriver(options = {}) {
1912
+ const connection = options.connection ?? process.env.DB_CONNECTION;
1913
+ if (connection) {
1914
+ return normalizeConnectionName(connection);
1915
+ }
1916
+ const url = options.url ?? process.env.DATABASE_URL ?? "";
1917
+ const fromUrl = resolveDriverFromUrl(url);
1918
+ if (fromUrl) {
1919
+ return fromUrl;
1920
+ }
1921
+ return "pgsql";
1922
+ }
1923
+
1924
+ // ../../src/core/database/dialect.ts
1925
+ function assertSafeIdentifier(identifier) {
1649
1926
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(identifier)) {
1650
1927
  throw new Error(`Invalid SQL identifier: ${identifier}`);
1651
1928
  }
1652
- return `"${identifier}"`;
1929
+ return identifier;
1930
+ }
1931
+ var postgresDialect = {
1932
+ driver: "pgsql",
1933
+ placeholder(index) {
1934
+ return `$${index}`;
1935
+ },
1936
+ quoteIdentifier(identifier) {
1937
+ return `"${assertSafeIdentifier(identifier)}"`;
1938
+ },
1939
+ nowExpression() {
1940
+ return "NOW()";
1941
+ },
1942
+ returningClause(columns) {
1943
+ return ` RETURNING ${columns}`;
1944
+ },
1945
+ ilikeOperator() {
1946
+ return "ILIKE";
1947
+ },
1948
+ nullsLastSuffix() {
1949
+ return " NULLS LAST";
1950
+ },
1951
+ castToText(expression) {
1952
+ return `${expression}::text`;
1953
+ }
1954
+ };
1955
+ var mysqlDialect = {
1956
+ driver: "mysql",
1957
+ placeholder() {
1958
+ return "?";
1959
+ },
1960
+ quoteIdentifier(identifier) {
1961
+ return `\`${assertSafeIdentifier(identifier)}\``;
1962
+ },
1963
+ nowExpression() {
1964
+ return "CURRENT_TIMESTAMP";
1965
+ },
1966
+ returningClause() {
1967
+ return "";
1968
+ },
1969
+ ilikeOperator() {
1970
+ return "LIKE";
1971
+ },
1972
+ nullsLastSuffix() {
1973
+ return "";
1974
+ },
1975
+ castToText(expression) {
1976
+ return `CAST(${expression} AS CHAR)`;
1977
+ }
1978
+ };
1979
+ var sqliteDialect = {
1980
+ driver: "sqlite",
1981
+ placeholder() {
1982
+ return "?";
1983
+ },
1984
+ quoteIdentifier(identifier) {
1985
+ return `"${assertSafeIdentifier(identifier)}"`;
1986
+ },
1987
+ nowExpression() {
1988
+ return "CURRENT_TIMESTAMP";
1989
+ },
1990
+ returningClause(columns) {
1991
+ return ` RETURNING ${columns}`;
1992
+ },
1993
+ ilikeOperator() {
1994
+ return "LIKE";
1995
+ },
1996
+ nullsLastSuffix() {
1997
+ return "";
1998
+ },
1999
+ castToText(expression) {
2000
+ return `CAST(${expression} AS TEXT)`;
2001
+ }
2002
+ };
2003
+ var dialects = {
2004
+ pgsql: postgresDialect,
2005
+ mysql: mysqlDialect,
2006
+ sqlite: sqliteDialect
2007
+ };
2008
+ var dialectContext = createAsyncContextStore("@getstrata/sqlDialect");
2009
+ var dialectOverride = null;
2010
+ function dialectFor(driver) {
2011
+ return dialects[driver];
2012
+ }
2013
+ function currentSqlDialect() {
2014
+ return dialectContext.getStore() ?? dialectOverride ?? dialectFor(resolveDatabaseDriver());
2015
+ }
2016
+ function useSqlDialect(driver) {
2017
+ dialectOverride = dialectFor(driver);
2018
+ return dialectOverride;
2019
+ }
2020
+ function resetSqlDialect() {
2021
+ dialectOverride = null;
2022
+ }
2023
+ function runWithSqlDialect(driver, callback) {
2024
+ return dialectContext.run(dialectFor(driver), callback);
2025
+ }
2026
+
2027
+ // ../../src/core/database/query.ts
2028
+ function quoteIdentifier(identifier) {
2029
+ return currentSqlDialect().quoteIdentifier(identifier);
2030
+ }
2031
+ function returningSuffix(columns) {
2032
+ return currentSqlDialect().returningClause(columns);
1653
2033
  }
1654
2034
  function qualifyColumn(tableName, column) {
1655
2035
  return `${quoteIdentifier(tableName)}.${quoteIdentifier(column)}`;
@@ -1679,7 +2059,7 @@ function isQueryOperator(value) {
1679
2059
  }
1680
2060
  function pushParam(values, value) {
1681
2061
  values.push(value);
1682
- return `$${values.length}`;
2062
+ return currentSqlDialect().placeholder(values.length);
1683
2063
  }
1684
2064
  function buildInClause(column, values, params) {
1685
2065
  if (values.length === 0) {
@@ -1719,9 +2099,12 @@ function buildOperatorClauses(column, operator, params) {
1719
2099
  clauses.push(`${column} <= ${pushParam(params, operator.lte)}`);
1720
2100
  }
1721
2101
  if (operator.ilike !== undefined) {
1722
- clauses.push(`${column} ILIKE ${pushParam(params, operator.ilike)}`);
2102
+ clauses.push(`${column} ${currentSqlDialect().ilikeOperator()} ${pushParam(params, operator.ilike)}`);
1723
2103
  }
1724
2104
  if (operator.tsMatch !== undefined) {
2105
+ if (currentSqlDialect().driver !== "pgsql") {
2106
+ throw new Error("Full-text search (tsMatch) is only available on PostgreSQL.");
2107
+ }
1725
2108
  clauses.push(`${column} @@ plainto_tsquery('english', ${pushParam(params, operator.tsMatch)})`);
1726
2109
  }
1727
2110
  return clauses;
@@ -1912,7 +2295,7 @@ function buildSelectList(table, select, params = []) {
1912
2295
  return item.as ? `${column2} AS ${quoteIdentifier(item.as)}` : column2;
1913
2296
  }
1914
2297
  if (item.kind === "literalText") {
1915
- return `${pushParam(params, item.value)}::text AS ${quoteIdentifier(item.as)}`;
2298
+ return `${currentSqlDialect().castToText(pushParam(params, item.value))} AS ${quoteIdentifier(item.as)}`;
1916
2299
  }
1917
2300
  const column = qualifyColumn(item.table, item.column);
1918
2301
  const placeholder = pushParam(params, item.query);
@@ -2004,7 +2387,7 @@ function buildInsertQuery(table, values) {
2004
2387
  const placeholders = entries.map(([, value]) => pushParam(params, value)).join(", ");
2005
2388
  const returningColumns = buildReturningColumns(table);
2006
2389
  return {
2007
- text: `INSERT INTO ${quoteIdentifier(table.name)} (${columns}) VALUES (${placeholders}) RETURNING ${returningColumns}`,
2390
+ text: `INSERT INTO ${quoteIdentifier(table.name)} (${columns}) VALUES (${placeholders})${returningSuffix(returningColumns)}`,
2008
2391
  params
2009
2392
  };
2010
2393
  }
@@ -2023,7 +2406,7 @@ function buildUpdateQuery(table, id, changes) {
2023
2406
  appendSoftDeleteScope(table, {}, scopeClauses);
2024
2407
  const scopeSuffix = scopeClauses.length > 0 ? ` AND ${scopeClauses.join(" AND ")}` : "";
2025
2408
  return {
2026
- text: `UPDATE ${quoteIdentifier(table.name)} SET ${setClause} WHERE ${quoteIdentifier(table.primaryKey)} = ${primaryKeyPlaceholder}${scopeSuffix} RETURNING ${returningColumns}`,
2409
+ text: `UPDATE ${quoteIdentifier(table.name)} SET ${setClause} WHERE ${quoteIdentifier(table.primaryKey)} = ${primaryKeyPlaceholder}${scopeSuffix}${returningSuffix(returningColumns)}`,
2027
2410
  params
2028
2411
  };
2029
2412
  }
@@ -2036,9 +2419,12 @@ function buildSoftDeleteByIdQuery(table, id, deletedAt) {
2036
2419
  const scopeClauses = [];
2037
2420
  appendSoftDeleteScope(table, {}, scopeClauses);
2038
2421
  const scopeSuffix = scopeClauses.length > 0 ? ` AND ${scopeClauses.join(" AND ")}` : "";
2422
+ const params = [];
2423
+ const deletedAtPlaceholder = pushParam(params, deletedAt);
2424
+ const idPlaceholder = pushParam(params, id);
2039
2425
  return {
2040
- text: `UPDATE ${quoteIdentifier(table.name)} SET ${quoteIdentifier(deletedAtColumn)} = $1 WHERE ${quoteIdentifier(table.primaryKey)} = $2${scopeSuffix} RETURNING ${returningColumns}`,
2041
- params: [deletedAt, id]
2426
+ text: `UPDATE ${quoteIdentifier(table.name)} SET ${quoteIdentifier(deletedAtColumn)} = ${deletedAtPlaceholder} WHERE ${quoteIdentifier(table.primaryKey)} = ${idPlaceholder}${scopeSuffix}${returningSuffix(returningColumns)}`,
2427
+ params
2042
2428
  };
2043
2429
  }
2044
2430
  function buildRestoreByIdQuery(table, id) {
@@ -2047,15 +2433,21 @@ function buildRestoreByIdQuery(table, id) {
2047
2433
  throw new Error(`Table ${table.name} does not support soft deletes.`);
2048
2434
  }
2049
2435
  const returningColumns = buildReturningColumns(table);
2436
+ const params = [];
2437
+ const deletedAtPlaceholder = pushParam(params, null);
2438
+ const idPlaceholder = pushParam(params, id);
2050
2439
  return {
2051
- text: `UPDATE ${quoteIdentifier(table.name)} SET ${quoteIdentifier(deletedAtColumn)} = $1 WHERE ${quoteIdentifier(table.primaryKey)} = $2 AND ${qualifyColumn(table.name, deletedAtColumn)} IS NOT NULL RETURNING ${returningColumns}`,
2052
- params: [null, id]
2440
+ text: `UPDATE ${quoteIdentifier(table.name)} SET ${quoteIdentifier(deletedAtColumn)} = ${deletedAtPlaceholder} WHERE ${quoteIdentifier(table.primaryKey)} = ${idPlaceholder} AND ${qualifyColumn(table.name, deletedAtColumn)} IS NOT NULL${returningSuffix(returningColumns)}`,
2441
+ params
2053
2442
  };
2054
2443
  }
2055
2444
  function buildDeleteByIdQuery(table, id) {
2445
+ const params = [];
2446
+ const idPlaceholder = pushParam(params, id);
2447
+ const returning = returningSuffix(`${quoteIdentifier(table.primaryKey)} AS ${quoteIdentifier("deleted_id")}`);
2056
2448
  return {
2057
- text: `DELETE FROM ${quoteIdentifier(table.name)} WHERE ${quoteIdentifier(table.primaryKey)} = $1 RETURNING ${quoteIdentifier(table.primaryKey)} AS ${quoteIdentifier("deleted_id")}`,
2058
- params: [id]
2449
+ text: `DELETE FROM ${quoteIdentifier(table.name)} WHERE ${quoteIdentifier(table.primaryKey)} = ${idPlaceholder}${returning}`,
2450
+ params
2059
2451
  };
2060
2452
  }
2061
2453
 
@@ -2078,6 +2470,13 @@ function belongsTo(definition) {
2078
2470
  ...definition
2079
2471
  };
2080
2472
  }
2473
+ function hasManyThrough(definition) {
2474
+ return {
2475
+ type: "hasManyThrough",
2476
+ throughParentKey: "__through_parent_id",
2477
+ ...definition
2478
+ };
2479
+ }
2081
2480
  function belongsToMany(definition) {
2082
2481
  return {
2083
2482
  type: "belongsToMany",
@@ -2099,6 +2498,19 @@ function relationMatchKey(value) {
2099
2498
  }
2100
2499
  return String(value);
2101
2500
  }
2501
+ var relationLookupCache = new WeakMap;
2502
+ function indexedRelationLookup(map) {
2503
+ const cached = relationLookupCache.get(map);
2504
+ if (cached) {
2505
+ return cached;
2506
+ }
2507
+ const indexed = new Map;
2508
+ for (const [existing, value] of map) {
2509
+ indexed.set(relationMatchKey(existing), value);
2510
+ }
2511
+ relationLookupCache.set(map, indexed);
2512
+ return indexed;
2513
+ }
2102
2514
  function getByRelationKey(map, key) {
2103
2515
  if (map.has(key)) {
2104
2516
  return map.get(key);
@@ -2107,12 +2519,7 @@ function getByRelationKey(map, key) {
2107
2519
  if (want === "") {
2108
2520
  return;
2109
2521
  }
2110
- for (const [existing, value] of map) {
2111
- if (relationMatchKey(existing) === want) {
2112
- return value;
2113
- }
2114
- }
2115
- return;
2522
+ return indexedRelationLookup(map).get(want);
2116
2523
  }
2117
2524
  function indexHasManyRelation(parents, children, relation) {
2118
2525
  const groups = new Map;
@@ -2251,6 +2658,26 @@ function indexMorphOneRelation(parents, children, relation) {
2251
2658
  }
2252
2659
  return result;
2253
2660
  }
2661
+ function indexHasManyThroughRelation(parents, children, relation) {
2662
+ const throughKey = relation.throughParentKey ?? "__through_parent_id";
2663
+ const grouped = new Map;
2664
+ for (const child of children) {
2665
+ const key = relationMatchKey(child[throughKey]);
2666
+ if (key === "") {
2667
+ continue;
2668
+ }
2669
+ const existing = grouped.get(key) ?? [];
2670
+ const { [throughKey]: _through, ...far } = child;
2671
+ existing.push(far);
2672
+ grouped.set(key, existing);
2673
+ }
2674
+ const result = new Map;
2675
+ for (const parent of parents) {
2676
+ const key = relationMatchKey(parent[relation.localKey]);
2677
+ result.set(parent[relation.localKey], grouped.get(key) ?? []);
2678
+ }
2679
+ return result;
2680
+ }
2254
2681
  function indexMorphToRelation(children, parentsByType, relation) {
2255
2682
  const result = new Map;
2256
2683
  for (const child of children) {
@@ -2434,6 +2861,24 @@ class RepositoryQuery {
2434
2861
  });
2435
2862
  return this;
2436
2863
  }
2864
+ withHasManyThrough(as, relation, farRepository, options = {}) {
2865
+ this.eagerLoads.push({
2866
+ kind: "hasManyThrough",
2867
+ as,
2868
+ relation,
2869
+ repository: farRepository,
2870
+ options
2871
+ });
2872
+ return this;
2873
+ }
2874
+ withTrashed() {
2875
+ this.queryOptions = { ...this.queryOptions, withTrashed: true };
2876
+ return this;
2877
+ }
2878
+ onlyTrashed() {
2879
+ this.queryOptions = { ...this.queryOptions, onlyTrashed: true };
2880
+ return this;
2881
+ }
2437
2882
  async get() {
2438
2883
  const rows = await this.repository.findAll(this.buildOptions());
2439
2884
  return await this.attach(rows);
@@ -2487,64 +2932,67 @@ class RepositoryQuery {
2487
2932
  return this;
2488
2933
  }
2489
2934
  async attach(rows) {
2490
- if (rows.length === 0 || this.eagerLoads.length === 0) {
2491
- return rows.map((row) => ({ ...row }));
2492
- }
2493
- let result = rows.map((row) => ({ ...row }));
2494
- for (const load of this.eagerLoads) {
2495
- if (load.kind === "hasMany") {
2496
- const relation2 = load.relation;
2497
- const grouped2 = await load.repository.withConnection(this.repository.getConnection()).loadHasManyForParents(rows, relation2, load.options);
2498
- result = result.map((row) => ({
2499
- ...row,
2500
- [load.as]: getByRelationKey(grouped2, row[relation2.localKey]) ?? []
2501
- }));
2502
- continue;
2935
+ const result = rows.map((row) => ({ ...row }));
2936
+ if (result.length === 0 || this.eagerLoads.length === 0) {
2937
+ return result;
2938
+ }
2939
+ await Promise.all(this.eagerLoads.map((load) => this.hydrateEagerLoad(rows, result, load)));
2940
+ return result;
2941
+ }
2942
+ async hydrateEagerLoad(rows, result, load) {
2943
+ if (load.kind === "hasMany") {
2944
+ const relation2 = load.relation;
2945
+ const grouped2 = await load.repository.withConnection(this.repository.getConnection()).loadHasManyForParents(rows, relation2, load.options);
2946
+ for (const row of result) {
2947
+ row[load.as] = getByRelationKey(grouped2, row[relation2.localKey]) ?? [];
2503
2948
  }
2504
- if (load.kind === "morphMany") {
2505
- const relation2 = load.relation;
2506
- const grouped2 = await load.repository.withConnection(this.repository.getConnection()).loadMorphManyForParents(rows, relation2, load.options);
2507
- result = result.map((row) => ({
2508
- ...row,
2509
- [load.as]: getByRelationKey(grouped2, row[relation2.localKey]) ?? []
2510
- }));
2511
- continue;
2949
+ return;
2950
+ }
2951
+ if (load.kind === "morphMany") {
2952
+ const relation2 = load.relation;
2953
+ const grouped2 = await load.repository.withConnection(this.repository.getConnection()).loadMorphManyForParents(rows, relation2, load.options);
2954
+ for (const row of result) {
2955
+ row[load.as] = getByRelationKey(grouped2, row[relation2.localKey]) ?? [];
2512
2956
  }
2513
- if (load.kind === "morphOne") {
2514
- const relation2 = load.relation;
2515
- const grouped2 = await load.repository.withConnection(this.repository.getConnection()).loadMorphOneForParents(rows, relation2, load.options);
2516
- result = result.map((row) => ({
2517
- ...row,
2518
- [load.as]: getByRelationKey(grouped2, row[relation2.localKey])
2519
- }));
2520
- continue;
2957
+ return;
2958
+ }
2959
+ if (load.kind === "morphOne") {
2960
+ const relation2 = load.relation;
2961
+ const grouped2 = await load.repository.withConnection(this.repository.getConnection()).loadMorphOneForParents(rows, relation2, load.options);
2962
+ for (const row of result) {
2963
+ row[load.as] = getByRelationKey(grouped2, row[relation2.localKey]);
2521
2964
  }
2522
- if (load.kind === "belongsToMany") {
2523
- const relation2 = load.relation;
2524
- const grouped2 = await this.repository.loadBelongsToManyForParents(rows, relation2, load.repository, load.options);
2525
- result = result.map((row) => ({
2526
- ...row,
2527
- [load.as]: getByRelationKey(grouped2, row[relation2.parentKey]) ?? []
2528
- }));
2529
- continue;
2965
+ return;
2966
+ }
2967
+ if (load.kind === "hasManyThrough") {
2968
+ const relation2 = load.relation;
2969
+ const grouped2 = await load.repository.withConnection(this.repository.getConnection()).loadHasManyThroughForParents(rows, relation2, load.options);
2970
+ for (const row of result) {
2971
+ row[load.as] = getByRelationKey(grouped2, row[relation2.localKey]) ?? [];
2530
2972
  }
2531
- if (load.kind === "morphTo") {
2532
- const relation2 = load.relation;
2533
- const grouped2 = await this.repository.loadMorphToForChildren(rows, relation2, load.morphRepositories ?? new Map, load.options);
2534
- result = result.map((row) => ({
2535
- ...row,
2536
- [load.as]: getByRelationKey(grouped2, row[relation2.morphIdKey])
2537
- }));
2538
- continue;
2973
+ return;
2974
+ }
2975
+ if (load.kind === "belongsToMany") {
2976
+ const relation2 = load.relation;
2977
+ const grouped2 = await this.repository.loadBelongsToManyForParents(rows, relation2, load.repository, load.options);
2978
+ for (const row of result) {
2979
+ row[load.as] = getByRelationKey(grouped2, row[relation2.parentKey]) ?? [];
2539
2980
  }
2540
- const relation = load.relation;
2541
- const grouped = await this.repository.loadBelongsToForParents(rows, relation, load.repository, load.options);
2542
- result = result.map((row) => ({
2543
- ...row,
2544
- [load.as]: getByRelationKey(grouped, row[relation.foreignKey])
2545
- }));
2981
+ return;
2982
+ }
2983
+ if (load.kind === "morphTo") {
2984
+ const relation2 = load.relation;
2985
+ const grouped2 = await this.repository.loadMorphToForChildren(rows, relation2, load.morphRepositories ?? new Map, load.options);
2986
+ for (const row of result) {
2987
+ row[load.as] = getByRelationKey(grouped2, row[relation2.morphIdKey]);
2988
+ }
2989
+ return;
2990
+ }
2991
+ const relation = load.relation;
2992
+ const grouped = await this.repository.loadBelongsToForParents(rows, relation, load.repository, load.options);
2993
+ for (const row of result) {
2994
+ row[load.as] = getByRelationKey(grouped, row[relation.foreignKey]);
2546
2995
  }
2547
- return result;
2548
2996
  }
2549
2997
  }
2550
2998
 
@@ -2807,6 +3255,55 @@ class BaseRepository {
2807
3255
  }, options);
2808
3256
  return indexHasManyRelation(parents, children, relation);
2809
3257
  }
3258
+ async findHasManyThrough(parentId, relation, options = {}) {
3259
+ const grouped = await this.loadHasManyThroughForParents([{ [relation.localKey]: parentId }], relation, options);
3260
+ return getByRelationKey(grouped, parentId) ?? [];
3261
+ }
3262
+ async loadHasManyThroughForParents(parents, relation, options = {}) {
3263
+ if (parents.length === 0) {
3264
+ return indexHasManyThroughRelation(parents, [], relation);
3265
+ }
3266
+ const parentIds = [...new Set(parents.map((parent) => parent[relation.localKey]))];
3267
+ const throughParentKey = relation.throughParentKey ?? "__through_parent_id";
3268
+ const farTable = this.table.name;
3269
+ const columns = this.table.columns.map((column) => `${qualifyColumn(farTable, column)}`).join(", ");
3270
+ const placeholders = parentIds.map((_, index) => `$${index + 1}`).join(", ");
3271
+ const { text: extraWhere, params: extraParams } = this.buildThroughWhere(options, parentIds.length);
3272
+ const softDelete = this.throughSoftDeleteClause(options);
3273
+ const sql = `SELECT ${columns}, ${qualifyColumn(relation.throughTable, relation.firstKey)} AS ${throughParentKey} FROM ${quoteIdentifier(farTable)} INNER JOIN ${quoteIdentifier(relation.throughTable)} ON ${qualifyColumn(relation.throughTable, relation.secondLocalKey)} = ${qualifyColumn(farTable, relation.secondKey)} WHERE ${qualifyColumn(relation.throughTable, relation.firstKey)} IN (${placeholders})${softDelete}${extraWhere}`;
3274
+ const children = await this.connection.unsafe(sql, [
3275
+ ...parentIds,
3276
+ ...extraParams
3277
+ ]);
3278
+ return indexHasManyThroughRelation(parents, children, relation);
3279
+ }
3280
+ throughSoftDeleteClause(options) {
3281
+ const column = resolveSoftDeleteColumn(this.table);
3282
+ if (!column) {
3283
+ return "";
3284
+ }
3285
+ const qualified = qualifyColumn(this.table.name, column);
3286
+ if (options.onlyTrashed) {
3287
+ return ` AND ${qualified} IS NOT NULL`;
3288
+ }
3289
+ if (options.withTrashed) {
3290
+ return "";
3291
+ }
3292
+ return ` AND ${qualified} IS NULL`;
3293
+ }
3294
+ buildThroughWhere(options, paramOffset = 1) {
3295
+ const where = options.where ?? {};
3296
+ const entries = Object.entries(where);
3297
+ if (entries.length === 0) {
3298
+ return { text: "", params: [] };
3299
+ }
3300
+ const params = [];
3301
+ const clauses = entries.map(([column, value], index) => {
3302
+ params.push(value);
3303
+ return `${qualifyColumn(this.table.name, column)} = $${paramOffset + index + 1}`;
3304
+ });
3305
+ return { text: ` AND ${clauses.join(" AND ")}`, params };
3306
+ }
2810
3307
  async loadBelongsToForParents(children, relation, parentRepository, options = {}) {
2811
3308
  if (children.length === 0) {
2812
3309
  return new Map;
@@ -2850,10 +3347,10 @@ class BaseRepository {
2850
3347
  idsByType.set(morphType, ids);
2851
3348
  }
2852
3349
  const parentsByType = new Map;
2853
- for (const [morphType, ids] of idsByType) {
3350
+ await Promise.all([...idsByType.entries()].map(async ([morphType, ids]) => {
2854
3351
  const repository = repositoriesByType.get(morphType);
2855
3352
  if (!repository) {
2856
- continue;
3353
+ return;
2857
3354
  }
2858
3355
  const ownerKey = repository.getTable().primaryKey;
2859
3356
  const parents = await repository.withConnection(this.connection).findWhere({
@@ -2864,7 +3361,7 @@ class BaseRepository {
2864
3361
  indexed.set(relationMatchKey(parent[ownerKey]), parent);
2865
3362
  }
2866
3363
  parentsByType.set(morphType, indexed);
2867
- }
3364
+ }));
2868
3365
  return indexMorphToRelation(children, parentsByType, relation);
2869
3366
  }
2870
3367
  async loadBelongsToManyForParents(parents, relation, relatedRepository, options = {}) {
@@ -2872,7 +3369,8 @@ class BaseRepository {
2872
3369
  return indexBelongsToManyRelation(parents, [], [], relation);
2873
3370
  }
2874
3371
  const parentIds = [...new Set(parents.map((parent) => parent[relation.parentKey]))];
2875
- const pivotRows = await this.connection.unsafe(`SELECT * FROM ${relation.pivotTable} WHERE ${String(relation.foreignPivotKey)} = ANY($1)`, [parentIds]);
3372
+ const placeholders = parentIds.map((_, index) => `$${index + 1}`).join(", ");
3373
+ const pivotRows = await this.connection.unsafe(`SELECT * FROM ${relation.pivotTable} WHERE ${String(relation.foreignPivotKey)} IN (${placeholders})`, parentIds);
2876
3374
  if (pivotRows.length === 0) {
2877
3375
  return indexBelongsToManyRelation(parents, [], [], relation);
2878
3376
  }
@@ -3065,10 +3563,11 @@ class Factory {
3065
3563
  return made;
3066
3564
  }
3067
3565
  async createOne(overrides = {}) {
3068
- const created = await this.persist(this.insertable(this.makeOne(overrides)));
3069
- for (const child of this.children) {
3070
- await child.factory.for(created, child.foreignKey).create();
3071
- }
3566
+ return this.persistCreated(this.makeOne(overrides));
3567
+ }
3568
+ async persistCreated(record) {
3569
+ const created = await this.persist(this.insertable(record));
3570
+ await Promise.all(this.children.map((child) => child.factory.for(created, child.foreignKey).create()));
3072
3571
  for (const callback of this.afterCreatingCallbacks) {
3073
3572
  await callback(created);
3074
3573
  }
@@ -3306,11 +3805,7 @@ class HasManyRelationQuery {
3306
3805
  return this.create(related);
3307
3806
  }
3308
3807
  async createMany(records) {
3309
- const created = [];
3310
- for (const attributes of records) {
3311
- created.push(await this.create(attributes));
3312
- }
3313
- return created;
3808
+ return Promise.all(records.map((attributes) => this.create(attributes)));
3314
3809
  }
3315
3810
  }
3316
3811
 
@@ -3534,7 +4029,8 @@ class BelongsToManyRelationQuery {
3534
4029
  return;
3535
4030
  }
3536
4031
  const list = Array.isArray(ids) ? ids : [ids];
3537
- await this.connection().unsafe(`DELETE FROM ${this.relation.pivotTable} WHERE ${String(this.relation.foreignPivotKey)} = $1 AND ${String(this.relation.relatedPivotKey)} = ANY($2)`, [parentId, list]);
4032
+ const placeholders = list.map((_, index) => `$${index + 2}`).join(", ");
4033
+ await this.connection().unsafe(`DELETE FROM ${this.relation.pivotTable} WHERE ${String(this.relation.foreignPivotKey)} = $1 AND ${String(this.relation.relatedPivotKey)} IN (${placeholders})`, [parentId, ...list]);
3538
4034
  }
3539
4035
  async sync(ids) {
3540
4036
  await this.detach();
@@ -3716,6 +4212,65 @@ class MorphToRelationQuery {
3716
4212
  }
3717
4213
  }
3718
4214
 
4215
+ class HasManyThroughRelationQuery {
4216
+ parent;
4217
+ related;
4218
+ relation;
4219
+ kind = "hasManyThrough";
4220
+ extraWhere = {};
4221
+ extraOptions = {};
4222
+ constructor(parent, related, relation) {
4223
+ this.parent = parent;
4224
+ this.related = related;
4225
+ this.relation = relation;
4226
+ }
4227
+ where(where) {
4228
+ this.extraWhere = { ...this.extraWhere, ...where };
4229
+ return this;
4230
+ }
4231
+ orderBy(orderBy) {
4232
+ this.extraOptions = { ...this.extraOptions, orderBy };
4233
+ return this;
4234
+ }
4235
+ limit(limit) {
4236
+ this.extraOptions = { ...this.extraOptions, limit };
4237
+ return this;
4238
+ }
4239
+ applyEagerLoad(query, alias) {
4240
+ query.withHasManyThrough(alias, this.relation, this.related.repository(), this.extraOptions);
4241
+ }
4242
+ hydrateEager(row, alias) {
4243
+ const value = row[alias] ?? [];
4244
+ const rows = Array.isArray(value) ? value : [];
4245
+ return rows.map((item) => this.related.newFromRecord(item));
4246
+ }
4247
+ toExistsClause(parentTable) {
4248
+ const farTable = this.related.repository().getTable().name;
4249
+ const extra = buildAdvancedWhereClause(farTable, this.extraWhere, [], []);
4250
+ const extraSql = extra.clause.replace(/^ WHERE /, "");
4251
+ const sql = `SELECT 1 FROM ${quoteIdentifier(farTable)} INNER JOIN ${quoteIdentifier(this.relation.throughTable)} ON ${qualifyColumn(this.relation.throughTable, this.relation.secondLocalKey)} = ${qualifyColumn(farTable, this.relation.secondKey)} WHERE ${qualifyColumn(this.relation.throughTable, this.relation.firstKey)} = ${qualifyColumn(parentTable, this.relation.localKey)}${extraSql ? ` AND ${extraSql}` : ""}`;
4252
+ return { sql, params: extra.params };
4253
+ }
4254
+ async get() {
4255
+ const rows = await this.related.repository().withConnection(this.parent.getRepository().getConnection()).findHasManyThrough(this.parent.get(this.relation.localKey), this.relation, {
4256
+ ...this.extraOptions,
4257
+ where: this.extraWhere
4258
+ });
4259
+ return rows.map((row) => this.related.newFromRecord(row));
4260
+ }
4261
+ async first() {
4262
+ const rows = await this.limit(1).get();
4263
+ return rows[0] ?? null;
4264
+ }
4265
+ async count() {
4266
+ const rows = await this.get();
4267
+ return rows.length;
4268
+ }
4269
+ then(onfulfilled, onrejected) {
4270
+ return thenGet(() => this.get(), onfulfilled, onrejected);
4271
+ }
4272
+ }
4273
+
3719
4274
  // ../../src/core/database/model.ts
3720
4275
  var modelRepositories = new WeakMap;
3721
4276
  var namedModels = new Map;
@@ -3756,12 +4311,12 @@ async function eagerLoadOnModels(models, paths) {
3756
4311
  }
3757
4312
  grouped.set(head, existing);
3758
4313
  }
3759
- for (const [head, nested] of grouped) {
4314
+ await Promise.all([...grouped.entries()].map(async ([head, nested]) => {
3760
4315
  const unloaded = models.filter((model) => model.loaded(head) === undefined);
3761
4316
  if (unloaded.length > 0) {
3762
4317
  const first = unloaded[0];
3763
4318
  if (!first) {
3764
- continue;
4319
+ return;
3765
4320
  }
3766
4321
  const method = first[head];
3767
4322
  if (typeof method !== "function") {
@@ -3777,14 +4332,14 @@ async function eagerLoadOnModels(models, paths) {
3777
4332
  }
3778
4333
  }
3779
4334
  if (nested.length === 0) {
3780
- continue;
4335
+ return;
3781
4336
  }
3782
4337
  const children = models.flatMap((model) => {
3783
4338
  const loaded = model.loaded(head);
3784
4339
  return Array.isArray(loaded) ? loaded : loaded ? [loaded] : [];
3785
4340
  });
3786
4341
  await eagerLoadOnModels(children.filter(isLoadableModel), nested);
3787
- }
4342
+ }));
3788
4343
  }
3789
4344
  async function loadNested(model, path) {
3790
4345
  await eagerLoadOnModels([model], [path]);
@@ -4044,18 +4599,30 @@ class ModelQuery {
4044
4599
  this.query.withMorphTo(...args);
4045
4600
  return this;
4046
4601
  }
4602
+ withHasManyThrough(...args) {
4603
+ this.query.withHasManyThrough(...args);
4604
+ return this;
4605
+ }
4606
+ withTrashed() {
4607
+ this.query.withTrashed();
4608
+ return this;
4609
+ }
4610
+ onlyTrashed() {
4611
+ this.query.onlyTrashed();
4612
+ return this;
4613
+ }
4047
4614
  async get() {
4048
4615
  const statics = modelStatics(this.modelClass);
4049
4616
  const rows = await this.query.get();
4050
4617
  const models = [];
4051
4618
  for (const row of rows) {
4052
4619
  const model = statics.newFromRecord(row, true);
4053
- await runObservers(model, "retrieved");
4054
4620
  for (const { name, relationQuery } of this.eager) {
4055
4621
  model.setLoaded(name, relationQuery.hydrateEager(row, name));
4056
4622
  }
4057
4623
  models.push(model);
4058
4624
  }
4625
+ await Promise.all(models.map((model) => runObservers(model, "retrieved")));
4059
4626
  const nested = this.eager.filter((item) => item.path.includes(".")).map((item) => item.path);
4060
4627
  await eagerLoadOnModels(models.filter(isLoadableModel), nested);
4061
4628
  return models;
@@ -4251,6 +4818,31 @@ class Model {
4251
4818
  static with(...relations) {
4252
4819
  return Model.query.call(this).with(...relations);
4253
4820
  }
4821
+ static withTrashed() {
4822
+ return Model.query.call(this).withTrashed();
4823
+ }
4824
+ static onlyTrashed() {
4825
+ return Model.query.call(this).onlyTrashed();
4826
+ }
4827
+ static async chunk(count, callback) {
4828
+ const statics = modelStatics(this);
4829
+ ensureBooted(this);
4830
+ await resolveModelRepository(this).chunk(count, async (rows) => {
4831
+ return await callback(rows.map((row) => statics.newFromRecord(row, true)));
4832
+ });
4833
+ }
4834
+ static async cursorPaginate(options) {
4835
+ const statics = modelStatics(this);
4836
+ ensureBooted(this);
4837
+ const page = await resolveModelRepository(this).cursorPaginate({
4838
+ perPage: options.perPage,
4839
+ cursor: options.cursor
4840
+ });
4841
+ return {
4842
+ data: page.data.map((row) => statics.newFromRecord(row, true)),
4843
+ meta: page.meta
4844
+ };
4845
+ }
4254
4846
  static whereHas(name, constrain) {
4255
4847
  return Model.query.call(this).whereHas(name, constrain);
4256
4848
  }
@@ -4435,6 +5027,20 @@ class Model {
4435
5027
  ownerKey: ownerKey ?? relatedTable.primaryKey
4436
5028
  }));
4437
5029
  }
5030
+ hasManyThrough(related, through, firstKey, secondKey, localKey, secondLocalKey) {
5031
+ const table = this.repository.getTable();
5032
+ const relatedClass = resolveRelated(related);
5033
+ const throughClass = resolveRelated(through);
5034
+ const throughTable = throughClass.repository().getTable();
5035
+ return new HasManyThroughRelationQuery(this, relatedClass, hasManyThrough({
5036
+ name: relatedClass.repository().getTable().name,
5037
+ throughTable: throughTable.name,
5038
+ localKey: localKey ?? table.primaryKey,
5039
+ firstKey: firstKey ?? foreignKeyFromTable(table.name),
5040
+ secondLocalKey: secondLocalKey ?? throughTable.primaryKey,
5041
+ secondKey: secondKey ?? foreignKeyFromTable(throughTable.name)
5042
+ }));
5043
+ }
4438
5044
  belongsToMany(related, pivotTable, foreignPivotKey, relatedPivotKey) {
4439
5045
  const table = this.repository.getTable();
4440
5046
  const relatedClass = resolveRelated(related);
@@ -4473,7 +5079,7 @@ class Model {
4473
5079
  morphTo(relatedByType, morphName, typeKey, idKey) {
4474
5080
  const resolvedName = morphName ?? inferRelationMethodName("morphTo");
4475
5081
  if (!resolvedName) {
4476
- throw new Error(`${this.constructor.name}.morphTo() needs an explicit morph name (Laravel infers it from the relation method).`);
5082
+ throw new Error(`${this.constructor.name}.morphTo() needs an explicit morph name (the calling method name is not available at runtime).`);
4477
5083
  }
4478
5084
  const resolvedMap = Object.fromEntries(Object.entries(relatedByType).map(([type, related]) => [type, resolveRelated(related)]));
4479
5085
  return new MorphToRelationQuery(this, resolvedMap, morphTo({
@@ -4522,6 +5128,74 @@ function registerModelRepository(model, repository) {
4522
5128
  ensureBooted(model);
4523
5129
  return model;
4524
5130
  }
5131
+ // ../../src/core/database/mysqlConnection.ts
5132
+ import mysql from "mysql2/promise";
5133
+ function rowsFromResult(result) {
5134
+ if (Array.isArray(result)) {
5135
+ return result;
5136
+ }
5137
+ if (result && typeof result === "object") {
5138
+ return [result];
5139
+ }
5140
+ return [];
5141
+ }
5142
+ function createMysqlConnectionFromPool(pool) {
5143
+ return {
5144
+ async unsafe(query, params = []) {
5145
+ const [result] = await pool.execute(query, [...params]);
5146
+ return rowsFromResult(result);
5147
+ },
5148
+ async close() {
5149
+ if (typeof pool.end === "function") {
5150
+ await pool.end();
5151
+ }
5152
+ }
5153
+ };
5154
+ }
5155
+ function createMysqlConnection(url) {
5156
+ if (!url.trim()) {
5157
+ throw new Error("MYSQL_URL is not configured. Set url before creating a MySQL pool.");
5158
+ }
5159
+ return createMysqlConnectionFromPool(mysql.createPool(url));
5160
+ }
5161
+ // ../../src/core/database/namedConnections.ts
5162
+ var REGISTRY_KEY = Symbol.for("@getstrata/namedConnections");
5163
+ function registry() {
5164
+ const globalRecord = globalThis;
5165
+ const existing = globalRecord[REGISTRY_KEY];
5166
+ if (existing) {
5167
+ return existing;
5168
+ }
5169
+ const created = new Map;
5170
+ globalRecord[REGISTRY_KEY] = created;
5171
+ return created;
5172
+ }
5173
+ function registerNamedConnection(name, driver, connection) {
5174
+ if (!name.trim()) {
5175
+ throw new Error("Named database connection requires a non-empty name.");
5176
+ }
5177
+ registry().set(name, { name, driver, connection });
5178
+ }
5179
+ function unregisterNamedConnection(name) {
5180
+ return registry().delete(name);
5181
+ }
5182
+ function hasNamedConnection(name) {
5183
+ return registry().has(name);
5184
+ }
5185
+ function getNamedConnection(name) {
5186
+ const entry = registry().get(name);
5187
+ if (!entry) {
5188
+ throw new Error(`Named database connection "${name}" is not registered.`);
5189
+ }
5190
+ return entry;
5191
+ }
5192
+ function resetNamedConnections() {
5193
+ registry().clear();
5194
+ }
5195
+ function runOnNamedConnection(name, callback) {
5196
+ const entry = getNamedConnection(name);
5197
+ return runWithSqlDialect(entry.driver, () => runWithDatabaseConnection(entry.connection, callback));
5198
+ }
4525
5199
  // ../../src/core/database/schema/columnDefinition.ts
4526
5200
  class ColumnDefinition {
4527
5201
  name;
@@ -4743,45 +5417,6 @@ class Blueprint {
4743
5417
  });
4744
5418
  }
4745
5419
  }
4746
- // ../../src/core/database/schema/driver.ts
4747
- function normalizeConnectionName(connection) {
4748
- const normalized = connection.trim().toLowerCase();
4749
- if (normalized === "pgsql" || normalized === "postgres" || normalized === "postgresql") {
4750
- return "pgsql";
4751
- }
4752
- if (normalized === "mysql" || normalized === "mariadb") {
4753
- return "mysql";
4754
- }
4755
- if (normalized === "sqlite") {
4756
- return "sqlite";
4757
- }
4758
- throw new Error(`Unsupported DB_CONNECTION: ${connection}`);
4759
- }
4760
- function resolveDriverFromUrl(url) {
4761
- const normalized = url.trim().toLowerCase();
4762
- if (normalized.startsWith("postgres://") || normalized.startsWith("postgresql://") || normalized.startsWith("postgres:")) {
4763
- return "pgsql";
4764
- }
4765
- if (normalized.startsWith("mysql://") || normalized.startsWith("mysql:")) {
4766
- return "mysql";
4767
- }
4768
- if (normalized.startsWith("sqlite:")) {
4769
- return "sqlite";
4770
- }
4771
- return null;
4772
- }
4773
- function resolveDatabaseDriver(options = {}) {
4774
- const connection = options.connection ?? process.env.DB_CONNECTION;
4775
- if (connection) {
4776
- return normalizeConnectionName(connection);
4777
- }
4778
- const url = options.url ?? process.env.DATABASE_URL ?? "";
4779
- const fromUrl = resolveDriverFromUrl(url);
4780
- if (fromUrl) {
4781
- return fromUrl;
4782
- }
4783
- return "pgsql";
4784
- }
4785
5420
  // ../../src/core/database/schema/errors.ts
4786
5421
  class UnsupportedSchemaFeatureError extends Error {
4787
5422
  constructor(feature, driver) {
@@ -5170,6 +5805,36 @@ async function runSeedersFromDirectory(directory, db, options) {
5170
5805
  }
5171
5806
  return seeders.length;
5172
5807
  }
5808
+ // ../../src/core/database/sqliteConnection.ts
5809
+ import { Database } from "bun:sqlite";
5810
+ function isRowReturning(sql) {
5811
+ const upper = sql.replace(/\s+/g, " ").trim().toUpperCase();
5812
+ if (upper.includes(" RETURNING ")) {
5813
+ return true;
5814
+ }
5815
+ return upper.startsWith("SELECT") || upper.startsWith("WITH") || upper.startsWith("PRAGMA") || upper.startsWith("EXPLAIN");
5816
+ }
5817
+ function createSqliteConnection(filename) {
5818
+ if (!filename.trim()) {
5819
+ throw new Error("SQLite path is not configured. Pass a filename or :memory:.");
5820
+ }
5821
+ const db = new Database(filename, { create: true });
5822
+ db.exec("PRAGMA foreign_keys = ON");
5823
+ return {
5824
+ async unsafe(query, params = []) {
5825
+ const statement = db.query(query);
5826
+ const args = [...params];
5827
+ if (isRowReturning(query)) {
5828
+ return statement.all(...args);
5829
+ }
5830
+ statement.run(...args);
5831
+ return [];
5832
+ },
5833
+ close() {
5834
+ db.close();
5835
+ }
5836
+ };
5837
+ }
5173
5838
  // ../../src/core/database/table.ts
5174
5839
  function defineTable(definition) {
5175
5840
  return definition;
@@ -5348,7 +6013,7 @@ class LogMailDriver {
5348
6013
  to: message.to,
5349
6014
  subject: message.subject,
5350
6015
  body: message.body,
5351
- ...message.html ? { html: message.html } : {}
6016
+ ...message.html ? { htmlBytes: Buffer.byteLength(message.html, "utf8") } : {}
5352
6017
  }));
5353
6018
  }
5354
6019
  }
@@ -5703,6 +6368,32 @@ function conditionalJsonResponse(request, data, init = {}) {
5703
6368
  headers
5704
6369
  });
5705
6370
  }
6371
+ // ../../src/core/runtime/frontendMode.ts
6372
+ var FRONTEND_MODES = ["api", "server-htmx", "spa-react", "hybrid"];
6373
+ var FRONTEND_MODE_PATTERN = new RegExp(`^(${FRONTEND_MODES.join("|")})$`);
6374
+ function parseFrontendMode(value) {
6375
+ const mode = (value ?? "api").trim();
6376
+ if (mode === "server-htmx" || mode === "spa-react" || mode === "hybrid") {
6377
+ return mode;
6378
+ }
6379
+ return "api";
6380
+ }
6381
+ function readFrontendMode() {
6382
+ return parseFrontendMode(process.env.FRONTEND_MODE);
6383
+ }
6384
+ function isViewsMode(mode) {
6385
+ return mode === "server-htmx" || mode === "hybrid";
6386
+ }
6387
+ function isSpaMode(mode) {
6388
+ return mode === "spa-react" || mode === "hybrid";
6389
+ }
6390
+ function isViewsEnabled() {
6391
+ return isViewsMode(readFrontendMode());
6392
+ }
6393
+ function isSpaEnabled() {
6394
+ return isSpaMode(readFrontendMode());
6395
+ }
6396
+
5706
6397
  // ../../src/core/http/contentSecurityPolicy.ts
5707
6398
  var HTMX_2_0_4_INDICATOR_STYLE_HASH = "'sha256-bsV5JivYxvGywDAZ22EZJKBFip65Ng9xoJVLbBg7bdo='";
5708
6399
  var API_DIRECTIVES = {
@@ -5790,11 +6481,10 @@ function applyNonce(directives, nonce) {
5790
6481
  return next;
5791
6482
  }
5792
6483
  function htmlBaselineDirectives() {
5793
- const frontendMode = (process.env.FRONTEND_MODE ?? "api").trim();
5794
- if (frontendMode === "server-htmx") {
6484
+ if (isViewsEnabled()) {
5795
6485
  return cloneDirectives(HTMX_HTML_DIRECTIVES);
5796
6486
  }
5797
- if (frontendMode === "spa-react") {
6487
+ if (isSpaEnabled()) {
5798
6488
  return cloneDirectives(SPA_HTML_DIRECTIVES);
5799
6489
  }
5800
6490
  return cloneDirectives(API_DIRECTIVES);
@@ -5909,7 +6599,7 @@ function buildCorsHeaders(request) {
5909
6599
  return headers;
5910
6600
  }
5911
6601
  // ../../src/core/http/csrfToken.ts
5912
- import { timingSafeEqual as timingSafeEqual2 } from "crypto";
6602
+ import { timingSafeEqual as timingSafeEqual3 } from "crypto";
5913
6603
 
5914
6604
  // ../../src/core/http/requestMetaContext.ts
5915
6605
  var requestMetaContext = createAsyncContextStore("@getstrata/requestMetaContext");
@@ -5941,7 +6631,7 @@ function tokensMatch(left, right) {
5941
6631
  if (leftBuffer.length !== rightBuffer.length) {
5942
6632
  return false;
5943
6633
  }
5944
- return timingSafeEqual2(leftBuffer, rightBuffer);
6634
+ return timingSafeEqual3(leftBuffer, rightBuffer);
5945
6635
  }
5946
6636
  function createCsrfTokenCookie() {
5947
6637
  const token = Bun.CSRF.generate(resolveCsrfSecret(), { expiresIn: CSRF_TTL_MS });
@@ -6018,6 +6708,9 @@ function appendSetCookie(response, cookie) {
6018
6708
  }
6019
6709
  function createCsrfMiddleware() {
6020
6710
  return async (request, next) => {
6711
+ if (requestUsesHeaderCredentials(request)) {
6712
+ return await next();
6713
+ }
6021
6714
  const method = request.method.toUpperCase();
6022
6715
  if (!MUTATING_METHODS.has(method)) {
6023
6716
  const csrf = resolveCsrfToken(request);
@@ -6055,7 +6748,7 @@ function createCsrfProtection(secret, options = {}) {
6055
6748
  };
6056
6749
  }
6057
6750
  // ../../src/core/http/flashSession.ts
6058
- import { createHmac, timingSafeEqual as timingSafeEqual3 } from "crypto";
6751
+ import { createHmac as createHmac2, timingSafeEqual as timingSafeEqual4 } from "crypto";
6059
6752
  var FLASH_COOKIE = appCookieName("flash");
6060
6753
  var FLASH_TTL_MS = 60 * 1000;
6061
6754
  function flashCookieName() {
@@ -6065,7 +6758,7 @@ function resolveFlashSecret() {
6065
6758
  return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || appDevSecret("flash-secret");
6066
6759
  }
6067
6760
  function signFlashPayload(payload, issuedAt) {
6068
- const signature = createHmac("sha256", resolveFlashSecret()).update(`${payload}.${issuedAt}`).digest("hex");
6761
+ const signature = createHmac2("sha256", resolveFlashSecret()).update(`${payload}.${issuedAt}`).digest("hex");
6069
6762
  return `${payload}.${issuedAt}.${signature}`;
6070
6763
  }
6071
6764
  function readFlashCookie(request) {
@@ -6105,7 +6798,7 @@ function parseFlashCookie(cookieValue) {
6105
6798
  if (expectedBuffer.length !== actualBuffer.length) {
6106
6799
  return null;
6107
6800
  }
6108
- if (!timingSafeEqual3(expectedBuffer, actualBuffer)) {
6801
+ if (!timingSafeEqual4(expectedBuffer, actualBuffer)) {
6109
6802
  return null;
6110
6803
  }
6111
6804
  try {
@@ -6415,6 +7108,9 @@ function whenLoaded(model, relation, transform) {
6415
7108
  if (value === undefined) {
6416
7109
  return;
6417
7110
  }
7111
+ if (value === null) {
7112
+ return null;
7113
+ }
6418
7114
  return transform ? transform(value) : value;
6419
7115
  }
6420
7116
 
@@ -6440,7 +7136,7 @@ class JsonResource {
6440
7136
  }
6441
7137
  whenLoaded(relation, transform) {
6442
7138
  const model = this.resource;
6443
- if (typeof model.loaded !== "function") {
7139
+ if (this.resource == null || typeof model.loaded !== "function") {
6444
7140
  return;
6445
7141
  }
6446
7142
  return whenLoaded(model, relation, transform);
@@ -6483,21 +7179,6 @@ function toPaginatedResourceCollection(items, meta, transformer) {
6483
7179
  meta
6484
7180
  };
6485
7181
  }
6486
- // ../../src/core/runtime/frontendMode.ts
6487
- function readFrontendMode() {
6488
- const mode = (process.env.FRONTEND_MODE ?? "api").trim();
6489
- if (mode === "server-htmx") {
6490
- return "server-htmx";
6491
- }
6492
- if (mode === "spa-react") {
6493
- return "spa-react";
6494
- }
6495
- return "api";
6496
- }
6497
- function isViewsEnabled() {
6498
- return readFrontendMode() === "server-htmx";
6499
- }
6500
-
6501
7182
  // ../../src/core/view/htmlResponse.ts
6502
7183
  function withCharset(contentType) {
6503
7184
  return contentType.includes("charset=") ? contentType : `${contentType}; charset=utf-8`;
@@ -6625,6 +7306,10 @@ function requestPrefersJson(request) {
6625
7306
  if (request.headers.get("HX-Request") === "true") {
6626
7307
  return false;
6627
7308
  }
7309
+ const pathname = new URL(request.url).pathname;
7310
+ if (pathname.startsWith("/api/")) {
7311
+ return true;
7312
+ }
6628
7313
  const accept = request.headers.get("accept")?.toLowerCase() ?? "";
6629
7314
  if (accept.includes("text/html")) {
6630
7315
  return false;
@@ -6636,8 +7321,7 @@ function requestPrefersJson(request) {
6636
7321
  if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
6637
7322
  return false;
6638
7323
  }
6639
- const pathname = new URL(request.url).pathname;
6640
- return pathname.startsWith("/api/");
7324
+ return false;
6641
7325
  }
6642
7326
 
6643
7327
  // ../../src/core/http/safeInternalPath.ts
@@ -6844,6 +7528,29 @@ function securedBindRouteModelByKey(param, resolver, authorization, handler) {
6844
7528
  }
6845
7529
  // ../../src/core/http/loginThrottleMiddleware.ts
6846
7530
  var {RedisClient: RedisClient2 } = globalThis.Bun;
7531
+
7532
+ // ../../src/core/http/throttleResponse.ts
7533
+ async function tooManyRequestsResponse(request, message, decaySeconds) {
7534
+ const retryAfter = { "retry-after": String(decaySeconds) };
7535
+ if (requestPrefersJson(request) || !isViewsEnabled()) {
7536
+ return Response.json({ error: message }, {
7537
+ status: 429,
7538
+ headers: retryAfter
7539
+ });
7540
+ }
7541
+ const html = await htmlErrorResponse({
7542
+ status: 429,
7543
+ title: "Too Many Requests",
7544
+ message,
7545
+ request
7546
+ });
7547
+ const headers = new Headers(html.headers);
7548
+ headers.set("retry-after", String(decaySeconds));
7549
+ return new Response(html.body, { status: 429, headers });
7550
+ }
7551
+
7552
+ // ../../src/core/http/loginThrottleMiddleware.ts
7553
+ var memoryLoginBuckets = new Map;
6847
7554
  function resolveLoginIdentity(request) {
6848
7555
  return readClientIp(request) ?? "unknown";
6849
7556
  }
@@ -6861,7 +7568,30 @@ async function resolveLoginEmail(request) {
6861
7568
  return "unknown";
6862
7569
  }
6863
7570
  }
6864
- function createLoginThrottleMiddleware(options) {
7571
+ function consumeMemoryAttempt(key, decaySeconds) {
7572
+ const now = Date.now();
7573
+ const existing = memoryLoginBuckets.get(key);
7574
+ if (!existing || existing.resetAt <= now) {
7575
+ memoryLoginBuckets.set(key, { count: 1, resetAt: now + decaySeconds * 1000 });
7576
+ return 1;
7577
+ }
7578
+ existing.count += 1;
7579
+ return existing.count;
7580
+ }
7581
+ function createMemoryLoginThrottleMiddleware(options) {
7582
+ const prefix = options.keyPrefix ?? namespacedRedisKey("login-throttle:");
7583
+ return async (request, next) => {
7584
+ const identity = resolveLoginIdentity(request);
7585
+ const email = await resolveLoginEmail(request);
7586
+ const throttleKey = `${prefix}${identity}:${email}`;
7587
+ const attempts = consumeMemoryAttempt(throttleKey, options.decaySeconds);
7588
+ if (attempts > options.maxAttempts) {
7589
+ return await tooManyRequestsResponse(request, "Too many login attempts. Try again later.", options.decaySeconds);
7590
+ }
7591
+ return await next();
7592
+ };
7593
+ }
7594
+ function createRedisLoginThrottleMiddleware(options) {
6865
7595
  const client = new RedisClient2(options.redisUrl);
6866
7596
  const prefix = options.keyPrefix ?? namespacedRedisKey("login-throttle:");
6867
7597
  return async (request, next) => {
@@ -6873,16 +7603,18 @@ function createLoginThrottleMiddleware(options) {
6873
7603
  await client.expire(throttleKey, options.decaySeconds);
6874
7604
  }
6875
7605
  if (attempts > options.maxAttempts) {
6876
- return Response.json({ error: "Too many login attempts. Try again later." }, {
6877
- status: 429,
6878
- headers: {
6879
- "retry-after": String(options.decaySeconds)
6880
- }
6881
- });
7606
+ return await tooManyRequestsResponse(request, "Too many login attempts. Try again later.", options.decaySeconds);
6882
7607
  }
6883
7608
  return await next();
6884
7609
  };
6885
7610
  }
7611
+ function createLoginThrottleMiddleware(options) {
7612
+ const redisUrl = options.redisUrl?.trim() ?? "";
7613
+ if (redisUrl) {
7614
+ return createRedisLoginThrottleMiddleware({ ...options, redisUrl });
7615
+ }
7616
+ return createMemoryLoginThrottleMiddleware(options);
7617
+ }
6886
7618
  // ../../src/core/http/memoryThrottleMiddleware.ts
6887
7619
  var throttleBucketRegistries = new Set;
6888
7620
  function createMemoryThrottleMiddleware(options) {
@@ -6901,12 +7633,7 @@ function createMemoryThrottleMiddleware(options) {
6901
7633
  }
6902
7634
  existing.count += 1;
6903
7635
  if (existing.count > options.maxAttempts) {
6904
- return Response.json({ error: "Too many requests." }, {
6905
- status: 429,
6906
- headers: {
6907
- "retry-after": String(options.decaySeconds)
6908
- }
6909
- });
7636
+ return await tooManyRequestsResponse(request, "Too many requests.", options.decaySeconds);
6910
7637
  }
6911
7638
  return await next();
6912
7639
  };
@@ -7052,6 +7779,60 @@ function createRequireGlobalAdminMiddleware() {
7052
7779
  return await next();
7053
7780
  };
7054
7781
  }
7782
+ // ../../src/core/auth/intendedUrlCookie.ts
7783
+ var INTENDED_URL_COOKIE = appCookieName("intended");
7784
+ var DEFAULT_INTENDED_URL_TTL_SECONDS = 60 * 60 * 24;
7785
+ var SKIP_EXACT_PATHS = new Set([
7786
+ "/email/verify",
7787
+ "/verify-email",
7788
+ "/login",
7789
+ "/register",
7790
+ "/logout",
7791
+ "/forgot-password",
7792
+ "/reset-password",
7793
+ "/two-factor-challenge",
7794
+ "/confirm-password",
7795
+ "/api"
7796
+ ]);
7797
+ function intendedUrlCookieName() {
7798
+ return process.env.INTENDED_URL_COOKIE_NAME?.trim() || appCookieName("intended");
7799
+ }
7800
+ function intendedUrlTtlSeconds() {
7801
+ const parsed = Number.parseInt(process.env.INTENDED_URL_TTL_SECONDS ?? "", 10);
7802
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : DEFAULT_INTENDED_URL_TTL_SECONDS;
7803
+ }
7804
+ function cookieSecureFlag() {
7805
+ return process.env.APP_ENV === "production" ? "; Secure" : "";
7806
+ }
7807
+ function pathnameOf(path) {
7808
+ const pathname = path.split("?")[0] ?? path;
7809
+ const trimmed = pathname.replace(/\/+$/, "");
7810
+ return trimmed || "/";
7811
+ }
7812
+ function isStashableIntendedPath(path) {
7813
+ if (!path.startsWith("/") || path.startsWith("//")) {
7814
+ return false;
7815
+ }
7816
+ const pathname = pathnameOf(path);
7817
+ if (SKIP_EXACT_PATHS.has(pathname) || pathname.startsWith("/oauth") || pathname.startsWith("/api/")) {
7818
+ return false;
7819
+ }
7820
+ return true;
7821
+ }
7822
+ function createIntendedUrlCookie(path) {
7823
+ const sanitized = sanitizeInternalPath(path, "");
7824
+ if (!sanitized || !isStashableIntendedPath(sanitized)) {
7825
+ return null;
7826
+ }
7827
+ return `${intendedUrlCookieName()}=${encodeURIComponent(sanitized)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${intendedUrlTtlSeconds()}${cookieSecureFlag()}`;
7828
+ }
7829
+ function createIntendedUrlCookieFromRequest(request) {
7830
+ if (request.method !== "GET" && request.method !== "HEAD") {
7831
+ return null;
7832
+ }
7833
+ return createIntendedUrlCookie(safeInternalRedirectPath(request, ""));
7834
+ }
7835
+
7055
7836
  // ../../src/core/http/requireWebAuthMiddleware.ts
7056
7837
  function createRequireWebAuthMiddleware(auth2) {
7057
7838
  return async (request, next) => {
@@ -7062,7 +7843,12 @@ function createRequireWebAuthMiddleware(auth2) {
7062
7843
  if (requestPrefersJson(request)) {
7063
7844
  throw new UnauthorizedError;
7064
7845
  }
7065
- return Response.redirect(loginRedirectLocation(request), 302);
7846
+ const intended = createIntendedUrlCookieFromRequest(request);
7847
+ const headers = new Headers({ Location: loginRedirectLocation(request) });
7848
+ if (intended) {
7849
+ headers.append("Set-Cookie", intended);
7850
+ }
7851
+ return new Response(null, { status: 302, headers });
7066
7852
  };
7067
7853
  }
7068
7854
  // ../../src/core/http/scimThrottleMiddleware.ts
@@ -7135,7 +7921,7 @@ function createSecurityHeadersMiddleware(options = {}) {
7135
7921
  };
7136
7922
  }
7137
7923
  // ../../src/core/http/signedUrl.ts
7138
- import { createHmac as createHmac2 } from "crypto";
7924
+ import { createHmac as createHmac3 } from "crypto";
7139
7925
  function resolveSignedUrlSecret() {
7140
7926
  return process.env.SIGNED_URL_SECRET?.trim() || process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || appDevSecret("signed-url-secret");
7141
7927
  }
@@ -7157,7 +7943,7 @@ function sortedQueryString(params) {
7157
7943
  return new URLSearchParams(entries).toString();
7158
7944
  }
7159
7945
  function signCanonicalPayload(path, query) {
7160
- return createHmac2("sha256", resolveSignedUrlSecret()).update(`${path}
7946
+ return createHmac3("sha256", resolveSignedUrlSecret()).update(`${path}
7161
7947
  ${query}`).digest("hex");
7162
7948
  }
7163
7949
  function buildSignedSearchParams(path, query = {}, expiresAt) {
@@ -7252,12 +8038,7 @@ function createThrottleMiddleware(options) {
7252
8038
  }
7253
8039
  const maxAttempts = options.maxAttempts * rateLimitMultiplierForPlan(currentTenant()?.plan ?? "free");
7254
8040
  if (attempts > maxAttempts) {
7255
- return Response.json({ error: "Too many requests." }, {
7256
- status: 429,
7257
- headers: {
7258
- "retry-after": String(options.decaySeconds)
7259
- }
7260
- });
8041
+ return await tooManyRequestsResponse(request, "Too many requests.", options.decaySeconds);
7261
8042
  }
7262
8043
  return await next();
7263
8044
  };
@@ -7675,9 +8456,9 @@ function readSharedJobRegistry() {
7675
8456
  if (globalRegistry) {
7676
8457
  return globalRegistry;
7677
8458
  }
7678
- const registry = new JobRegistry;
7679
- globalThis[JOB_REGISTRY_KEY] = registry;
7680
- return registry;
8459
+ const registry2 = new JobRegistry;
8460
+ globalThis[JOB_REGISTRY_KEY] = registry2;
8461
+ return registry2;
7681
8462
  }
7682
8463
  var jobRegistry = readSharedJobRegistry();
7683
8464
 
@@ -8076,17 +8857,33 @@ async function resolveTenantForRequest(request) {
8076
8857
  if (Number.isInteger(parsedHeader) && parsedHeader > 0 && parsedHeader !== userTenantId) {
8077
8858
  throw new ForbiddenError("Tenant header does not match your account.");
8078
8859
  }
8079
- return await resolveTenant(userTenantId) ?? DEFAULT_TENANT;
8860
+ const memberTenant = await resolveTenant(userTenantId);
8861
+ if (memberTenant) {
8862
+ return memberTenant;
8863
+ }
8864
+ return DEFAULT_TENANT;
8080
8865
  }
8081
8866
  if (Number.isInteger(parsedHeader) && parsedHeader > 0) {
8082
- return await resolveTenant(parsedHeader) ?? DEFAULT_TENANT;
8867
+ const headerTenant = await resolveTenant(parsedHeader);
8868
+ if (headerTenant) {
8869
+ return headerTenant;
8870
+ }
8871
+ return DEFAULT_TENANT;
8083
8872
  }
8084
- return await resolveTenant(userTenantId) ?? DEFAULT_TENANT;
8873
+ const adminTenant = await resolveTenant(userTenantId);
8874
+ if (adminTenant) {
8875
+ return adminTenant;
8876
+ }
8877
+ return DEFAULT_TENANT;
8085
8878
  }
8086
8879
  }
8087
8880
  const headerTenantId = Number.isInteger(parsedHeader) && parsedHeader > 0 ? parsedHeader : null;
8088
8881
  const tenantId = isPublicReadsEnabled() && headerTenantId !== null ? headerTenantId : DEFAULT_TENANT.id;
8089
- return await resolveTenant(tenantId) ?? DEFAULT_TENANT;
8882
+ const guestTenant = await resolveTenant(tenantId);
8883
+ if (guestTenant) {
8884
+ return guestTenant;
8885
+ }
8886
+ return DEFAULT_TENANT;
8090
8887
  }
8091
8888
  function createTenantMiddleware() {
8092
8889
  return async (request, next) => {
@@ -8556,6 +9353,7 @@ export {
8556
9353
  AuthManager,
8557
9354
  BadRequestError,
8558
9355
  baseRepository_default as BaseRepository,
9356
+ BasicAuthGuard,
8559
9357
  BelongsToManyRelationQuery,
8560
9358
  BelongsToRelationQuery,
8561
9359
  Blueprint,
@@ -8582,6 +9380,7 @@ export {
8582
9380
  HttpError,
8583
9381
  Job,
8584
9382
  JsonResource,
9383
+ JwtGuard,
8585
9384
  LocalStorageDriver,
8586
9385
  LogMailDriver,
8587
9386
  Mailer,
@@ -8667,6 +9466,8 @@ export {
8667
9466
  createMembershipMiddleware,
8668
9467
  createMemoryThrottleMiddleware,
8669
9468
  createMetricsMiddleware,
9469
+ createMysqlConnection,
9470
+ createMysqlConnectionFromPool,
8670
9471
  createNotificationDispatcher,
8671
9472
  createProductionQueue,
8672
9473
  createQueue,
@@ -8680,9 +9481,11 @@ export {
8680
9481
  createScimAuthMiddleware,
8681
9482
  createScimThrottleMiddleware,
8682
9483
  createSecurityHeadersMiddleware,
9484
+ createSqliteConnection,
8683
9485
  createStorageDriver,
8684
9486
  createTenantMiddleware,
8685
9487
  createThrottleMiddleware,
9488
+ createTokenAbilityChecker,
8686
9489
  createTracingMiddleware,
8687
9490
  createTrackedJob,
8688
9491
  createValidateSignatureMiddleware,
@@ -8691,11 +9494,13 @@ export {
8691
9494
  currentOrgRole,
8692
9495
  currentOrganizationIds,
8693
9496
  currentRequestMeta,
9497
+ currentSqlDialect,
8694
9498
  currentTenant,
8695
9499
  currentTenantId,
8696
9500
  currentTraceId,
8697
9501
  defineTable,
8698
9502
  dehydrateValue,
9503
+ dialectFor,
8699
9504
  emailRule,
8700
9505
  emptyPaginateResult,
8701
9506
  errorTemplateName,
@@ -8712,10 +9517,13 @@ export {
8712
9517
  getDefaultDatabasePool,
8713
9518
  getDefaultDatabaseQuery,
8714
9519
  getMigrationStatus,
9520
+ getNamedConnection,
8715
9521
  grammarForDriver,
8716
9522
  guestCanViewResource,
9523
+ hasActiveDatabaseConnection,
8717
9524
  hasMany,
8718
9525
  hasMinimumOrgRole2 as hasMinimumOrgRole,
9526
+ hasNamedConnection,
8719
9527
  hasOne,
8720
9528
  hasOrgMembership,
8721
9529
  hasValidSignature,
@@ -8740,6 +9548,7 @@ export {
8740
9548
  isTenancyEnabled,
8741
9549
  jobRegistry,
8742
9550
  jsonResponse2 as jsonResponse,
9551
+ jwtTtlSeconds,
8743
9552
  loadMigrationsFromDirectory,
8744
9553
  loadSeedersFromDirectory,
8745
9554
  log,
@@ -8777,6 +9586,7 @@ export {
8777
9586
  registerDefaultDatabasePool,
8778
9587
  registerModelClass,
8779
9588
  registerModelRepository,
9589
+ registerNamedConnection,
8780
9590
  registerShutdownHandler,
8781
9591
  renderKernelErrorChrome,
8782
9592
  renderMarkdownMail,
@@ -8786,6 +9596,8 @@ export {
8786
9596
  resetBoundDatabaseConnection,
8787
9597
  resetDefaultStorage,
8788
9598
  resetMemoryThrottleForTests,
9599
+ resetNamedConnections,
9600
+ resetSqlDialect,
8789
9601
  resolveApplicationAuth,
8790
9602
  resolveApplicationCache,
8791
9603
  resolveApplicationConfig,
@@ -8812,12 +9624,14 @@ export {
8812
9624
  runDueScheduledTasks,
8813
9625
  runGracefulShutdown,
8814
9626
  runInTransaction,
9627
+ runOnNamedConnection,
8815
9628
  runQueueJob,
8816
9629
  runSeedersFromDirectory,
8817
9630
  runWithAuthUser,
8818
9631
  runWithDatabaseConnection,
8819
9632
  runWithMembershipContext,
8820
9633
  runWithRequestMeta,
9634
+ runWithSqlDialect,
8821
9635
  runWithTenant,
8822
9636
  runWithTenantDatabase,
8823
9637
  runWithTraceContext,
@@ -8831,6 +9645,7 @@ export {
8831
9645
  serializeDate,
8832
9646
  serverHtmxContentSecurityPolicy,
8833
9647
  setActiveApplicationContext,
9648
+ signJwt,
8834
9649
  signedUrl,
8835
9650
  singularize,
8836
9651
  spaContentSecurityPolicy,
@@ -8844,8 +9659,11 @@ export {
8844
9659
  toPaginatedResourceCollection,
8845
9660
  toResourceCollection,
8846
9661
  trustForwardedFor,
9662
+ unregisterNamedConnection,
9663
+ useSqlDialect,
8847
9664
  validateObject,
8848
9665
  verifyCsrfToken,
9666
+ verifyJwt,
8849
9667
  whenLoaded,
8850
9668
  withErrorHandling,
8851
9669
  withMiddleware,