@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.
- package/CHANGELOG.md +67 -32
- package/README.md +16 -35
- package/dist/core/auth/abilityCatalog.d.ts +2 -2
- package/dist/core/auth/basicAuthGuard.d.ts +9 -0
- package/dist/core/auth/guard.d.ts +6 -0
- package/dist/core/auth/jwt.d.ts +19 -0
- package/dist/core/auth/jwtGuard.d.ts +14 -0
- package/dist/core/auth/tokenAbilityChecker.d.ts +5 -0
- package/dist/core/cache/tags.d.ts +6 -0
- package/dist/core/contracts/authUserDirectory.d.ts +4 -0
- package/dist/core/database/baseRepository.d.ts +5 -1
- package/dist/core/database/dialect.d.ts +18 -0
- package/dist/core/database/factory.d.ts +1 -0
- package/dist/core/database/index.d.ts +11 -3
- package/dist/core/database/model.d.ts +21 -2
- package/dist/core/database/mysqlConnection.d.ts +12 -0
- package/dist/core/database/namedConnections.d.ts +15 -0
- package/dist/core/database/relationQuery.d.ts +22 -3
- package/dist/core/database/relationships.d.ts +22 -2
- package/dist/core/database/repositoryQuery.d.ts +5 -1
- package/dist/core/database/sqliteConnection.d.ts +7 -0
- package/dist/core/http/loginThrottleMiddleware.d.ts +5 -2
- package/dist/core/http/resources.d.ts +2 -2
- package/dist/core/http/response.d.ts +2 -1
- package/dist/core/http/statelessAuth.d.ts +8 -0
- package/dist/core/http/throttleResponse.d.ts +2 -0
- package/dist/core/runtime/frontendMode.d.ts +10 -2
- package/dist/entries/auth/basicAuthGuard.js +137 -0
- package/dist/entries/auth/jwt.js +135 -0
- package/dist/entries/auth/jwtGuard.js +203 -0
- package/dist/entries/auth/sessionGuard.js +3 -21
- package/dist/entries/auth/tokenAbilityChecker.js +24 -0
- package/dist/entries/cache/tags.js +7 -1
- package/dist/entries/database/connectionContext.js +1 -0
- package/dist/entries/database/dialect.js +1 -0
- package/dist/entries/database/factory.js +5 -4
- package/dist/entries/database/model.js +189 -33
- package/dist/entries/database/mysqlConnection.js +35 -0
- package/dist/entries/database/namedConnections.js +1 -0
- package/dist/entries/database/query.js +28 -15
- package/dist/entries/database/relationships.js +43 -6
- package/dist/entries/database/repositoryQuery.js +142 -73
- package/dist/entries/database/schema.js +28 -15
- package/dist/entries/database/sqliteConnection.js +34 -0
- package/dist/entries/facades.js +1 -1
- package/dist/entries/http/contentNegotiation.js +5 -2
- package/dist/entries/http/csrfMiddleware.js +45 -0
- package/dist/entries/http/loginThrottleMiddleware.js +246 -7
- package/dist/entries/http/memoryThrottleMiddleware.js +208 -6
- package/dist/entries/http/requireAbilityMiddleware.js +35 -11
- package/dist/entries/http/requirePasswordConfirmMiddleware.js +5 -2
- package/dist/entries/http/requireVerifiedMiddleware.js +5 -2
- package/dist/entries/http/requireWebAuthMiddleware.js +12 -3
- package/dist/entries/http/resources.js +4 -1
- package/dist/entries/http/response.js +46 -12
- package/dist/entries/http/statelessAuth.js +48 -0
- package/dist/entries/http/throttleMiddleware.js +208 -6
- package/dist/entries/http/webErrorResponse.js +35 -11
- package/dist/entries/http/webFormRequest.js +5 -2
- package/dist/entries/mail/mailer.js +1 -1
- package/dist/entries/openapi/generator.js +48 -5
- package/dist/entries/runtime/frontendMode.js +39 -10
- package/dist/framework/public-api.d.ts +11 -1
- package/dist/index.js +1068 -250
- 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
|
-
"
|
|
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
|
|
296
|
-
if (!
|
|
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
|
|
314
|
-
if (!
|
|
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
|
|
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
|
|
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/
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
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
|
|
1016
|
-
return
|
|
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/
|
|
1648
|
-
function
|
|
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
|
|
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
|
|
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}
|
|
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)}
|
|
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})
|
|
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}
|
|
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)} = $
|
|
2041
|
-
params
|
|
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)} = $
|
|
2052
|
-
params
|
|
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)} = $
|
|
2058
|
-
params
|
|
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
|
-
|
|
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
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
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
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
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
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
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
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
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
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
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
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
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
|
-
|
|
3350
|
+
await Promise.all([...idsByType.entries()].map(async ([morphType, ids]) => {
|
|
2854
3351
|
const repository = repositoriesByType.get(morphType);
|
|
2855
3352
|
if (!repository) {
|
|
2856
|
-
|
|
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
|
|
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
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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 ? {
|
|
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
|
-
|
|
5794
|
-
if (frontendMode === "server-htmx") {
|
|
6484
|
+
if (isViewsEnabled()) {
|
|
5795
6485
|
return cloneDirectives(HTMX_HTML_DIRECTIVES);
|
|
5796
6486
|
}
|
|
5797
|
-
if (
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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 (!
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
7679
|
-
globalThis[JOB_REGISTRY_KEY] =
|
|
7680
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
8867
|
+
const headerTenant = await resolveTenant(parsedHeader);
|
|
8868
|
+
if (headerTenant) {
|
|
8869
|
+
return headerTenant;
|
|
8870
|
+
}
|
|
8871
|
+
return DEFAULT_TENANT;
|
|
8083
8872
|
}
|
|
8084
|
-
|
|
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
|
-
|
|
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,
|