@getstrata/core 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/dist/core/cache/createCacheStore.d.ts +3 -1
- package/dist/core/cache/redisCacheStore.d.ts +21 -8
- package/dist/core/cache/repository.d.ts +3 -2
- package/dist/core/cache/store.d.ts +4 -1
- package/dist/core/cache/taggedCache.d.ts +1 -1
- package/dist/core/contracts/di.d.ts +4 -2
- package/dist/core/contracts/serviceTokens.d.ts +6 -1
- package/dist/core/database/baseRepository.d.ts +0 -1
- package/dist/core/database/index.d.ts +1 -1
- package/dist/core/database/model.d.ts +51 -22
- package/dist/core/database/repositoryQuery.d.ts +16 -1
- package/dist/core/database/types.d.ts +25 -2
- package/dist/core/database/whereBuilder.d.ts +5 -5
- package/dist/core/events/deferredModelEvents.d.ts +2 -1
- package/dist/core/http/loginThrottleMiddleware.d.ts +6 -5
- package/dist/core/http/memoryThrottleMiddleware.d.ts +11 -3
- package/dist/core/http/scimThrottleMiddleware.d.ts +12 -3
- package/dist/core/http/throttleMiddleware.d.ts +24 -6
- package/dist/core/lifecycle/gracefulShutdown.d.ts +33 -5
- package/dist/core/queue/index.d.ts +7 -0
- package/dist/core/queue/redisQueue.d.ts +1 -0
- package/dist/core/queue/resilientQueue.d.ts +2 -2
- package/dist/core/runtime/applicationRegistry.d.ts +2 -1
- package/dist/core/runtime/boundedThrottleStore.d.ts +29 -0
- package/dist/core/scheduler/leases.d.ts +40 -0
- package/dist/core/scheduler/schedule.d.ts +29 -5
- package/dist/core/tenant/tenantContext.d.ts +5 -4
- package/dist/core/tenant/tenantMiddleware.d.ts +7 -1
- package/dist/core/tracing/otel.d.ts +28 -26
- package/dist/core/tracing/tracingMiddleware.d.ts +4 -1
- package/dist/entries/audit/exportAuditLogs.js +14 -2
- package/dist/entries/auth/basicAuthGuard.js +80 -0
- package/dist/entries/auth/jwtGuard.js +80 -0
- package/dist/entries/auth/scimAuthMiddleware.js +17 -7
- package/dist/entries/auth/sessionGuard.js +80 -0
- package/dist/entries/cache/createCacheStore.js +365 -119
- package/dist/entries/cache/repository.js +5 -0
- package/dist/entries/contracts/serviceTokens.js +83 -0
- package/dist/entries/database/factory.js +0 -3
- package/dist/entries/database/model.js +1 -2486
- package/dist/entries/database/repositoryQuery.js +67 -9
- package/dist/entries/http/bodySizeLimitMiddleware.js +76 -10
- package/dist/entries/http/memoryThrottleMiddleware.js +276 -21
- package/dist/entries/http/scimThrottleMiddleware.js +576 -54
- package/dist/entries/http/throttleMiddleware.js +65 -16
- package/dist/entries/jobs/exportAuditLogsJob.js +44 -7
- package/dist/entries/jobs/invalidateCacheTagsJob.js +30 -5
- package/dist/entries/queue/createAppQueue.js +67 -6
- package/dist/entries/queue/publicQueue.js +67 -6
- package/dist/entries/queue/queueMetrics.js +67 -6
- package/dist/entries/queue/redisQueue.js +3 -0
- package/dist/entries/queue.js +30 -5
- package/dist/entries/scheduler/schedule.js +1 -72
- package/dist/entries/tenant/databaseTenantContext.js +14 -2
- package/dist/entries/tenant/tenantDatabaseScope.js +14 -2
- package/dist/entries/tracing/tracingMiddleware.js +186 -454
- package/dist/framework/public-api.d.ts +13 -9
- package/dist/index.js +1745 -633
- package/dist/types/services.d.ts +3 -3
- package/package.json +17 -8
package/dist/index.js
CHANGED
|
@@ -364,16 +364,6 @@ function currentTenantId() {
|
|
|
364
364
|
}
|
|
365
365
|
return tenant.id;
|
|
366
366
|
}
|
|
367
|
-
function rateLimitMultiplierForPlan(plan) {
|
|
368
|
-
switch (plan) {
|
|
369
|
-
case "enterprise":
|
|
370
|
-
return 4;
|
|
371
|
-
case "pro":
|
|
372
|
-
return 2;
|
|
373
|
-
default:
|
|
374
|
-
return 1;
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
367
|
var tenantContext;
|
|
378
368
|
var init_tenantContext = __esm(() => {
|
|
379
369
|
init_asyncContextStore();
|
|
@@ -656,9 +646,12 @@ async function runWithDeferredModelEvents(callback) {
|
|
|
656
646
|
const existing = deferredModelEvents.getStore();
|
|
657
647
|
if (existing) {
|
|
658
648
|
existing.frames.push([]);
|
|
649
|
+
existing.rollbackFrames.push([]);
|
|
659
650
|
try {
|
|
660
651
|
const result = await callback();
|
|
661
652
|
const nested = existing.frames.pop() ?? [];
|
|
653
|
+
const rollback = existing.rollbackFrames.pop() ?? [];
|
|
654
|
+
existing.rollbackFrames.at(-1)?.push(...rollback);
|
|
662
655
|
const parent = existing.frames[existing.frames.length - 1];
|
|
663
656
|
if (parent) {
|
|
664
657
|
parent.push(...nested);
|
|
@@ -666,14 +659,26 @@ async function runWithDeferredModelEvents(callback) {
|
|
|
666
659
|
return result;
|
|
667
660
|
} catch (error) {
|
|
668
661
|
existing.frames.pop();
|
|
662
|
+
for (const rollback of (existing.rollbackFrames.pop() ?? []).reverse())
|
|
663
|
+
rollback();
|
|
669
664
|
throw error;
|
|
670
665
|
}
|
|
671
666
|
}
|
|
672
|
-
const state = { frames: [[]] };
|
|
673
|
-
|
|
667
|
+
const state = { frames: [[]], rollbackFrames: [[]] };
|
|
668
|
+
let result;
|
|
669
|
+
try {
|
|
670
|
+
result = await deferredModelEvents.run(state, callback);
|
|
671
|
+
} catch (error) {
|
|
672
|
+
for (const rollback of (state.rollbackFrames[0] ?? []).reverse())
|
|
673
|
+
rollback();
|
|
674
|
+
throw error;
|
|
675
|
+
}
|
|
674
676
|
await dispatchQueuedModelEvents(state.frames[0] ?? []);
|
|
675
677
|
return result;
|
|
676
678
|
}
|
|
679
|
+
function onDeferredModelRollback(callback) {
|
|
680
|
+
deferredModelEvents.getStore()?.rollbackFrames.at(-1)?.push(callback);
|
|
681
|
+
}
|
|
677
682
|
var deferredModelEvents;
|
|
678
683
|
var init_deferredModelEvents = __esm(() => {
|
|
679
684
|
init_asyncContextStore();
|
|
@@ -1052,7 +1057,87 @@ function resolveUserId(user) {
|
|
|
1052
1057
|
}
|
|
1053
1058
|
return userId;
|
|
1054
1059
|
}
|
|
1060
|
+
// ../../src/core/contracts/container.ts
|
|
1061
|
+
function createServiceToken(key) {
|
|
1062
|
+
if (!key.trim())
|
|
1063
|
+
throw new Error("Service token key must not be empty.");
|
|
1064
|
+
return key;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
class ServiceContainer {
|
|
1068
|
+
services = new Map;
|
|
1069
|
+
singletonFactories = new Map;
|
|
1070
|
+
bindings = new Map;
|
|
1071
|
+
set(key, value) {
|
|
1072
|
+
this.singletonFactories.delete(key);
|
|
1073
|
+
this.bindings.delete(key);
|
|
1074
|
+
this.services.set(key, value);
|
|
1075
|
+
return value;
|
|
1076
|
+
}
|
|
1077
|
+
singleton(key, factory) {
|
|
1078
|
+
this.bindings.delete(key);
|
|
1079
|
+
this.services.delete(key);
|
|
1080
|
+
this.singletonFactories.set(key, factory);
|
|
1081
|
+
}
|
|
1082
|
+
bind(key, factory) {
|
|
1083
|
+
this.singletonFactories.delete(key);
|
|
1084
|
+
this.services.delete(key);
|
|
1085
|
+
this.bindings.set(key, factory);
|
|
1086
|
+
}
|
|
1087
|
+
get(key) {
|
|
1088
|
+
if (this.services.has(key)) {
|
|
1089
|
+
return this.services.get(key);
|
|
1090
|
+
}
|
|
1091
|
+
const singletonFactory = this.singletonFactories.get(key);
|
|
1092
|
+
if (singletonFactory) {
|
|
1093
|
+
const value = singletonFactory(this);
|
|
1094
|
+
this.services.set(key, value);
|
|
1095
|
+
return value;
|
|
1096
|
+
}
|
|
1097
|
+
const binding = this.bindings.get(key);
|
|
1098
|
+
if (binding) {
|
|
1099
|
+
return binding(this);
|
|
1100
|
+
}
|
|
1101
|
+
throw new Error(`Service "${key}" is not registered.`);
|
|
1102
|
+
}
|
|
1103
|
+
resolve(key) {
|
|
1104
|
+
return this.get(key);
|
|
1105
|
+
}
|
|
1106
|
+
make(key) {
|
|
1107
|
+
return this.resolve(key);
|
|
1108
|
+
}
|
|
1109
|
+
instance(key, value) {
|
|
1110
|
+
return this.set(key, value);
|
|
1111
|
+
}
|
|
1112
|
+
has(key) {
|
|
1113
|
+
return this.services.has(key) || this.singletonFactories.has(key) || this.bindings.has(key);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
class ConfigStore {
|
|
1118
|
+
values = new Map;
|
|
1119
|
+
set(key, value) {
|
|
1120
|
+
this.values.set(key, value);
|
|
1121
|
+
return value;
|
|
1122
|
+
}
|
|
1123
|
+
get(key) {
|
|
1124
|
+
return this.values.get(key);
|
|
1125
|
+
}
|
|
1126
|
+
require(key) {
|
|
1127
|
+
if (!this.values.has(key)) {
|
|
1128
|
+
throw new Error(`Config key "${key}" is not defined.`);
|
|
1129
|
+
}
|
|
1130
|
+
return this.values.get(key);
|
|
1131
|
+
}
|
|
1132
|
+
has(key) {
|
|
1133
|
+
return this.values.has(key);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1055
1137
|
// ../../src/core/contracts/serviceTokens.ts
|
|
1138
|
+
var CORE_THROTTLE_QUOTA_POLICY_TOKEN = createServiceToken("core.throttleQuotaPolicy");
|
|
1139
|
+
var CORE_TENANT_RESOLVER_TOKEN = createServiceToken("core.tenantResolver");
|
|
1140
|
+
var CORE_HTTP_CLEANUP_TOKEN = createServiceToken("core.httpCleanup");
|
|
1056
1141
|
var CORE_QUEUE_TOKEN = "core.queue";
|
|
1057
1142
|
var CORE_EVENT_BUS_TOKEN = "core.eventBus";
|
|
1058
1143
|
var CORE_POLICY_GATE_TOKEN = "core.policyGate";
|
|
@@ -2054,6 +2139,12 @@ function setActiveApplicationContext(context) {
|
|
|
2054
2139
|
activeContext = context;
|
|
2055
2140
|
globalThis[APPLICATION_CONTEXT_KEY] = context;
|
|
2056
2141
|
}
|
|
2142
|
+
function clearActiveApplicationContext(context) {
|
|
2143
|
+
if (readStoredApplicationContext() !== context)
|
|
2144
|
+
return;
|
|
2145
|
+
activeContext = undefined;
|
|
2146
|
+
delete globalThis[APPLICATION_CONTEXT_KEY];
|
|
2147
|
+
}
|
|
2057
2148
|
function requireActiveApplicationContext() {
|
|
2058
2149
|
const context = readStoredApplicationContext();
|
|
2059
2150
|
if (!context) {
|
|
@@ -3247,19 +3338,17 @@ async function resolveTenant(tenantId) {
|
|
|
3247
3338
|
if (!isTenancyEnabled()) {
|
|
3248
3339
|
return {
|
|
3249
3340
|
id: tenantId,
|
|
3250
|
-
slug: "default"
|
|
3251
|
-
plan: "free",
|
|
3252
|
-
region: "eu"
|
|
3341
|
+
slug: "default"
|
|
3253
3342
|
};
|
|
3254
3343
|
}
|
|
3255
3344
|
const rows = await repositoryConnection`
|
|
3256
|
-
SELECT id, slug
|
|
3345
|
+
SELECT id, slug
|
|
3257
3346
|
FROM tenant
|
|
3258
3347
|
WHERE id = ${tenantId}
|
|
3259
3348
|
LIMIT 1
|
|
3260
3349
|
`;
|
|
3261
3350
|
const row = rows[0];
|
|
3262
|
-
return row ? { id: row.id, slug: row.slug
|
|
3351
|
+
return row ? { id: row.id, slug: row.slug } : null;
|
|
3263
3352
|
}
|
|
3264
3353
|
|
|
3265
3354
|
// ../../src/core/auth/scimAuthMiddleware.ts
|
|
@@ -3319,151 +3408,397 @@ function createTokenAbilityChecker() {
|
|
|
3319
3408
|
}
|
|
3320
3409
|
// ../../src/core/cache/redisCacheStore.ts
|
|
3321
3410
|
var {RedisClient } = globalThis.Bun;
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
return
|
|
3327
|
-
|
|
3411
|
+
var CACHE_SCRIPT = `
|
|
3412
|
+
local expected = {'hash', 'zset', 'zset', 'hash', 'zset'}
|
|
3413
|
+
for i, key in ipairs(KEYS) do
|
|
3414
|
+
local kind = redis.call('TYPE', key).ok
|
|
3415
|
+
if kind ~= 'none' and kind ~= expected[i] then return redis.error_reply('Unexpected cache key type') end
|
|
3416
|
+
end
|
|
3417
|
+
-- Redis eviction can remove one container without its companions. Never read
|
|
3418
|
+
-- orphaned values or leases after losing the expiry/order/generation indexes.
|
|
3419
|
+
local entries = redis.call('HLEN', KEYS[1])
|
|
3420
|
+
local order = redis.call('ZCARD', KEYS[3])
|
|
3421
|
+
if redis.call('ZCARD', KEYS[2]) ~= order or entries ~= (order > 0 and order + 1 or 0) then
|
|
3422
|
+
redis.call('DEL', KEYS[1], KEYS[2], KEYS[3])
|
|
3423
|
+
end
|
|
3424
|
+
if redis.call('HLEN', KEYS[4]) ~= redis.call('ZCARD', KEYS[5]) then
|
|
3425
|
+
redis.call('DEL', KEYS[4], KEYS[5])
|
|
3426
|
+
end
|
|
3427
|
+
local r = cjson.decode(ARGV[1])
|
|
3428
|
+
local time = redis.call('TIME')
|
|
3429
|
+
local now = tonumber(time[1]) * 1000 + tonumber(time[2]) / 1000
|
|
3430
|
+
local function removeEntry(key)
|
|
3431
|
+
local removed = redis.call('HDEL', KEYS[1], 'e:' .. key)
|
|
3432
|
+
redis.call('ZREM', KEYS[2], key)
|
|
3433
|
+
redis.call('ZREM', KEYS[3], key)
|
|
3434
|
+
return removed
|
|
3435
|
+
end
|
|
3436
|
+
local function removeMeta(field)
|
|
3437
|
+
redis.call('HDEL', KEYS[4], field)
|
|
3438
|
+
redis.call('ZREM', KEYS[5], field)
|
|
3439
|
+
end
|
|
3440
|
+
for _, key in ipairs(redis.call('ZRANGEBYSCORE', KEYS[2], '-inf', now, 'LIMIT', 0, r.capacity)) do removeEntry(key) end
|
|
3441
|
+
for _, field in ipairs(redis.call('ZRANGEBYSCORE', KEYS[5], '-inf', now, 'LIMIT', 0, r.metadataLimit)) do removeMeta(field) end
|
|
3442
|
+
local function expireContainers()
|
|
3443
|
+
local last = redis.call('ZREVRANGE', KEYS[2], 0, 0, 'WITHSCORES')
|
|
3444
|
+
if #last == 0 then
|
|
3445
|
+
redis.call('DEL', KEYS[1], KEYS[2], KEYS[3])
|
|
3446
|
+
elseif last[2] == 'inf' then
|
|
3447
|
+
for i = 1, 3 do redis.call('PERSIST', KEYS[i]) end
|
|
3448
|
+
else
|
|
3449
|
+
local ttl = math.max(1, math.ceil(tonumber(last[2]) - now))
|
|
3450
|
+
for i = 1, 3 do redis.call('PEXPIRE', KEYS[i], ttl) end
|
|
3451
|
+
end
|
|
3452
|
+
for i = 4, 5 do redis.call('PEXPIRE', KEYS[i], r.metadataTtlMs) end
|
|
3453
|
+
end
|
|
3454
|
+
local function response(value)
|
|
3455
|
+
expireContainers()
|
|
3456
|
+
return cjson.encode(value)
|
|
3457
|
+
end
|
|
3458
|
+
local function addTags(existing, added)
|
|
3459
|
+
local seen = {}
|
|
3460
|
+
local result = {}
|
|
3461
|
+
for _, tags in ipairs({existing, added}) do
|
|
3462
|
+
for _, tag in ipairs(tags) do
|
|
3463
|
+
if not seen[tag] then seen[tag] = true; table.insert(result, tag) end
|
|
3464
|
+
end
|
|
3465
|
+
end
|
|
3466
|
+
if #result > 32 then error('Cache entries support at most 32 tags') end
|
|
3467
|
+
return result
|
|
3468
|
+
end
|
|
3469
|
+
local function touch(key)
|
|
3470
|
+
local sequence = redis.call('HINCRBY', KEYS[1], '_sequence', 1)
|
|
3471
|
+
redis.call('ZADD', KEYS[3], sequence, key)
|
|
3472
|
+
end
|
|
3473
|
+
local function read(key, tags)
|
|
3474
|
+
local raw = redis.call('HGET', KEYS[1], 'e:' .. key)
|
|
3475
|
+
if not raw then return nil end
|
|
3476
|
+
local entry = cjson.decode(raw)
|
|
3477
|
+
if #tags > 0 then
|
|
3478
|
+
entry.tags = addTags(entry.tags, tags)
|
|
3479
|
+
redis.call('HSET', KEYS[1], 'e:' .. key, cjson.encode(entry))
|
|
3480
|
+
end
|
|
3481
|
+
touch(key)
|
|
3482
|
+
return entry.payload
|
|
3483
|
+
end
|
|
3484
|
+
local function write(key, payload, tags)
|
|
3485
|
+
local entry = {payload=payload, tags=tags}
|
|
3486
|
+
redis.call('HSET', KEYS[1], 'e:' .. key, cjson.encode(entry))
|
|
3487
|
+
redis.call('ZADD', KEYS[2], r.ttlMs == 0 and '+inf' or now + r.ttlMs, key)
|
|
3488
|
+
touch(key)
|
|
3489
|
+
local overflow = redis.call('ZCARD', KEYS[3]) - r.capacity
|
|
3490
|
+
if overflow > 0 then
|
|
3491
|
+
for _, victim in ipairs(redis.call('ZRANGE', KEYS[3], 0, overflow - 1)) do removeEntry(victim) end
|
|
3492
|
+
end
|
|
3493
|
+
end
|
|
3494
|
+
local leaseField = 'l:' .. r.key
|
|
3495
|
+
local function lease()
|
|
3496
|
+
local raw = redis.call('HGET', KEYS[4], leaseField)
|
|
3497
|
+
if not raw then return nil end
|
|
3498
|
+
local value = cjson.decode(raw)
|
|
3499
|
+
if value.expires <= now or value.deadline <= now then removeMeta(leaseField); return nil end
|
|
3500
|
+
return value
|
|
3501
|
+
end
|
|
3502
|
+
local function metadataSpace(count)
|
|
3503
|
+
if redis.call('HLEN', KEYS[4]) + count > r.metadataLimit then
|
|
3504
|
+
-- Pressure sheds generation/lease state. Every old owner is fenced; cached
|
|
3505
|
+
-- entries remain valid because invalidation removes matching entries atomically.
|
|
3506
|
+
redis.call('DEL', KEYS[4], KEYS[5])
|
|
3507
|
+
end
|
|
3508
|
+
end
|
|
3509
|
+
local function putMeta(field, value, expires)
|
|
3510
|
+
redis.call('HSET', KEYS[4], field, value)
|
|
3511
|
+
redis.call('ZADD', KEYS[5], expires, field)
|
|
3512
|
+
end
|
|
3513
|
+
local function scopes()
|
|
3514
|
+
local fields = {}
|
|
3515
|
+
for _, tag in ipairs(r.tags) do table.insert(fields, 'g:tag:' .. tag) end
|
|
3516
|
+
for _, prefix in ipairs(r.prefixes) do table.insert(fields, 'g:prefix:' .. prefix) end
|
|
3517
|
+
return fields
|
|
3518
|
+
end
|
|
3519
|
+
local function matches(key, tags)
|
|
3520
|
+
if r.op == 'invalidate' then return key == r.key end
|
|
3521
|
+
if r.op == 'prefix' then return key == r.key or string.sub(key, 1, #r.key + 1) == r.key .. '?' end
|
|
3522
|
+
if r.op == 'tags' then
|
|
3523
|
+
for _, existing in ipairs(tags) do
|
|
3524
|
+
for _, wanted in ipairs(r.tags) do if wanted == existing then return true end end
|
|
3525
|
+
end
|
|
3526
|
+
end
|
|
3527
|
+
return false
|
|
3528
|
+
end
|
|
3529
|
+
if r.op == 'clear' then
|
|
3530
|
+
redis.call('DEL', unpack(KEYS))
|
|
3531
|
+
return cjson.encode({removed=0})
|
|
3532
|
+
elseif r.op == 'get' then
|
|
3533
|
+
local payload = read(r.key, r.tags)
|
|
3534
|
+
return response(payload and {kind='value', payload=payload} or {kind='missing'})
|
|
3535
|
+
elseif r.op == 'set' then
|
|
3536
|
+
local old = redis.call('HGET', KEYS[1], 'e:' .. r.key)
|
|
3537
|
+
local tags = old and cjson.decode(old).tags or {}
|
|
3538
|
+
write(r.key, r.payload, tags)
|
|
3539
|
+
removeMeta(leaseField)
|
|
3540
|
+
return response({written=true})
|
|
3541
|
+
elseif r.op == 'attach' then
|
|
3542
|
+
read(r.key, r.tags)
|
|
3543
|
+
return response({written=true})
|
|
3544
|
+
elseif r.op == 'size' then
|
|
3545
|
+
return response({size=redis.call('ZCARD', KEYS[3])})
|
|
3546
|
+
elseif r.op == 'tags' or r.op == 'prefix' or r.op == 'invalidate' then
|
|
3547
|
+
local generationFields = {}
|
|
3548
|
+
if r.op == 'tags' then
|
|
3549
|
+
for _, tag in ipairs(r.tags) do table.insert(generationFields, 'g:tag:' .. tag) end
|
|
3550
|
+
elseif r.op == 'prefix' then table.insert(generationFields, 'g:prefix:' .. r.key) end
|
|
3551
|
+
metadataSpace(#generationFields)
|
|
3552
|
+
for _, field in ipairs(generationFields) do putMeta(field, r.owner, now + r.metadataTtlMs) end
|
|
3553
|
+
local removed = 0
|
|
3554
|
+
for _, key in ipairs(redis.call('ZRANGE', KEYS[3], 0, -1)) do
|
|
3555
|
+
local raw = redis.call('HGET', KEYS[1], 'e:' .. key)
|
|
3556
|
+
if raw and matches(key, cjson.decode(raw).tags) then removed = removed + removeEntry(key) end
|
|
3557
|
+
end
|
|
3558
|
+
for _, field in ipairs(redis.call('HKEYS', KEYS[4])) do
|
|
3559
|
+
if string.sub(field, 1, 2) == 'l:' then
|
|
3560
|
+
local value = cjson.decode(redis.call('HGET', KEYS[4], field))
|
|
3561
|
+
if matches(string.sub(field, 3), value.tags) then removeMeta(field) end
|
|
3562
|
+
end
|
|
3563
|
+
end
|
|
3564
|
+
return response({removed=removed})
|
|
3565
|
+
elseif r.op == 'claim' then
|
|
3566
|
+
local payload = read(r.key, r.tags)
|
|
3567
|
+
if payload then return response({kind='value', payload=payload}) end
|
|
3568
|
+
local current = lease()
|
|
3569
|
+
if current then
|
|
3570
|
+
local shared = addTags(current.tags, r.tags)
|
|
3571
|
+
if #shared == #current.tags then return response({kind='wait'}) end
|
|
3572
|
+
-- A newly requested tag must not inherit a fill started before that tag's
|
|
3573
|
+
-- invalidation. Restart with the union of tags and a new owner.
|
|
3574
|
+
r.tags = shared
|
|
3575
|
+
removeMeta(leaseField)
|
|
3576
|
+
end
|
|
3577
|
+
local active = 0
|
|
3578
|
+
for _, field in ipairs(redis.call('HKEYS', KEYS[4])) do if string.sub(field, 1, 2) == 'l:' then active = active + 1 end end
|
|
3579
|
+
if active >= r.capacity then return response({kind='wait'}) end
|
|
3580
|
+
local fields = scopes()
|
|
3581
|
+
metadataSpace(#fields + 1)
|
|
3582
|
+
local generations = {}
|
|
3583
|
+
for _, field in ipairs(fields) do
|
|
3584
|
+
local token = redis.call('HGET', KEYS[4], field) or r.owner
|
|
3585
|
+
putMeta(field, token, now + r.metadataTtlMs)
|
|
3586
|
+
generations[field] = token
|
|
3587
|
+
end
|
|
3588
|
+
local value = {owner=r.owner, tags=r.tags, generations=generations, deadline=now + r.fillTimeoutMs, expires=now + r.leaseMs}
|
|
3589
|
+
putMeta(leaseField, cjson.encode(value), value.expires)
|
|
3590
|
+
return response({kind='owner'})
|
|
3591
|
+
elseif r.op == 'renew' or r.op == 'publish' or r.op == 'release' then
|
|
3592
|
+
local current = lease()
|
|
3593
|
+
if not current or current.owner ~= r.owner then return response({owned=false}) end
|
|
3594
|
+
if r.op == 'release' then removeMeta(leaseField); return response({owned=true}) end
|
|
3595
|
+
for field, token in pairs(current.generations) do
|
|
3596
|
+
if redis.call('HGET', KEYS[4], field) ~= token then removeMeta(leaseField); return response({owned=false}) end
|
|
3597
|
+
end
|
|
3598
|
+
if r.op == 'renew' then
|
|
3599
|
+
current.expires = math.min(current.deadline, now + r.leaseMs)
|
|
3600
|
+
putMeta(leaseField, cjson.encode(current), current.expires)
|
|
3601
|
+
else
|
|
3602
|
+
write(r.key, r.payload, current.tags)
|
|
3603
|
+
removeMeta(leaseField)
|
|
3604
|
+
end
|
|
3605
|
+
return response({owned=true})
|
|
3606
|
+
end
|
|
3607
|
+
return redis.error_reply('Unknown cache operation')
|
|
3608
|
+
`;
|
|
3328
3609
|
|
|
3329
3610
|
class RedisCacheStore {
|
|
3330
3611
|
ttlMs;
|
|
3331
3612
|
maxEntries;
|
|
3332
3613
|
client;
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3614
|
+
keys;
|
|
3615
|
+
options;
|
|
3616
|
+
fills = new Set;
|
|
3617
|
+
closed = false;
|
|
3618
|
+
constructor(redisUrl, ttlMs, maxEntries, options = {}) {
|
|
3336
3619
|
this.ttlMs = ttlMs;
|
|
3337
3620
|
this.maxEntries = maxEntries;
|
|
3338
|
-
|
|
3621
|
+
if (!Number.isSafeInteger(ttlMs) || ttlMs < 0)
|
|
3622
|
+
throw new RangeError("ttlMs must be a non-negative safe integer.");
|
|
3623
|
+
if (!Number.isInteger(maxEntries) || maxEntries < 1 || maxEntries > 1e4)
|
|
3624
|
+
throw new RangeError("maxEntries must be between 1 and 10000.");
|
|
3625
|
+
this.options = {
|
|
3626
|
+
commandTimeoutMs: 5000,
|
|
3627
|
+
leaseMs: 1e4,
|
|
3628
|
+
fillTimeoutMs: 30000,
|
|
3629
|
+
waitTimeoutMs: 35000,
|
|
3630
|
+
pollMs: 25,
|
|
3631
|
+
metadataTtlMs: 60000,
|
|
3632
|
+
metadataLimit: 4096,
|
|
3633
|
+
...options
|
|
3634
|
+
};
|
|
3635
|
+
for (const [key, value] of Object.entries(this.options))
|
|
3636
|
+
if (!Number.isSafeInteger(value) || value < 1)
|
|
3637
|
+
throw new RangeError(`${key} must be a positive safe integer.`);
|
|
3638
|
+
if (this.options.leaseMs < 30 || this.options.fillTimeoutMs < this.options.leaseMs || this.options.metadataTtlMs < this.options.fillTimeoutMs + this.options.leaseMs || this.options.metadataLimit < 66 || this.options.metadataLimit > 1e5)
|
|
3639
|
+
throw new RangeError("Invalid Redis cache lease, deadline or metadata bounds.");
|
|
3640
|
+
const namespace = namespacedRedisKey("cache:v3:");
|
|
3641
|
+
const slot = new Bun.CryptoHasher("sha256").update(namespace).digest("hex");
|
|
3642
|
+
this.keys = ["entries", "expiry", "order", "metadata", "metadata-expiry"].map((name) => `${namespace}{${slot}}:${name}`);
|
|
3643
|
+
this.client = new RedisClient(redisUrl, {
|
|
3644
|
+
connectionTimeout: this.options.commandTimeoutMs,
|
|
3645
|
+
maxRetries: 1
|
|
3646
|
+
});
|
|
3647
|
+
}
|
|
3648
|
+
close() {
|
|
3649
|
+
this.closed = true;
|
|
3650
|
+
for (const fill of this.fills)
|
|
3651
|
+
fill.abort(new Error("Redis cache store is closed."));
|
|
3652
|
+
this.client.close();
|
|
3339
3653
|
}
|
|
3340
3654
|
async get(key) {
|
|
3341
|
-
const
|
|
3342
|
-
|
|
3343
|
-
return;
|
|
3344
|
-
}
|
|
3345
|
-
return JSON.parse(raw);
|
|
3655
|
+
const result = await this.execute("get", key);
|
|
3656
|
+
return result.kind === "value" ? JSON.parse(result.payload ?? "null") : undefined;
|
|
3346
3657
|
}
|
|
3347
3658
|
async set(key, value, ttlMs) {
|
|
3348
|
-
|
|
3349
|
-
const payload = JSON.stringify(value);
|
|
3350
|
-
if (resolvedTtlMs > 0) {
|
|
3351
|
-
await this.client.psetex(this.storageKey(key), resolvedTtlMs, payload);
|
|
3352
|
-
} else {
|
|
3353
|
-
await this.client.set(this.storageKey(key), payload);
|
|
3354
|
-
}
|
|
3355
|
-
await this.enforceMaxEntries();
|
|
3659
|
+
await this.execute("set", key, { payload: this.serialize(value), ttlMs: ttlMs ?? this.ttlMs });
|
|
3356
3660
|
}
|
|
3357
3661
|
async getOrSet(key, loader, ttlMs) {
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
const
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3662
|
+
return this.getOrSetTagged(key, loader, [], ttlMs);
|
|
3663
|
+
}
|
|
3664
|
+
async getOrSetTagged(key, loader, tags, ttlMs) {
|
|
3665
|
+
const owner = crypto.randomUUID();
|
|
3666
|
+
const end = performance.now() + this.options.waitTimeoutMs;
|
|
3667
|
+
while (!this.closed) {
|
|
3668
|
+
const claim = await this.execute("claim", key, { owner, tags, ttlMs: ttlMs ?? this.ttlMs });
|
|
3669
|
+
if (claim.kind === "value")
|
|
3670
|
+
return JSON.parse(claim.payload ?? "null");
|
|
3671
|
+
if (claim.kind === "owner")
|
|
3672
|
+
return this.fill(key, owner, loader, ttlMs);
|
|
3673
|
+
if (performance.now() >= end)
|
|
3674
|
+
throw new Error("Redis cache fill wait deadline exceeded.");
|
|
3675
|
+
await Bun.sleep(this.options.pollMs);
|
|
3676
|
+
}
|
|
3677
|
+
throw new Error("Redis cache store is closed.");
|
|
3374
3678
|
}
|
|
3375
3679
|
async attachTags(key, tags) {
|
|
3376
|
-
|
|
3377
|
-
return;
|
|
3378
|
-
}
|
|
3379
|
-
let tagsForKey = this.keyTags.get(key);
|
|
3380
|
-
if (!tagsForKey) {
|
|
3381
|
-
tagsForKey = new Set;
|
|
3382
|
-
this.keyTags.set(key, tagsForKey);
|
|
3383
|
-
}
|
|
3384
|
-
for (const tag of tags) {
|
|
3385
|
-
tagsForKey.add(tag);
|
|
3386
|
-
await this.client.sadd(this.tagKey(tag), key);
|
|
3387
|
-
}
|
|
3680
|
+
await this.execute("attach", key, { tags });
|
|
3388
3681
|
}
|
|
3389
3682
|
async flushTags(tags) {
|
|
3390
|
-
|
|
3391
|
-
for (const tag of tags) {
|
|
3392
|
-
const members = await this.client.smembers(this.tagKey(tag));
|
|
3393
|
-
for (const member of members) {
|
|
3394
|
-
keysToRemove.add(member);
|
|
3395
|
-
}
|
|
3396
|
-
}
|
|
3397
|
-
let removed = 0;
|
|
3398
|
-
for (const key of keysToRemove) {
|
|
3399
|
-
if (await this.invalidate(key)) {
|
|
3400
|
-
removed += 1;
|
|
3401
|
-
}
|
|
3402
|
-
}
|
|
3403
|
-
for (const tag of tags) {
|
|
3404
|
-
await this.client.del(this.tagKey(tag));
|
|
3405
|
-
}
|
|
3406
|
-
return removed;
|
|
3683
|
+
return (await this.execute("tags", "", { tags })).removed ?? 0;
|
|
3407
3684
|
}
|
|
3408
3685
|
async invalidate(key) {
|
|
3409
|
-
|
|
3410
|
-
await this.detachKeyFromTags(key);
|
|
3411
|
-
return deleted > 0;
|
|
3686
|
+
return ((await this.execute("invalidate", key)).removed ?? 0) > 0;
|
|
3412
3687
|
}
|
|
3413
3688
|
async invalidateByPrefix(prefix) {
|
|
3414
|
-
|
|
3415
|
-
let removed = 0;
|
|
3416
|
-
for (const storageKey of keys) {
|
|
3417
|
-
const key = storageKey.slice(cacheKeyPrefix().length);
|
|
3418
|
-
if (key === prefix || key.startsWith(`${prefix}?`)) {
|
|
3419
|
-
if (await this.invalidate(key)) {
|
|
3420
|
-
removed += 1;
|
|
3421
|
-
}
|
|
3422
|
-
}
|
|
3423
|
-
}
|
|
3424
|
-
return removed;
|
|
3689
|
+
return (await this.execute("prefix", prefix)).removed ?? 0;
|
|
3425
3690
|
}
|
|
3426
3691
|
async clear() {
|
|
3427
|
-
|
|
3428
|
-
if (keys.length > 0) {
|
|
3429
|
-
await this.client.del(...keys);
|
|
3430
|
-
}
|
|
3431
|
-
const tagKeys = await this.client.keys(`${cacheTagPrefix()}*`);
|
|
3432
|
-
if (tagKeys.length > 0) {
|
|
3433
|
-
await this.client.del(...tagKeys);
|
|
3434
|
-
}
|
|
3435
|
-
this.inflight.clear();
|
|
3436
|
-
this.keyTags.clear();
|
|
3692
|
+
await this.execute("clear", "");
|
|
3437
3693
|
}
|
|
3438
3694
|
async size() {
|
|
3439
|
-
|
|
3440
|
-
return keys.length;
|
|
3441
|
-
}
|
|
3442
|
-
storageKey(key) {
|
|
3443
|
-
return `${cacheKeyPrefix()}${key}`;
|
|
3444
|
-
}
|
|
3445
|
-
tagKey(tag) {
|
|
3446
|
-
return `${cacheTagPrefix()}${tag}`;
|
|
3695
|
+
return (await this.execute("size", "")).size ?? 0;
|
|
3447
3696
|
}
|
|
3448
|
-
async
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3697
|
+
async fill(key, owner, loader, ttlMs) {
|
|
3698
|
+
if (this.fills.size >= this.maxEntries) {
|
|
3699
|
+
await this.execute("release", key, { owner });
|
|
3700
|
+
throw new Error("Redis cache local fill capacity exceeded.");
|
|
3452
3701
|
}
|
|
3453
|
-
|
|
3454
|
-
|
|
3702
|
+
const controller = new AbortController;
|
|
3703
|
+
this.fills.add(controller);
|
|
3704
|
+
let done = false;
|
|
3705
|
+
let renewing = false;
|
|
3706
|
+
const renew = setInterval(() => {
|
|
3707
|
+
if (renewing || done)
|
|
3708
|
+
return;
|
|
3709
|
+
renewing = true;
|
|
3710
|
+
this.execute("renew", key, { owner }).then((result) => {
|
|
3711
|
+
if (!result.owned)
|
|
3712
|
+
clearInterval(renew);
|
|
3713
|
+
}).catch((error) => {
|
|
3714
|
+
if (!done)
|
|
3715
|
+
controller.abort(error);
|
|
3716
|
+
}).finally(() => {
|
|
3717
|
+
renewing = false;
|
|
3718
|
+
});
|
|
3719
|
+
}, Math.max(10, Math.floor(this.options.leaseMs / 3)));
|
|
3720
|
+
const timeout = setTimeout(() => controller.abort(new Error("Redis cache fill deadline exceeded.")), this.options.fillTimeoutMs);
|
|
3721
|
+
let rejectAbort;
|
|
3722
|
+
const aborted = new Promise((_, reject) => {
|
|
3723
|
+
rejectAbort = () => reject(controller.signal.reason);
|
|
3724
|
+
controller.signal.addEventListener("abort", rejectAbort, { once: true });
|
|
3725
|
+
});
|
|
3726
|
+
try {
|
|
3727
|
+
const value = await Promise.race([
|
|
3728
|
+
Promise.resolve().then(() => loader(controller.signal)),
|
|
3729
|
+
aborted
|
|
3730
|
+
]);
|
|
3731
|
+
done = true;
|
|
3732
|
+
clearInterval(renew);
|
|
3733
|
+
await this.execute("publish", key, {
|
|
3734
|
+
owner,
|
|
3735
|
+
payload: this.serialize(value),
|
|
3736
|
+
ttlMs: ttlMs ?? this.ttlMs
|
|
3737
|
+
});
|
|
3738
|
+
return value;
|
|
3739
|
+
} finally {
|
|
3740
|
+
done = true;
|
|
3741
|
+
clearInterval(renew);
|
|
3742
|
+
clearTimeout(timeout);
|
|
3743
|
+
controller.signal.removeEventListener("abort", rejectAbort);
|
|
3744
|
+
this.fills.delete(controller);
|
|
3745
|
+
if (!this.closed)
|
|
3746
|
+
await this.execute("release", key, { owner }).catch(() => {});
|
|
3455
3747
|
}
|
|
3456
|
-
this.keyTags.delete(key);
|
|
3457
3748
|
}
|
|
3458
|
-
|
|
3459
|
-
const
|
|
3460
|
-
if (
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
if (
|
|
3466
|
-
|
|
3749
|
+
serialize(value) {
|
|
3750
|
+
const payload = JSON.stringify(value);
|
|
3751
|
+
if (typeof payload !== "string")
|
|
3752
|
+
throw new TypeError("Redis cache values must be JSON serializable.");
|
|
3753
|
+
return payload;
|
|
3754
|
+
}
|
|
3755
|
+
async execute(op, key, extra = {}) {
|
|
3756
|
+
if (this.closed)
|
|
3757
|
+
throw new Error("Redis cache store is closed.");
|
|
3758
|
+
if (Buffer.byteLength(key) > 4096)
|
|
3759
|
+
throw new RangeError("Invalid Redis cache identity.");
|
|
3760
|
+
const tags = Array.from(new Set(extra.tags ?? []));
|
|
3761
|
+
if (tags.length > 32 || tags.some((tag) => typeof tag !== "string" || Buffer.byteLength(tag) > 1024))
|
|
3762
|
+
throw new RangeError("Redis cache supports at most 32 tags of 1024 bytes each.");
|
|
3763
|
+
const prefixes = [key];
|
|
3764
|
+
for (let i = 0;i < key.length; i++)
|
|
3765
|
+
if (key[i] === "?")
|
|
3766
|
+
prefixes.push(key.slice(0, i));
|
|
3767
|
+
if (prefixes.length > 33)
|
|
3768
|
+
throw new RangeError("Redis cache identities support at most 32 query separators.");
|
|
3769
|
+
const ttlMs = extra.ttlMs ?? this.ttlMs;
|
|
3770
|
+
if (typeof ttlMs !== "number" || !Number.isSafeInteger(ttlMs) || ttlMs < 0)
|
|
3771
|
+
throw new RangeError("ttlMs must be a non-negative safe integer.");
|
|
3772
|
+
let timer;
|
|
3773
|
+
const timeout = new Promise((_, reject) => {
|
|
3774
|
+
timer = setTimeout(() => {
|
|
3775
|
+
reject(new Error("Redis cache command deadline exceeded; recreate the store."));
|
|
3776
|
+
this.close();
|
|
3777
|
+
}, this.options.commandTimeoutMs);
|
|
3778
|
+
});
|
|
3779
|
+
try {
|
|
3780
|
+
const raw = await Promise.race([
|
|
3781
|
+
this.client.send("EVAL", [
|
|
3782
|
+
CACHE_SCRIPT,
|
|
3783
|
+
String(this.keys.length),
|
|
3784
|
+
...this.keys,
|
|
3785
|
+
JSON.stringify({
|
|
3786
|
+
op,
|
|
3787
|
+
key,
|
|
3788
|
+
capacity: this.maxEntries,
|
|
3789
|
+
owner: crypto.randomUUID(),
|
|
3790
|
+
...this.options,
|
|
3791
|
+
...extra,
|
|
3792
|
+
tags,
|
|
3793
|
+
prefixes,
|
|
3794
|
+
ttlMs
|
|
3795
|
+
})
|
|
3796
|
+
]),
|
|
3797
|
+
timeout
|
|
3798
|
+
]);
|
|
3799
|
+
return JSON.parse(String(raw));
|
|
3800
|
+
} finally {
|
|
3801
|
+
clearTimeout(timer);
|
|
3467
3802
|
}
|
|
3468
3803
|
}
|
|
3469
3804
|
}
|
|
@@ -3689,7 +4024,7 @@ function createCacheStore(options) {
|
|
|
3689
4024
|
if (!options.redisUrl) {
|
|
3690
4025
|
throw new Error('CACHE_DRIVER="redis" requires REDIS_URL to be set.');
|
|
3691
4026
|
}
|
|
3692
|
-
return new redisCacheStore_default(options.redisUrl, options.ttlMs, options.maxEntries);
|
|
4027
|
+
return new redisCacheStore_default(options.redisUrl, options.ttlMs, options.maxEntries, options.redis);
|
|
3693
4028
|
}
|
|
3694
4029
|
return new simpleCacheStore_default(new simpleCache_default(options.ttlMs, options.maxEntries));
|
|
3695
4030
|
}
|
|
@@ -3702,6 +4037,8 @@ class TaggedCache {
|
|
|
3702
4037
|
this.tags = tags;
|
|
3703
4038
|
}
|
|
3704
4039
|
async remember(key, callback, ttlMs) {
|
|
4040
|
+
if (this.store.getOrSetTagged)
|
|
4041
|
+
return this.store.getOrSetTagged(key, callback, this.tags, ttlMs);
|
|
3705
4042
|
const value = await this.store.getOrSet(key, callback, ttlMs);
|
|
3706
4043
|
await this.store.attachTags(key, this.tags);
|
|
3707
4044
|
return value;
|
|
@@ -3718,6 +4055,9 @@ class CacheRepository {
|
|
|
3718
4055
|
constructor(store) {
|
|
3719
4056
|
this.store = store;
|
|
3720
4057
|
}
|
|
4058
|
+
async close() {
|
|
4059
|
+
await this.store.close?.();
|
|
4060
|
+
}
|
|
3721
4061
|
async get(key) {
|
|
3722
4062
|
return this.store.get(key);
|
|
3723
4063
|
}
|
|
@@ -3765,90 +4105,14 @@ var CACHE_TAGS = {
|
|
|
3765
4105
|
careers: "careers",
|
|
3766
4106
|
offers: "offers"
|
|
3767
4107
|
};
|
|
3768
|
-
// ../../src/core/
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
services = new Map;
|
|
3777
|
-
singletonFactories = new Map;
|
|
3778
|
-
bindings = new Map;
|
|
3779
|
-
set(key, value) {
|
|
3780
|
-
this.singletonFactories.delete(key);
|
|
3781
|
-
this.bindings.delete(key);
|
|
3782
|
-
this.services.set(key, value);
|
|
3783
|
-
return value;
|
|
3784
|
-
}
|
|
3785
|
-
singleton(key, factory) {
|
|
3786
|
-
this.bindings.delete(key);
|
|
3787
|
-
this.services.delete(key);
|
|
3788
|
-
this.singletonFactories.set(key, factory);
|
|
3789
|
-
}
|
|
3790
|
-
bind(key, factory) {
|
|
3791
|
-
this.singletonFactories.delete(key);
|
|
3792
|
-
this.services.delete(key);
|
|
3793
|
-
this.bindings.set(key, factory);
|
|
3794
|
-
}
|
|
3795
|
-
get(key) {
|
|
3796
|
-
if (this.services.has(key)) {
|
|
3797
|
-
return this.services.get(key);
|
|
3798
|
-
}
|
|
3799
|
-
const singletonFactory = this.singletonFactories.get(key);
|
|
3800
|
-
if (singletonFactory) {
|
|
3801
|
-
const value = singletonFactory(this);
|
|
3802
|
-
this.services.set(key, value);
|
|
3803
|
-
return value;
|
|
3804
|
-
}
|
|
3805
|
-
const binding = this.bindings.get(key);
|
|
3806
|
-
if (binding) {
|
|
3807
|
-
return binding(this);
|
|
3808
|
-
}
|
|
3809
|
-
throw new Error(`Service "${key}" is not registered.`);
|
|
3810
|
-
}
|
|
3811
|
-
resolve(key) {
|
|
3812
|
-
return this.get(key);
|
|
3813
|
-
}
|
|
3814
|
-
make(key) {
|
|
3815
|
-
return this.resolve(key);
|
|
3816
|
-
}
|
|
3817
|
-
instance(key, value) {
|
|
3818
|
-
return this.set(key, value);
|
|
3819
|
-
}
|
|
3820
|
-
has(key) {
|
|
3821
|
-
return this.services.has(key) || this.singletonFactories.has(key) || this.bindings.has(key);
|
|
3822
|
-
}
|
|
3823
|
-
}
|
|
3824
|
-
|
|
3825
|
-
class ConfigStore {
|
|
3826
|
-
values = new Map;
|
|
3827
|
-
set(key, value) {
|
|
3828
|
-
this.values.set(key, value);
|
|
3829
|
-
return value;
|
|
3830
|
-
}
|
|
3831
|
-
get(key) {
|
|
3832
|
-
return this.values.get(key);
|
|
3833
|
-
}
|
|
3834
|
-
require(key) {
|
|
3835
|
-
if (!this.values.has(key)) {
|
|
3836
|
-
throw new Error(`Config key "${key}" is not defined.`);
|
|
3837
|
-
}
|
|
3838
|
-
return this.values.get(key);
|
|
3839
|
-
}
|
|
3840
|
-
has(key) {
|
|
3841
|
-
return this.values.has(key);
|
|
3842
|
-
}
|
|
3843
|
-
}
|
|
3844
|
-
// ../../src/core/database/baseRepository.ts
|
|
3845
|
-
init_eventBus();
|
|
3846
|
-
|
|
3847
|
-
// ../../src/core/events/index.ts
|
|
3848
|
-
init_deferredModelEvents();
|
|
3849
|
-
init_eventBus();
|
|
3850
|
-
function modelEventName(tableName, action) {
|
|
3851
|
-
return `${tableName}.${action}`;
|
|
4108
|
+
// ../../src/core/database/baseRepository.ts
|
|
4109
|
+
init_eventBus();
|
|
4110
|
+
|
|
4111
|
+
// ../../src/core/events/index.ts
|
|
4112
|
+
init_deferredModelEvents();
|
|
4113
|
+
init_eventBus();
|
|
4114
|
+
function modelEventName(tableName, action) {
|
|
4115
|
+
return `${tableName}.${action}`;
|
|
3852
4116
|
}
|
|
3853
4117
|
|
|
3854
4118
|
// ../../src/core/pagination/index.ts
|
|
@@ -3864,6 +4128,7 @@ function buildPaginationMeta(input) {
|
|
|
3864
4128
|
|
|
3865
4129
|
// ../../src/core/database/baseRepository.ts
|
|
3866
4130
|
init_connectionContext();
|
|
4131
|
+
init_dialect();
|
|
3867
4132
|
|
|
3868
4133
|
// ../../src/core/database/query.ts
|
|
3869
4134
|
init_dialect();
|
|
@@ -4684,6 +4949,7 @@ class RepositoryQuery {
|
|
|
4684
4949
|
queryOptions;
|
|
4685
4950
|
eagerLoads = [];
|
|
4686
4951
|
whereNodes = [];
|
|
4952
|
+
protectedWhere = [];
|
|
4687
4953
|
constructor(repository, whereClause = {}, queryOptions = {}) {
|
|
4688
4954
|
this.repository = repository;
|
|
4689
4955
|
this.whereClause = whereClause;
|
|
@@ -4693,10 +4959,23 @@ class RepositoryQuery {
|
|
|
4693
4959
|
if (typeof input === "function") {
|
|
4694
4960
|
const builder = new WhereBuilder;
|
|
4695
4961
|
input(builder);
|
|
4696
|
-
|
|
4962
|
+
if (builder.nodes.length > 0)
|
|
4963
|
+
this.whereNodes.push({ kind: "and", group: builder.nodes });
|
|
4697
4964
|
return this;
|
|
4698
4965
|
}
|
|
4699
|
-
this.
|
|
4966
|
+
this.whereNodes.push({ kind: "and", where: { ...input } });
|
|
4967
|
+
return this;
|
|
4968
|
+
}
|
|
4969
|
+
protectWhere() {
|
|
4970
|
+
const nodes = [];
|
|
4971
|
+
if (Object.keys(this.whereClause).length > 0) {
|
|
4972
|
+
nodes.push({ kind: "and", where: this.whereClause });
|
|
4973
|
+
}
|
|
4974
|
+
nodes.push(...this.whereNodes);
|
|
4975
|
+
if (nodes.length > 0)
|
|
4976
|
+
this.protectedWhere.push({ kind: "and", group: nodes });
|
|
4977
|
+
this.whereClause = {};
|
|
4978
|
+
this.whereNodes.length = 0;
|
|
4700
4979
|
return this;
|
|
4701
4980
|
}
|
|
4702
4981
|
orWhere(input) {
|
|
@@ -4810,9 +5089,10 @@ class RepositoryQuery {
|
|
|
4810
5089
|
});
|
|
4811
5090
|
return this;
|
|
4812
5091
|
}
|
|
4813
|
-
withMorphTo(as, relation, repositoriesByType, options = {}) {
|
|
5092
|
+
withMorphTo(as, relation, repositoriesByType, options = {}, optionsByType) {
|
|
4814
5093
|
this.eagerLoads.push({
|
|
4815
5094
|
kind: "morphTo",
|
|
5095
|
+
morphOptions: optionsByType,
|
|
4816
5096
|
as,
|
|
4817
5097
|
relation,
|
|
4818
5098
|
repository: this.repository,
|
|
@@ -4883,6 +5163,19 @@ class RepositoryQuery {
|
|
|
4883
5163
|
}
|
|
4884
5164
|
return row[column];
|
|
4885
5165
|
}
|
|
5166
|
+
async project(columns, limit) {
|
|
5167
|
+
if (columns.length === 0)
|
|
5168
|
+
throw new RangeError("A projection requires at least one column.");
|
|
5169
|
+
const table = this.repository.getTable();
|
|
5170
|
+
for (const column of columns)
|
|
5171
|
+
if (!table.columns.includes(column))
|
|
5172
|
+
throw new TypeError(`Unknown projection column: ${column}`);
|
|
5173
|
+
return await this.repository.findAll({
|
|
5174
|
+
...this.buildOptions(),
|
|
5175
|
+
...limit === undefined ? {} : { limit },
|
|
5176
|
+
select: uniqueColumnSelect(table.name, columns)
|
|
5177
|
+
});
|
|
5178
|
+
}
|
|
4886
5179
|
async attachToRows(rows) {
|
|
4887
5180
|
return await this.attach(rows);
|
|
4888
5181
|
}
|
|
@@ -4893,11 +5186,24 @@ class RepositoryQuery {
|
|
|
4893
5186
|
perPage: options.perPage
|
|
4894
5187
|
});
|
|
4895
5188
|
}
|
|
5189
|
+
async chunk(count, callback) {
|
|
5190
|
+
await this.repository.chunk(count, async (rows) => callback(await this.attach(rows)), this.buildOptions());
|
|
5191
|
+
}
|
|
5192
|
+
async cursorPaginate(options) {
|
|
5193
|
+
const page = await this.repository.cursorPaginate({ ...this.buildOptions(), ...options });
|
|
5194
|
+
return { ...page, data: await this.attach(page.data) };
|
|
5195
|
+
}
|
|
5196
|
+
getOptions() {
|
|
5197
|
+
return this.buildOptions();
|
|
5198
|
+
}
|
|
4896
5199
|
buildOptions() {
|
|
5200
|
+
if (this.protectedWhere.length === 0) {
|
|
5201
|
+
return { ...this.queryOptions, where: this.whereClause, whereNodes: this.whereNodes };
|
|
5202
|
+
}
|
|
4897
5203
|
return {
|
|
4898
5204
|
...this.queryOptions,
|
|
4899
|
-
where:
|
|
4900
|
-
whereNodes: this.whereNodes
|
|
5205
|
+
where: {},
|
|
5206
|
+
whereNodes: [...this.protectedWhere, { kind: "and", group: [...this.whereNodes] }]
|
|
4901
5207
|
};
|
|
4902
5208
|
}
|
|
4903
5209
|
addJoin(type, left, right) {
|
|
@@ -4974,10 +5280,27 @@ class RepositoryQuery {
|
|
|
4974
5280
|
}
|
|
4975
5281
|
if (load.kind === "morphTo") {
|
|
4976
5282
|
const relation = load.relation;
|
|
4977
|
-
const
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
|
|
5283
|
+
const repositories = load.morphRepositories ?? new Map;
|
|
5284
|
+
const partitions = new Map;
|
|
5285
|
+
rows.forEach((row, index) => {
|
|
5286
|
+
const type = String(row[relation.morphTypeKey]);
|
|
5287
|
+
const target = result[index];
|
|
5288
|
+
if (!target || !repositories.has(type))
|
|
5289
|
+
return;
|
|
5290
|
+
const partition = partitions.get(type) ?? { rows: [], targets: [] };
|
|
5291
|
+
partition.rows.push(row);
|
|
5292
|
+
partition.targets.push(target);
|
|
5293
|
+
partitions.set(type, partition);
|
|
5294
|
+
});
|
|
5295
|
+
await Promise.all([...partitions.entries()].map(async ([type, partition]) => {
|
|
5296
|
+
const repository = repositories.get(type);
|
|
5297
|
+
if (!repository)
|
|
5298
|
+
return;
|
|
5299
|
+
const grouped = await this.repository.loadMorphToForChildren(partition.rows, relation, new Map([[type, repository]]), load.morphOptions?.get(type) ?? load.options);
|
|
5300
|
+
for (const row of partition.targets) {
|
|
5301
|
+
row[load.as] = getByRelationKey(grouped, row[relation.morphIdKey]);
|
|
5302
|
+
}
|
|
5303
|
+
}));
|
|
4981
5304
|
return;
|
|
4982
5305
|
}
|
|
4983
5306
|
const relation = load.relation;
|
|
@@ -5099,14 +5422,20 @@ class BaseRepository {
|
|
|
5099
5422
|
if (!Number.isInteger(perPage) || perPage <= 0) {
|
|
5100
5423
|
throw new Error("Cursor page size must be a positive integer.");
|
|
5101
5424
|
}
|
|
5102
|
-
const
|
|
5425
|
+
const filters = [];
|
|
5426
|
+
if (Object.keys(where).length > 0)
|
|
5427
|
+
filters.push({ kind: "and", where });
|
|
5428
|
+
filters.push(...whereNodes ?? []);
|
|
5429
|
+
const cursorFilters = [{ kind: "and", group: filters }];
|
|
5103
5430
|
if (cursor !== undefined) {
|
|
5431
|
+
const cursorWhere = {};
|
|
5104
5432
|
cursorWhere[cursorColumn] = direction === "asc" ? { gt: cursor } : { lt: cursor };
|
|
5433
|
+
cursorFilters.push({ kind: "and", where: cursorWhere });
|
|
5105
5434
|
}
|
|
5106
5435
|
const rows = await this.findAll({
|
|
5107
5436
|
...queryOptions,
|
|
5108
|
-
where:
|
|
5109
|
-
whereNodes,
|
|
5437
|
+
where: {},
|
|
5438
|
+
whereNodes: cursorFilters,
|
|
5110
5439
|
orderBy: { [cursorColumn]: direction },
|
|
5111
5440
|
limit: perPage + 1
|
|
5112
5441
|
});
|
|
@@ -5330,46 +5659,29 @@ class BaseRepository {
|
|
|
5330
5659
|
if (parents.length === 0) {
|
|
5331
5660
|
return indexHasManyThroughRelation(parents, [], relation);
|
|
5332
5661
|
}
|
|
5662
|
+
if (options.joins?.some((join) => join.table === relation.throughTable)) {
|
|
5663
|
+
throw new Error("A related model scope joins the through table twice; use an explicitly aliased correlated predicate instead.");
|
|
5664
|
+
}
|
|
5333
5665
|
const parentIds = [...new Set(parents.map((parent) => parent[relation.localKey]))];
|
|
5334
5666
|
const throughParentKey = relation.throughParentKey ?? "__through_parent_id";
|
|
5335
5667
|
const farTable = this.table.name;
|
|
5336
5668
|
const columns = this.table.columns.map((column) => `${qualifyColumn(farTable, column)}`).join(", ");
|
|
5337
|
-
const placeholders = parentIds.map((_, index) =>
|
|
5338
|
-
const { text: extraWhere, params: extraParams } = this.buildThroughWhere(options, parentIds
|
|
5339
|
-
const
|
|
5340
|
-
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}`;
|
|
5669
|
+
const placeholders = parentIds.map((_, index) => currentSqlDialect().placeholder(index + 1)).join(", ");
|
|
5670
|
+
const { text: extraWhere, params: extraParams } = this.buildThroughWhere(options, parentIds);
|
|
5671
|
+
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)}${buildJoinClause(options.joins)} WHERE ${qualifyColumn(relation.throughTable, relation.firstKey)} IN (${placeholders})${extraWhere}`;
|
|
5341
5672
|
const children = await this.connection.unsafe(sql, [
|
|
5342
5673
|
...parentIds,
|
|
5343
5674
|
...extraParams
|
|
5344
5675
|
]);
|
|
5345
5676
|
return indexHasManyThroughRelation(parents, children, relation);
|
|
5346
5677
|
}
|
|
5347
|
-
|
|
5348
|
-
const
|
|
5349
|
-
|
|
5350
|
-
|
|
5351
|
-
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
return ` AND ${qualified} IS NOT NULL`;
|
|
5355
|
-
}
|
|
5356
|
-
if (options.withTrashed) {
|
|
5357
|
-
return "";
|
|
5358
|
-
}
|
|
5359
|
-
return ` AND ${qualified} IS NULL`;
|
|
5360
|
-
}
|
|
5361
|
-
buildThroughWhere(options, paramOffset = 1) {
|
|
5362
|
-
const where = options.where ?? {};
|
|
5363
|
-
const entries = Object.entries(where);
|
|
5364
|
-
if (entries.length === 0) {
|
|
5365
|
-
return { text: "", params: [] };
|
|
5366
|
-
}
|
|
5367
|
-
const params = [];
|
|
5368
|
-
const clauses = entries.map(([column, value], index) => {
|
|
5369
|
-
params.push(value);
|
|
5370
|
-
return `${qualifyColumn(this.table.name, column)} = $${paramOffset + index + 1}`;
|
|
5371
|
-
});
|
|
5372
|
-
return { text: ` AND ${clauses.join(" AND ")}`, params };
|
|
5678
|
+
buildThroughWhere(options, parentIds) {
|
|
5679
|
+
const parameters = [...parentIds];
|
|
5680
|
+
const { clause } = buildQueryWhereClause(this.table, options, options.whereNodes, parameters);
|
|
5681
|
+
return {
|
|
5682
|
+
text: clause ? ` AND (${clause.replace(/^ WHERE /, "")})` : "",
|
|
5683
|
+
params: parameters.slice(parentIds.length)
|
|
5684
|
+
};
|
|
5373
5685
|
}
|
|
5374
5686
|
async loadBelongsToForParents(children, relation, parentRepository, options = {}) {
|
|
5375
5687
|
if (children.length === 0) {
|
|
@@ -5436,7 +5748,7 @@ class BaseRepository {
|
|
|
5436
5748
|
return indexBelongsToManyRelation(parents, [], [], relation);
|
|
5437
5749
|
}
|
|
5438
5750
|
const parentIds = [...new Set(parents.map((parent) => parent[relation.parentKey]))];
|
|
5439
|
-
const placeholders = parentIds.map((_, index) =>
|
|
5751
|
+
const placeholders = parentIds.map((_, index) => currentSqlDialect().placeholder(index + 1)).join(", ");
|
|
5440
5752
|
const pivotRows = await this.connection.unsafe(`SELECT * FROM ${relation.pivotTable} WHERE ${String(relation.foreignPivotKey)} IN (${placeholders})`, parentIds);
|
|
5441
5753
|
if (pivotRows.length === 0) {
|
|
5442
5754
|
return indexBelongsToManyRelation(parents, [], [], relation);
|
|
@@ -5794,7 +6106,12 @@ async function freshDatabase(db, migrations, options = {}) {
|
|
|
5794
6106
|
}
|
|
5795
6107
|
await runFresh();
|
|
5796
6108
|
}
|
|
6109
|
+
// ../../src/core/database/model.ts
|
|
6110
|
+
import { isDeepStrictEqual } from "util";
|
|
6111
|
+
init_deferredModelEvents();
|
|
6112
|
+
|
|
5797
6113
|
// ../../src/core/database/relationQuery.ts
|
|
6114
|
+
init_dialect();
|
|
5798
6115
|
function asWhere(where) {
|
|
5799
6116
|
return where;
|
|
5800
6117
|
}
|
|
@@ -5816,13 +6133,48 @@ function pluckFromQuery(query, column, keyBy) {
|
|
|
5816
6133
|
}
|
|
5817
6134
|
return keyBy === undefined ? query.pluck(column) : query.pluck(column, keyBy);
|
|
5818
6135
|
}
|
|
6136
|
+
function scopedRelatedQuery(model, repository = model.repository()) {
|
|
6137
|
+
const query = repository.query();
|
|
6138
|
+
const scopedModel = model;
|
|
6139
|
+
return scopedModel.scopeQuery?.(query) ?? query;
|
|
6140
|
+
}
|
|
6141
|
+
function relatedOptions(model, filters, options = {}) {
|
|
6142
|
+
const query = scopedRelatedQuery(model);
|
|
6143
|
+
for (const filter of filters)
|
|
6144
|
+
query.where(filter);
|
|
6145
|
+
return { ...query.getOptions(), ...options };
|
|
6146
|
+
}
|
|
6147
|
+
function relatedRead(model, connection, binding, filters, options = {}) {
|
|
6148
|
+
const query = scopedRelatedQuery(model, model.repository().withConnection(connection));
|
|
6149
|
+
query.where(binding).protectWhere();
|
|
6150
|
+
for (const filter of filters)
|
|
6151
|
+
query.where(filter);
|
|
6152
|
+
if (options.orderBy)
|
|
6153
|
+
query.orderBy(options.orderBy);
|
|
6154
|
+
if (options.limit !== undefined)
|
|
6155
|
+
query.limit(options.limit);
|
|
6156
|
+
return query;
|
|
6157
|
+
}
|
|
6158
|
+
function relatedConditions(model, filters, options = {}, parameters = [], reservedTables = []) {
|
|
6159
|
+
const compiled = relatedOptions(model, filters, options);
|
|
6160
|
+
if (compiled.joins?.some((join) => reservedTables.includes(join.table))) {
|
|
6161
|
+
throw new Error("A related model scope joins a reserved relation table; use an explicitly aliased correlated predicate instead.");
|
|
6162
|
+
}
|
|
6163
|
+
const extra = buildQueryWhereClause(model.repository().getTable(), compiled, compiled.whereNodes, parameters);
|
|
6164
|
+
const predicate = extra.clause.replace(/^ WHERE /, "");
|
|
6165
|
+
return {
|
|
6166
|
+
sql: predicate ? ` AND (${predicate})` : "",
|
|
6167
|
+
joins: buildJoinClause(compiled.joins),
|
|
6168
|
+
params: extra.params
|
|
6169
|
+
};
|
|
6170
|
+
}
|
|
5819
6171
|
|
|
5820
6172
|
class HasManyRelationQuery {
|
|
5821
6173
|
parent;
|
|
5822
6174
|
related;
|
|
5823
6175
|
relation;
|
|
5824
6176
|
kind = "hasMany";
|
|
5825
|
-
extraWhere =
|
|
6177
|
+
extraWhere = [];
|
|
5826
6178
|
extraOptions = {};
|
|
5827
6179
|
constructor(parent, related, relation) {
|
|
5828
6180
|
this.parent = parent;
|
|
@@ -5830,7 +6182,7 @@ class HasManyRelationQuery {
|
|
|
5830
6182
|
this.relation = relation;
|
|
5831
6183
|
}
|
|
5832
6184
|
where(where) {
|
|
5833
|
-
this.extraWhere
|
|
6185
|
+
this.extraWhere.push({ ...where });
|
|
5834
6186
|
return this;
|
|
5835
6187
|
}
|
|
5836
6188
|
orderBy(orderBy) {
|
|
@@ -5842,7 +6194,7 @@ class HasManyRelationQuery {
|
|
|
5842
6194
|
return this;
|
|
5843
6195
|
}
|
|
5844
6196
|
applyEagerLoad(query, alias) {
|
|
5845
|
-
query.withHasMany(alias, this.relation, this.related.repository(), this.extraOptions);
|
|
6197
|
+
query.withHasMany(alias, this.relation, this.related.repository(), relatedOptions(this.related, this.extraWhere, this.extraOptions));
|
|
5846
6198
|
}
|
|
5847
6199
|
hydrateEager(row, alias) {
|
|
5848
6200
|
const value = row[alias] ?? [];
|
|
@@ -5851,24 +6203,12 @@ class HasManyRelationQuery {
|
|
|
5851
6203
|
}
|
|
5852
6204
|
toExistsClause(parentTable) {
|
|
5853
6205
|
const childTable = this.related.repository().getTable().name;
|
|
5854
|
-
const extra =
|
|
5855
|
-
const
|
|
5856
|
-
const sql = `SELECT 1 FROM ${quoteIdentifier(childTable)} WHERE ${qualifyColumn(childTable, this.relation.foreignKey)} = ${qualifyColumn(parentTable, this.relation.localKey)}${extraSql ? ` AND ${extraSql}` : ""}`;
|
|
6206
|
+
const extra = relatedConditions(this.related, this.extraWhere, this.extraOptions, [], [parentTable]);
|
|
6207
|
+
const sql = `SELECT 1 FROM ${quoteIdentifier(childTable)}${extra.joins} WHERE ${qualifyColumn(childTable, this.relation.foreignKey)} = ${qualifyColumn(parentTable, this.relation.localKey)}${extra.sql}`;
|
|
5857
6208
|
return { sql, params: extra.params };
|
|
5858
6209
|
}
|
|
5859
6210
|
scopedQuery() {
|
|
5860
|
-
|
|
5861
|
-
let query = repository.query(asWhere({
|
|
5862
|
-
[this.relation.foreignKey]: this.parent.get(this.relation.localKey),
|
|
5863
|
-
...this.extraWhere
|
|
5864
|
-
}));
|
|
5865
|
-
if (this.extraOptions.orderBy) {
|
|
5866
|
-
query = query.orderBy(this.extraOptions.orderBy);
|
|
5867
|
-
}
|
|
5868
|
-
if (this.extraOptions.limit !== undefined) {
|
|
5869
|
-
query = query.limit(this.extraOptions.limit);
|
|
5870
|
-
}
|
|
5871
|
-
return query;
|
|
6211
|
+
return relatedRead(this.related, this.parent.getRepository().getConnection(), asWhere({ [this.relation.foreignKey]: this.parent.get(this.relation.localKey) }), this.extraWhere, this.extraOptions);
|
|
5872
6212
|
}
|
|
5873
6213
|
async get() {
|
|
5874
6214
|
const rows = await this.scopedQuery().get();
|
|
@@ -5934,7 +6274,7 @@ class HasOneRelationQuery {
|
|
|
5934
6274
|
return this;
|
|
5935
6275
|
}
|
|
5936
6276
|
applyEagerLoad(query, alias) {
|
|
5937
|
-
this.inner.
|
|
6277
|
+
this.inner.applyEagerLoad(query, alias);
|
|
5938
6278
|
}
|
|
5939
6279
|
hydrateEager(row, alias) {
|
|
5940
6280
|
const hydrated = this.inner.hydrateEager(row, alias);
|
|
@@ -5975,7 +6315,7 @@ class BelongsToRelationQuery {
|
|
|
5975
6315
|
related;
|
|
5976
6316
|
relation;
|
|
5977
6317
|
kind = "belongsTo";
|
|
5978
|
-
extraWhere =
|
|
6318
|
+
extraWhere = [];
|
|
5979
6319
|
extraOptions = {};
|
|
5980
6320
|
constructor(parent, related, relation) {
|
|
5981
6321
|
this.parent = parent;
|
|
@@ -5983,7 +6323,7 @@ class BelongsToRelationQuery {
|
|
|
5983
6323
|
this.relation = relation;
|
|
5984
6324
|
}
|
|
5985
6325
|
where(where) {
|
|
5986
|
-
this.extraWhere
|
|
6326
|
+
this.extraWhere.push({ ...where });
|
|
5987
6327
|
return this;
|
|
5988
6328
|
}
|
|
5989
6329
|
orderBy(orderBy) {
|
|
@@ -5991,7 +6331,7 @@ class BelongsToRelationQuery {
|
|
|
5991
6331
|
return this;
|
|
5992
6332
|
}
|
|
5993
6333
|
applyEagerLoad(query, alias) {
|
|
5994
|
-
query.withBelongsTo(alias, this.relation, this.related.repository(), this.extraOptions);
|
|
6334
|
+
query.withBelongsTo(alias, this.relation, this.related.repository(), relatedOptions(this.related, this.extraWhere, this.extraOptions));
|
|
5995
6335
|
}
|
|
5996
6336
|
hydrateEager(row, alias) {
|
|
5997
6337
|
const value = row[alias];
|
|
@@ -5999,9 +6339,8 @@ class BelongsToRelationQuery {
|
|
|
5999
6339
|
}
|
|
6000
6340
|
toExistsClause(parentTable) {
|
|
6001
6341
|
const relatedTable = this.related.repository().getTable().name;
|
|
6002
|
-
const extra =
|
|
6003
|
-
const
|
|
6004
|
-
const sql = `SELECT 1 FROM ${quoteIdentifier(relatedTable)} WHERE ${qualifyColumn(relatedTable, this.relation.ownerKey)} = ${qualifyColumn(parentTable, this.relation.foreignKey)}${extraSql ? ` AND ${extraSql}` : ""}`;
|
|
6342
|
+
const extra = relatedConditions(this.related, this.extraWhere, this.extraOptions, [], [parentTable]);
|
|
6343
|
+
const sql = `SELECT 1 FROM ${quoteIdentifier(relatedTable)}${extra.joins} WHERE ${qualifyColumn(relatedTable, this.relation.ownerKey)} = ${qualifyColumn(parentTable, this.relation.foreignKey)}${extra.sql}`;
|
|
6005
6344
|
return { sql, params: extra.params };
|
|
6006
6345
|
}
|
|
6007
6346
|
async get() {
|
|
@@ -6026,12 +6365,7 @@ class BelongsToRelationQuery {
|
|
|
6026
6365
|
if (foreign === null || foreign === undefined) {
|
|
6027
6366
|
return null;
|
|
6028
6367
|
}
|
|
6029
|
-
|
|
6030
|
-
let query = repository.query(asWhere({ [this.relation.ownerKey]: foreign, ...this.extraWhere }));
|
|
6031
|
-
if (this.extraOptions.orderBy) {
|
|
6032
|
-
query = query.orderBy(this.extraOptions.orderBy);
|
|
6033
|
-
}
|
|
6034
|
-
return query;
|
|
6368
|
+
return relatedRead(this.related, this.parent.getRepository().getConnection(), asWhere({ [this.relation.ownerKey]: foreign }), this.extraWhere, this.extraOptions);
|
|
6035
6369
|
}
|
|
6036
6370
|
then(onfulfilled, onrejected) {
|
|
6037
6371
|
return thenGet(() => this.get(), onfulfilled, onrejected);
|
|
@@ -6053,7 +6387,7 @@ class BelongsToManyRelationQuery {
|
|
|
6053
6387
|
related;
|
|
6054
6388
|
relation;
|
|
6055
6389
|
kind = "belongsToMany";
|
|
6056
|
-
extraWhere =
|
|
6390
|
+
extraWhere = [];
|
|
6057
6391
|
extraOptions = {};
|
|
6058
6392
|
pivotValues = {};
|
|
6059
6393
|
constructor(parent, related, relation) {
|
|
@@ -6062,7 +6396,7 @@ class BelongsToManyRelationQuery {
|
|
|
6062
6396
|
this.relation = relation;
|
|
6063
6397
|
}
|
|
6064
6398
|
where(where) {
|
|
6065
|
-
this.extraWhere
|
|
6399
|
+
this.extraWhere.push({ ...where });
|
|
6066
6400
|
return this;
|
|
6067
6401
|
}
|
|
6068
6402
|
orderBy(orderBy) {
|
|
@@ -6070,7 +6404,7 @@ class BelongsToManyRelationQuery {
|
|
|
6070
6404
|
return this;
|
|
6071
6405
|
}
|
|
6072
6406
|
applyEagerLoad(query, alias) {
|
|
6073
|
-
query.withBelongsToMany(alias, this.relation, this.related.repository(), this.extraOptions);
|
|
6407
|
+
query.withBelongsToMany(alias, this.relation, this.related.repository(), relatedOptions(this.related, this.extraWhere, this.extraOptions));
|
|
6074
6408
|
}
|
|
6075
6409
|
withPivotValues(values) {
|
|
6076
6410
|
this.pivotValues = { ...this.pivotValues, ...values };
|
|
@@ -6083,31 +6417,16 @@ class BelongsToManyRelationQuery {
|
|
|
6083
6417
|
}
|
|
6084
6418
|
toExistsClause(parentTable) {
|
|
6085
6419
|
const relatedTable = this.related.repository().getTable().name;
|
|
6086
|
-
const extra =
|
|
6087
|
-
const
|
|
6088
|
-
const sql = `SELECT 1 FROM ${quoteIdentifier(relatedTable)} INNER JOIN ${quoteIdentifier(this.relation.pivotTable)} ON ${qualifyColumn(this.relation.pivotTable, this.relation.relatedPivotKey)} = ${qualifyColumn(relatedTable, this.relation.relatedKey)} WHERE ${qualifyColumn(this.relation.pivotTable, this.relation.foreignPivotKey)} = ${qualifyColumn(parentTable, this.relation.parentKey)}${extraSql ? ` AND ${extraSql}` : ""}`;
|
|
6420
|
+
const extra = relatedConditions(this.related, this.extraWhere, this.extraOptions, [], [parentTable, this.relation.pivotTable]);
|
|
6421
|
+
const sql = `SELECT 1 FROM ${quoteIdentifier(relatedTable)} INNER JOIN ${quoteIdentifier(this.relation.pivotTable)} ON ${qualifyColumn(this.relation.pivotTable, this.relation.relatedPivotKey)} = ${qualifyColumn(relatedTable, this.relation.relatedKey)}${extra.joins} WHERE ${qualifyColumn(this.relation.pivotTable, this.relation.foreignPivotKey)} = ${qualifyColumn(parentTable, this.relation.parentKey)}${extra.sql}`;
|
|
6089
6422
|
return { sql, params: extra.params };
|
|
6090
6423
|
}
|
|
6091
6424
|
connection() {
|
|
6092
6425
|
return this.parent.getRepository().getConnection();
|
|
6093
6426
|
}
|
|
6094
6427
|
async get() {
|
|
6095
|
-
const
|
|
6096
|
-
const
|
|
6097
|
-
if (pivotRows.length === 0) {
|
|
6098
|
-
return [];
|
|
6099
|
-
}
|
|
6100
|
-
const relatedIds = [
|
|
6101
|
-
...new Set(pivotRows.map((row) => row[this.relation.relatedPivotKey]))
|
|
6102
|
-
];
|
|
6103
|
-
const repository = this.related.repository().withConnection(this.connection());
|
|
6104
|
-
const rows = await repository.findAll({
|
|
6105
|
-
...this.extraOptions,
|
|
6106
|
-
where: asWhere({
|
|
6107
|
-
[this.relation.relatedKey]: relatedIds,
|
|
6108
|
-
...this.extraWhere
|
|
6109
|
-
})
|
|
6110
|
-
});
|
|
6428
|
+
const query = await this.relatedQuery();
|
|
6429
|
+
const rows = query ? await query.get() : [];
|
|
6111
6430
|
return rows.map((row) => this.related.newFromRecord(row));
|
|
6112
6431
|
}
|
|
6113
6432
|
async first() {
|
|
@@ -6115,9 +6434,8 @@ class BelongsToManyRelationQuery {
|
|
|
6115
6434
|
return rows[0] ?? null;
|
|
6116
6435
|
}
|
|
6117
6436
|
async count() {
|
|
6118
|
-
const
|
|
6119
|
-
|
|
6120
|
-
return Number(rows[0]?.count ?? 0);
|
|
6437
|
+
const query = await this.relatedQuery();
|
|
6438
|
+
return query ? query.count() : 0;
|
|
6121
6439
|
}
|
|
6122
6440
|
async pluck(column, keyBy) {
|
|
6123
6441
|
return pluckFromQuery(await this.relatedQuery(), column, keyBy);
|
|
@@ -6128,21 +6446,14 @@ class BelongsToManyRelationQuery {
|
|
|
6128
6446
|
}
|
|
6129
6447
|
async relatedQuery() {
|
|
6130
6448
|
const parentId = this.parent.get(this.relation.parentKey);
|
|
6131
|
-
const pivotRows = await this.connection().unsafe(`SELECT * FROM ${quoteIdentifier(this.relation.pivotTable)} WHERE ${quoteIdentifier(String(this.relation.foreignPivotKey))} = $1`, [parentId]);
|
|
6449
|
+
const pivotRows = await this.connection().unsafe(`SELECT * FROM ${quoteIdentifier(this.relation.pivotTable)} WHERE ${quoteIdentifier(String(this.relation.foreignPivotKey))} = ${currentSqlDialect().placeholder(1)}`, [parentId]);
|
|
6132
6450
|
if (pivotRows.length === 0) {
|
|
6133
6451
|
return null;
|
|
6134
6452
|
}
|
|
6135
6453
|
const relatedIds = [
|
|
6136
6454
|
...new Set(pivotRows.map((row) => row[this.relation.relatedPivotKey]))
|
|
6137
6455
|
];
|
|
6138
|
-
|
|
6139
|
-
[this.relation.relatedKey]: relatedIds,
|
|
6140
|
-
...this.extraWhere
|
|
6141
|
-
}));
|
|
6142
|
-
if (this.extraOptions.orderBy) {
|
|
6143
|
-
query = query.orderBy(this.extraOptions.orderBy);
|
|
6144
|
-
}
|
|
6145
|
-
return query;
|
|
6456
|
+
return relatedRead(this.related, this.connection(), asWhere({ [this.relation.relatedKey]: relatedIds }), this.extraWhere, this.extraOptions);
|
|
6146
6457
|
}
|
|
6147
6458
|
then(onfulfilled, onrejected) {
|
|
6148
6459
|
return thenGet(() => this.get(), onfulfilled, onrejected);
|
|
@@ -6198,7 +6509,7 @@ class MorphManyRelationQuery {
|
|
|
6198
6509
|
related;
|
|
6199
6510
|
relation;
|
|
6200
6511
|
kind = "morphMany";
|
|
6201
|
-
extraWhere =
|
|
6512
|
+
extraWhere = [];
|
|
6202
6513
|
extraOptions = {};
|
|
6203
6514
|
constructor(parent, related, relation) {
|
|
6204
6515
|
this.parent = parent;
|
|
@@ -6206,11 +6517,11 @@ class MorphManyRelationQuery {
|
|
|
6206
6517
|
this.relation = relation;
|
|
6207
6518
|
}
|
|
6208
6519
|
where(where) {
|
|
6209
|
-
this.extraWhere
|
|
6520
|
+
this.extraWhere.push({ ...where });
|
|
6210
6521
|
return this;
|
|
6211
6522
|
}
|
|
6212
6523
|
applyEagerLoad(query, alias) {
|
|
6213
|
-
query.withMorphMany(alias, this.relation, this.related.repository(), this.extraOptions);
|
|
6524
|
+
query.withMorphMany(alias, this.relation, this.related.repository(), relatedOptions(this.related, this.extraWhere, this.extraOptions));
|
|
6214
6525
|
}
|
|
6215
6526
|
hydrateEager(row, alias) {
|
|
6216
6527
|
const value = row[alias] ?? [];
|
|
@@ -6219,20 +6530,18 @@ class MorphManyRelationQuery {
|
|
|
6219
6530
|
}
|
|
6220
6531
|
toExistsClause(parentTable) {
|
|
6221
6532
|
const childTable = this.related.repository().getTable().name;
|
|
6222
|
-
const extra =
|
|
6223
|
-
[this.relation.morphTypeKey]: this.relation.morphType,
|
|
6533
|
+
const extra = relatedConditions(this.related, [
|
|
6534
|
+
asWhere({ [this.relation.morphTypeKey]: this.relation.morphType }),
|
|
6224
6535
|
...this.extraWhere
|
|
6225
|
-
|
|
6226
|
-
const
|
|
6227
|
-
const sql = `SELECT 1 FROM ${quoteIdentifier(childTable)} WHERE ${qualifyColumn(childTable, this.relation.morphIdKey)} = ${qualifyColumn(parentTable, this.relation.localKey)}${extraSql ? ` AND ${extraSql}` : ""}`;
|
|
6536
|
+
], this.extraOptions, [], [parentTable]);
|
|
6537
|
+
const sql = `SELECT 1 FROM ${quoteIdentifier(childTable)}${extra.joins} WHERE ${qualifyColumn(childTable, this.relation.morphIdKey)} = ${qualifyColumn(parentTable, this.relation.localKey)}${extra.sql}`;
|
|
6228
6538
|
return { sql, params: extra.params };
|
|
6229
6539
|
}
|
|
6230
6540
|
scopedQuery() {
|
|
6231
|
-
return this.related
|
|
6541
|
+
return relatedRead(this.related, this.parent.getRepository().getConnection(), asWhere({
|
|
6232
6542
|
[this.relation.morphTypeKey]: this.relation.morphType,
|
|
6233
|
-
[this.relation.morphIdKey]: this.parent.get(this.relation.localKey)
|
|
6234
|
-
|
|
6235
|
-
}));
|
|
6543
|
+
[this.relation.morphIdKey]: this.parent.get(this.relation.localKey)
|
|
6544
|
+
}), this.extraWhere);
|
|
6236
6545
|
}
|
|
6237
6546
|
async get() {
|
|
6238
6547
|
const rows = await this.scopedQuery().get();
|
|
@@ -6319,14 +6628,14 @@ class MorphToRelationQuery {
|
|
|
6319
6628
|
relatedByType;
|
|
6320
6629
|
relation;
|
|
6321
6630
|
kind = "morphTo";
|
|
6322
|
-
extraWhere =
|
|
6631
|
+
extraWhere = [];
|
|
6323
6632
|
constructor(parent, relatedByType, relation) {
|
|
6324
6633
|
this.parent = parent;
|
|
6325
6634
|
this.relatedByType = relatedByType;
|
|
6326
6635
|
this.relation = relation;
|
|
6327
6636
|
}
|
|
6328
6637
|
where(where) {
|
|
6329
|
-
this.extraWhere
|
|
6638
|
+
this.extraWhere.push({ ...where });
|
|
6330
6639
|
return this;
|
|
6331
6640
|
}
|
|
6332
6641
|
applyEagerLoad(query, alias) {
|
|
@@ -6334,23 +6643,30 @@ class MorphToRelationQuery {
|
|
|
6334
6643
|
type,
|
|
6335
6644
|
model.repository()
|
|
6336
6645
|
]));
|
|
6337
|
-
query.withMorphTo(alias, this.relation, repositories)
|
|
6646
|
+
query.withMorphTo(alias, this.relation, repositories, {}, new Map(Object.entries(this.relatedByType).map(([type, model]) => [
|
|
6647
|
+
type,
|
|
6648
|
+
relatedOptions(model, this.extraWhere)
|
|
6649
|
+
])));
|
|
6338
6650
|
}
|
|
6339
6651
|
hydrateEager(row, alias) {
|
|
6340
6652
|
return row[alias];
|
|
6341
6653
|
}
|
|
6342
6654
|
toExistsClause(parentTable) {
|
|
6343
|
-
const
|
|
6344
|
-
const
|
|
6345
|
-
if (!
|
|
6346
|
-
return { sql: "SELECT 1 WHERE 1 = 0", params
|
|
6347
|
-
}
|
|
6348
|
-
const
|
|
6349
|
-
|
|
6350
|
-
|
|
6655
|
+
const params = [];
|
|
6656
|
+
const type = this.parent.get(this.relation.morphTypeKey);
|
|
6657
|
+
if (type !== undefined && type !== null && !this.relatedByType[String(type)]) {
|
|
6658
|
+
return { sql: "SELECT 1 WHERE 1 = 0", params };
|
|
6659
|
+
}
|
|
6660
|
+
const branches = Object.entries(this.relatedByType).map(([type, related]) => {
|
|
6661
|
+
params.push(type);
|
|
6662
|
+
const discriminator = currentSqlDialect().placeholder(params.length);
|
|
6663
|
+
const table = related.repository().getTable();
|
|
6664
|
+
const extra = relatedConditions(related, this.extraWhere, {}, params, [parentTable]);
|
|
6665
|
+
return `(${qualifyColumn(parentTable, this.relation.morphTypeKey)} = ${discriminator} AND EXISTS (SELECT 1 FROM ${quoteIdentifier(table.name)}${extra.joins} WHERE ${qualifyColumn(table.name, table.primaryKey)} = ${qualifyColumn(parentTable, this.relation.morphIdKey)}${extra.sql}))`;
|
|
6666
|
+
});
|
|
6351
6667
|
return {
|
|
6352
|
-
sql: `SELECT 1
|
|
6353
|
-
params
|
|
6668
|
+
sql: `SELECT 1 WHERE ${branches.length > 0 ? branches.join(" OR ") : "1 = 0"}`,
|
|
6669
|
+
params
|
|
6354
6670
|
};
|
|
6355
6671
|
}
|
|
6356
6672
|
async get() {
|
|
@@ -6378,7 +6694,7 @@ class MorphToRelationQuery {
|
|
|
6378
6694
|
return null;
|
|
6379
6695
|
}
|
|
6380
6696
|
const table = related.repository().getTable();
|
|
6381
|
-
return related
|
|
6697
|
+
return relatedRead(related, this.parent.getRepository().getConnection(), asWhere({ [table.primaryKey]: id }), this.extraWhere);
|
|
6382
6698
|
}
|
|
6383
6699
|
then(onfulfilled, onrejected) {
|
|
6384
6700
|
return thenGet(() => this.get(), onfulfilled, onrejected);
|
|
@@ -6390,7 +6706,7 @@ class HasManyThroughRelationQuery {
|
|
|
6390
6706
|
related;
|
|
6391
6707
|
relation;
|
|
6392
6708
|
kind = "hasManyThrough";
|
|
6393
|
-
extraWhere =
|
|
6709
|
+
extraWhere = [];
|
|
6394
6710
|
extraOptions = {};
|
|
6395
6711
|
constructor(parent, related, relation) {
|
|
6396
6712
|
this.parent = parent;
|
|
@@ -6398,7 +6714,7 @@ class HasManyThroughRelationQuery {
|
|
|
6398
6714
|
this.relation = relation;
|
|
6399
6715
|
}
|
|
6400
6716
|
where(where) {
|
|
6401
|
-
this.extraWhere
|
|
6717
|
+
this.extraWhere.push({ ...where });
|
|
6402
6718
|
return this;
|
|
6403
6719
|
}
|
|
6404
6720
|
orderBy(orderBy) {
|
|
@@ -6410,7 +6726,7 @@ class HasManyThroughRelationQuery {
|
|
|
6410
6726
|
return this;
|
|
6411
6727
|
}
|
|
6412
6728
|
applyEagerLoad(query, alias) {
|
|
6413
|
-
query.withHasManyThrough(alias, this.relation, this.related.repository(), this.extraOptions);
|
|
6729
|
+
query.withHasManyThrough(alias, this.relation, this.related.repository(), relatedOptions(this.related, this.extraWhere, this.extraOptions));
|
|
6414
6730
|
}
|
|
6415
6731
|
hydrateEager(row, alias) {
|
|
6416
6732
|
const value = row[alias] ?? [];
|
|
@@ -6419,9 +6735,8 @@ class HasManyThroughRelationQuery {
|
|
|
6419
6735
|
}
|
|
6420
6736
|
toExistsClause(parentTable) {
|
|
6421
6737
|
const farTable = this.related.repository().getTable().name;
|
|
6422
|
-
const extra =
|
|
6423
|
-
const
|
|
6424
|
-
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}` : ""}`;
|
|
6738
|
+
const extra = relatedConditions(this.related, this.extraWhere, this.extraOptions, [], [parentTable, this.relation.throughTable]);
|
|
6739
|
+
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)}${extra.joins} WHERE ${qualifyColumn(this.relation.throughTable, this.relation.firstKey)} = ${qualifyColumn(parentTable, this.relation.localKey)}${extra.sql}`;
|
|
6425
6740
|
return { sql, params: extra.params };
|
|
6426
6741
|
}
|
|
6427
6742
|
async get() {
|
|
@@ -6446,8 +6761,7 @@ class HasManyThroughRelationQuery {
|
|
|
6446
6761
|
}
|
|
6447
6762
|
async farRows() {
|
|
6448
6763
|
return this.related.repository().withConnection(this.parent.getRepository().getConnection()).findHasManyThrough(this.parent.get(this.relation.localKey), this.relation, {
|
|
6449
|
-
...this.extraOptions
|
|
6450
|
-
where: this.extraWhere
|
|
6764
|
+
...relatedOptions(this.related, this.extraWhere, this.extraOptions)
|
|
6451
6765
|
});
|
|
6452
6766
|
}
|
|
6453
6767
|
then(onfulfilled, onrejected) {
|
|
@@ -6456,7 +6770,17 @@ class HasManyThroughRelationQuery {
|
|
|
6456
6770
|
}
|
|
6457
6771
|
|
|
6458
6772
|
// ../../src/core/database/model.ts
|
|
6773
|
+
function snapshotAttributes(values) {
|
|
6774
|
+
return Object.fromEntries(Object.entries(values).map(([key, value]) => [
|
|
6775
|
+
key,
|
|
6776
|
+
Buffer.isBuffer(value) ? Buffer.from(value) : structuredClone(value)
|
|
6777
|
+
]));
|
|
6778
|
+
}
|
|
6779
|
+
function changedAttributes(values, original) {
|
|
6780
|
+
return Object.fromEntries(Object.entries(values).filter(([key, value]) => !(key in original) || !isDeepStrictEqual(value, original[key])));
|
|
6781
|
+
}
|
|
6459
6782
|
var modelRepositories = new WeakMap;
|
|
6783
|
+
var modelRepositoryFactories = new WeakMap;
|
|
6460
6784
|
var namedModels = new Map;
|
|
6461
6785
|
var modelGlobalScopes = new WeakMap;
|
|
6462
6786
|
var modelObservers = new WeakMap;
|
|
@@ -6547,12 +6871,54 @@ async function eagerLoadOnModels(models, paths) {
|
|
|
6547
6871
|
async function loadNested(model, path) {
|
|
6548
6872
|
await eagerLoadOnModels([model], [path]);
|
|
6549
6873
|
}
|
|
6874
|
+
function nameModel(model) {
|
|
6875
|
+
const { name, $morphClass } = model;
|
|
6876
|
+
if (name)
|
|
6877
|
+
namedModels.set(name, model);
|
|
6878
|
+
if ($morphClass)
|
|
6879
|
+
namedModels.set($morphClass, model);
|
|
6880
|
+
}
|
|
6550
6881
|
function resolveModelRepository(model) {
|
|
6551
|
-
const
|
|
6552
|
-
if (
|
|
6553
|
-
|
|
6882
|
+
const existing = modelRepositories.get(model);
|
|
6883
|
+
if (existing)
|
|
6884
|
+
return existing;
|
|
6885
|
+
for (let current = model;current; current = Object.getPrototypeOf(current)) {
|
|
6886
|
+
const factory = modelRepositoryFactories.get(current);
|
|
6887
|
+
if (!factory)
|
|
6888
|
+
continue;
|
|
6889
|
+
const repository = factory();
|
|
6890
|
+
modelRepositories.set(model, repository);
|
|
6891
|
+
nameModel(model);
|
|
6892
|
+
return repository;
|
|
6893
|
+
}
|
|
6894
|
+
throw new Error(`${model.name}.repository() is not implemented.`);
|
|
6895
|
+
}
|
|
6896
|
+
function defineModel(table) {
|
|
6897
|
+
|
|
6898
|
+
class TableModel extends Model {
|
|
6899
|
+
}
|
|
6900
|
+
modelRepositoryFactories.set(TableModel, () => new baseRepository_default(table));
|
|
6901
|
+
return TableModel;
|
|
6902
|
+
}
|
|
6903
|
+
function bootModels(models) {
|
|
6904
|
+
const names = new Map;
|
|
6905
|
+
for (const model of models) {
|
|
6906
|
+
const { name, $morphClass } = model;
|
|
6907
|
+
for (const alias of [name, $morphClass]) {
|
|
6908
|
+
if (!alias)
|
|
6909
|
+
continue;
|
|
6910
|
+
const existing = names.get(alias);
|
|
6911
|
+
if (existing && existing !== model)
|
|
6912
|
+
throw new Error(`Duplicate model name [${alias}].`);
|
|
6913
|
+
names.set(alias, model);
|
|
6914
|
+
}
|
|
6915
|
+
}
|
|
6916
|
+
for (const model of models)
|
|
6917
|
+
nameModel(model);
|
|
6918
|
+
for (const model of models) {
|
|
6919
|
+
resolveModelRepository(model);
|
|
6920
|
+
ensureBooted(model);
|
|
6554
6921
|
}
|
|
6555
|
-
return repository;
|
|
6556
6922
|
}
|
|
6557
6923
|
function registerModelClass(name, model) {
|
|
6558
6924
|
namedModels.set(name, model);
|
|
@@ -6603,10 +6969,26 @@ function ensureBooted(model) {
|
|
|
6603
6969
|
if (modelBooted.has(model)) {
|
|
6604
6970
|
return;
|
|
6605
6971
|
}
|
|
6972
|
+
nameModel(model);
|
|
6606
6973
|
modelBooted.add(model);
|
|
6974
|
+
const scopes = modelGlobalScopes.get(model);
|
|
6975
|
+
const observers = modelObservers.get(model);
|
|
6607
6976
|
const boot = model.boot;
|
|
6608
6977
|
if (typeof boot === "function") {
|
|
6609
|
-
|
|
6978
|
+
try {
|
|
6979
|
+
boot.call(model);
|
|
6980
|
+
} catch (error) {
|
|
6981
|
+
modelBooted.delete(model);
|
|
6982
|
+
if (scopes)
|
|
6983
|
+
modelGlobalScopes.set(model, scopes);
|
|
6984
|
+
else
|
|
6985
|
+
modelGlobalScopes.delete(model);
|
|
6986
|
+
if (observers)
|
|
6987
|
+
modelObservers.set(model, observers);
|
|
6988
|
+
else
|
|
6989
|
+
modelObservers.delete(model);
|
|
6990
|
+
throw error;
|
|
6991
|
+
}
|
|
6610
6992
|
}
|
|
6611
6993
|
}
|
|
6612
6994
|
function getGlobalScopes(model) {
|
|
@@ -6734,6 +7116,19 @@ function applyTimestampsOnUpdate(columns, values, enabled) {
|
|
|
6734
7116
|
return result;
|
|
6735
7117
|
}
|
|
6736
7118
|
|
|
7119
|
+
class ModelProjection {
|
|
7120
|
+
load;
|
|
7121
|
+
constructor(load) {
|
|
7122
|
+
this.load = load;
|
|
7123
|
+
}
|
|
7124
|
+
get() {
|
|
7125
|
+
return this.load();
|
|
7126
|
+
}
|
|
7127
|
+
async first() {
|
|
7128
|
+
return (await this.load(1))[0] ?? null;
|
|
7129
|
+
}
|
|
7130
|
+
}
|
|
7131
|
+
|
|
6737
7132
|
class ModelQuery {
|
|
6738
7133
|
modelClass;
|
|
6739
7134
|
query;
|
|
@@ -6766,6 +7161,16 @@ class ModelQuery {
|
|
|
6766
7161
|
this.query.orWhere(input);
|
|
6767
7162
|
return this;
|
|
6768
7163
|
}
|
|
7164
|
+
whereDynamic(input) {
|
|
7165
|
+
this.query.where(input);
|
|
7166
|
+
return this;
|
|
7167
|
+
}
|
|
7168
|
+
select(...columns) {
|
|
7169
|
+
return new ModelProjection(async (limit) => {
|
|
7170
|
+
const rows = await this.query.project(columns, limit);
|
|
7171
|
+
return rows.map((row) => applyCasts(row, modelStatics(this.modelClass).$casts, "hydrate"));
|
|
7172
|
+
});
|
|
7173
|
+
}
|
|
6769
7174
|
orderBy(orderBy) {
|
|
6770
7175
|
this.query.orderBy(orderBy);
|
|
6771
7176
|
return this;
|
|
@@ -6914,7 +7319,7 @@ class ModelQuery {
|
|
|
6914
7319
|
}
|
|
6915
7320
|
async find(id) {
|
|
6916
7321
|
const primaryKey = resolveModelRepository(this.modelClass).getTable().primaryKey;
|
|
6917
|
-
return this.
|
|
7322
|
+
return this.whereDynamic({ [primaryKey]: id }).first();
|
|
6918
7323
|
}
|
|
6919
7324
|
async findOrFail(id, errorFactory) {
|
|
6920
7325
|
const model = await this.find(id);
|
|
@@ -6971,6 +7376,7 @@ class Model {
|
|
|
6971
7376
|
static $appends;
|
|
6972
7377
|
static $morphClass;
|
|
6973
7378
|
_exists;
|
|
7379
|
+
originalAttributes;
|
|
6974
7380
|
loadedRelations = {};
|
|
6975
7381
|
hiddenOverrides = [];
|
|
6976
7382
|
visibleOverrides = [];
|
|
@@ -6980,6 +7386,24 @@ class Model {
|
|
|
6980
7386
|
this.repository = repository;
|
|
6981
7387
|
this._exists = exists;
|
|
6982
7388
|
this.attributes = modelStatics(this.constructor).hydrateAttributes(attributes);
|
|
7389
|
+
this.originalAttributes = snapshotAttributes(this.attributes);
|
|
7390
|
+
}
|
|
7391
|
+
acceptPersistedAttributes(record) {
|
|
7392
|
+
const original = this.originalAttributes;
|
|
7393
|
+
const beforeWrite = snapshotAttributes(this.attributes);
|
|
7394
|
+
const existed = this._exists;
|
|
7395
|
+
this.attributes = modelStatics(this.constructor).hydrateAttributes(record);
|
|
7396
|
+
this.originalAttributes = snapshotAttributes(this.attributes);
|
|
7397
|
+
const written = this.originalAttributes;
|
|
7398
|
+
this._exists = true;
|
|
7399
|
+
onDeferredModelRollback(() => {
|
|
7400
|
+
this.attributes = {
|
|
7401
|
+
...beforeWrite,
|
|
7402
|
+
...changedAttributes(this.attributes, written)
|
|
7403
|
+
};
|
|
7404
|
+
this.originalAttributes = original;
|
|
7405
|
+
this._exists = existed;
|
|
7406
|
+
});
|
|
6983
7407
|
}
|
|
6984
7408
|
getRepository() {
|
|
6985
7409
|
return this.repository;
|
|
@@ -7072,21 +7496,30 @@ class Model {
|
|
|
7072
7496
|
]);
|
|
7073
7497
|
}
|
|
7074
7498
|
static repository() {
|
|
7075
|
-
|
|
7499
|
+
const repository = resolveModelRepository(this);
|
|
7500
|
+
ensureBooted(this);
|
|
7501
|
+
return repository;
|
|
7502
|
+
}
|
|
7503
|
+
static scopeQuery(query) {
|
|
7504
|
+
ensureBooted(this);
|
|
7505
|
+
let scoped = query;
|
|
7506
|
+
for (const scope of getGlobalScopes(this))
|
|
7507
|
+
scoped = scope(scoped).protectWhere();
|
|
7508
|
+
return scoped;
|
|
7076
7509
|
}
|
|
7077
7510
|
static query() {
|
|
7078
7511
|
ensureBooted(this);
|
|
7079
7512
|
const repository = resolveModelRepository(this);
|
|
7080
|
-
|
|
7081
|
-
|
|
7082
|
-
query = scope(query);
|
|
7083
|
-
}
|
|
7084
|
-
return new ModelQuery(this, query);
|
|
7513
|
+
const scopeQuery = Model.scopeQuery;
|
|
7514
|
+
return new ModelQuery(this, scopeQuery.call(this, repository.query()));
|
|
7085
7515
|
}
|
|
7086
7516
|
static newFromRecord(record, exists = true) {
|
|
7087
7517
|
const repository = resolveModelRepository(this);
|
|
7088
7518
|
return modelStatics(this).fromRecord(record, repository, exists);
|
|
7089
7519
|
}
|
|
7520
|
+
static newFromTrustedRecord(record, exists = true) {
|
|
7521
|
+
return modelStatics(this).newFromRecord(record, exists);
|
|
7522
|
+
}
|
|
7090
7523
|
static async create(attributes, forced = {}) {
|
|
7091
7524
|
const statics = modelStatics(this);
|
|
7092
7525
|
ensureBooted(this);
|
|
@@ -7124,14 +7557,14 @@ class Model {
|
|
|
7124
7557
|
static async chunk(count, callback) {
|
|
7125
7558
|
const statics = modelStatics(this);
|
|
7126
7559
|
ensureBooted(this);
|
|
7127
|
-
await
|
|
7560
|
+
await Model.query.call(this).query.chunk(count, async (rows) => {
|
|
7128
7561
|
return await callback(rows.map((row) => statics.newFromRecord(row, true)));
|
|
7129
7562
|
});
|
|
7130
7563
|
}
|
|
7131
7564
|
static async cursorPaginate(options) {
|
|
7132
7565
|
const statics = modelStatics(this);
|
|
7133
7566
|
ensureBooted(this);
|
|
7134
|
-
const page = await
|
|
7567
|
+
const page = await Model.query.call(this).query.cursorPaginate({
|
|
7135
7568
|
perPage: options.perPage,
|
|
7136
7569
|
cursor: options.cursor
|
|
7137
7570
|
});
|
|
@@ -7239,9 +7672,14 @@ class Model {
|
|
|
7239
7672
|
return this;
|
|
7240
7673
|
}
|
|
7241
7674
|
if (updating) {
|
|
7242
|
-
const
|
|
7675
|
+
const dirty = changedAttributes(this.attributes, this.originalAttributes);
|
|
7676
|
+
if (Object.keys(dirty).length === 0) {
|
|
7677
|
+
await runObservers(this, "saved");
|
|
7678
|
+
return this;
|
|
7679
|
+
}
|
|
7680
|
+
const changes = applyTimestampsOnUpdate(table.columns, await applyCasts(dirty, casts, "dehydrate"), timestamps);
|
|
7243
7681
|
const record = await this.repository.updateByIdOrThrow(this.id, changes);
|
|
7244
|
-
this.
|
|
7682
|
+
this.acceptPersistedAttributes(record);
|
|
7245
7683
|
await runObservers(this, "updated");
|
|
7246
7684
|
await runObservers(this, "saved");
|
|
7247
7685
|
return this;
|
|
@@ -7250,8 +7688,7 @@ class Model {
|
|
|
7250
7688
|
const withTimestamps = applyTimestampsOnCreate(table.columns, assignable, timestamps);
|
|
7251
7689
|
const payload = await ModelClass.dehydrateAttributes(withTimestamps);
|
|
7252
7690
|
const record = await this.repository.create(payload);
|
|
7253
|
-
this.
|
|
7254
|
-
this._exists = true;
|
|
7691
|
+
this.acceptPersistedAttributes(record);
|
|
7255
7692
|
await runObservers(this, "created");
|
|
7256
7693
|
await runObservers(this, "saved");
|
|
7257
7694
|
return this;
|
|
@@ -7276,12 +7713,11 @@ class Model {
|
|
|
7276
7713
|
return await this.repository.forceDeleteById(this.id);
|
|
7277
7714
|
}
|
|
7278
7715
|
async restore() {
|
|
7279
|
-
const ModelClass = modelStatics(this.constructor);
|
|
7280
7716
|
const record = await this.repository.restoreById(this.id);
|
|
7281
7717
|
if (!record) {
|
|
7282
7718
|
return null;
|
|
7283
7719
|
}
|
|
7284
|
-
this.
|
|
7720
|
+
this.acceptPersistedAttributes(record);
|
|
7285
7721
|
return this;
|
|
7286
7722
|
}
|
|
7287
7723
|
async loadHasMany(as, relation, childRepository, options = {}) {
|
|
@@ -7436,14 +7872,7 @@ class Model {
|
|
|
7436
7872
|
}
|
|
7437
7873
|
function registerModelRepository(model, repository) {
|
|
7438
7874
|
modelRepositories.set(model, repository);
|
|
7439
|
-
|
|
7440
|
-
if (name) {
|
|
7441
|
-
namedModels.set(name, model);
|
|
7442
|
-
}
|
|
7443
|
-
const morphClass = model.$morphClass;
|
|
7444
|
-
if (morphClass) {
|
|
7445
|
-
namedModels.set(morphClass, model);
|
|
7446
|
-
}
|
|
7875
|
+
nameModel(model);
|
|
7447
7876
|
ensureBooted(model);
|
|
7448
7877
|
return model;
|
|
7449
7878
|
}
|
|
@@ -9168,29 +9597,95 @@ function storageFacade() {
|
|
|
9168
9597
|
return storage();
|
|
9169
9598
|
}
|
|
9170
9599
|
// ../../src/core/http/bodySizeLimitMiddleware.ts
|
|
9600
|
+
var guards = new WeakMap;
|
|
9601
|
+
function guardBody(request, maxBytes) {
|
|
9602
|
+
const existing = guards.get(request);
|
|
9603
|
+
if (existing && existing.maxBytes <= maxBytes)
|
|
9604
|
+
return existing;
|
|
9605
|
+
const state = { maxBytes, exceeded: false };
|
|
9606
|
+
const body = request.body;
|
|
9607
|
+
if (!body)
|
|
9608
|
+
return state;
|
|
9609
|
+
let consumed = 0;
|
|
9610
|
+
const stream = body.pipeThrough(new TransformStream({
|
|
9611
|
+
transform(chunk, controller) {
|
|
9612
|
+
const bytes = typeof chunk === "string" ? new TextEncoder().encode(chunk) : chunk;
|
|
9613
|
+
consumed += bytes.byteLength;
|
|
9614
|
+
if (consumed > maxBytes) {
|
|
9615
|
+
state.exceeded = true;
|
|
9616
|
+
throw new PayloadTooLargeError(`Request body exceeds the ${maxBytes} byte limit.`);
|
|
9617
|
+
}
|
|
9618
|
+
controller.enqueue(bytes);
|
|
9619
|
+
}
|
|
9620
|
+
}));
|
|
9621
|
+
const bounded = new Request(request, { body: stream });
|
|
9622
|
+
const decorateClone = (clone) => {
|
|
9623
|
+
for (const field of ["params", "cookies"]) {
|
|
9624
|
+
if (field in request)
|
|
9625
|
+
Object.defineProperty(clone, field, { value: Reflect.get(request, field) });
|
|
9626
|
+
}
|
|
9627
|
+
const nativeClone = () => Reflect.apply(Request.prototype.clone, clone, []);
|
|
9628
|
+
Object.defineProperty(clone, "clone", {
|
|
9629
|
+
value: () => decorateClone(nativeClone())
|
|
9630
|
+
});
|
|
9631
|
+
guards.set(clone, state);
|
|
9632
|
+
return clone;
|
|
9633
|
+
};
|
|
9634
|
+
const descriptors = {
|
|
9635
|
+
body: { configurable: true, get: () => bounded.body },
|
|
9636
|
+
bodyUsed: { configurable: true, get: () => bounded.bodyUsed },
|
|
9637
|
+
clone: {
|
|
9638
|
+
configurable: true,
|
|
9639
|
+
value: () => decorateClone(Reflect.apply(Request.prototype.clone, bounded, []))
|
|
9640
|
+
}
|
|
9641
|
+
};
|
|
9642
|
+
for (const method of [
|
|
9643
|
+
"text",
|
|
9644
|
+
"json",
|
|
9645
|
+
"formData",
|
|
9646
|
+
"arrayBuffer",
|
|
9647
|
+
"blob",
|
|
9648
|
+
"bytes",
|
|
9649
|
+
"textStream"
|
|
9650
|
+
]) {
|
|
9651
|
+
descriptors[method] = { configurable: true, value: bounded[method].bind(bounded) };
|
|
9652
|
+
}
|
|
9653
|
+
Object.defineProperties(request, descriptors);
|
|
9654
|
+
guards.set(request, state);
|
|
9655
|
+
return state;
|
|
9656
|
+
}
|
|
9171
9657
|
var DEFAULT_MAX_BODY_BYTES = 1048576;
|
|
9172
9658
|
function resolveMaxBodyBytes() {
|
|
9173
9659
|
const raw = process.env.MAX_REQUEST_BODY_BYTES?.trim();
|
|
9174
9660
|
if (!raw) {
|
|
9175
9661
|
return DEFAULT_MAX_BODY_BYTES;
|
|
9176
9662
|
}
|
|
9177
|
-
const parsed = Number
|
|
9178
|
-
if (!Number.
|
|
9663
|
+
const parsed = Number(raw);
|
|
9664
|
+
if (!Number.isSafeInteger(parsed) || parsed <= 0) {
|
|
9179
9665
|
return DEFAULT_MAX_BODY_BYTES;
|
|
9180
9666
|
}
|
|
9181
9667
|
return parsed;
|
|
9182
9668
|
}
|
|
9183
9669
|
function createBodySizeLimitMiddleware(maxBytes = resolveMaxBodyBytes()) {
|
|
9670
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0)
|
|
9671
|
+
throw new RangeError("Body limit must be a positive safe integer.");
|
|
9184
9672
|
return async (request, next) => {
|
|
9185
|
-
const
|
|
9186
|
-
|
|
9187
|
-
|
|
9188
|
-
|
|
9189
|
-
|
|
9190
|
-
|
|
9191
|
-
|
|
9673
|
+
const tooLarge = () => {
|
|
9674
|
+
const error = new PayloadTooLargeError(`Request body exceeds the ${maxBytes} byte limit.`);
|
|
9675
|
+
return Response.json({ error: error.message }, { status: error.status });
|
|
9676
|
+
};
|
|
9677
|
+
const length = Number(request.headers.get("content-length"));
|
|
9678
|
+
if (Number.isFinite(length) && length > maxBytes)
|
|
9679
|
+
return tooLarge();
|
|
9680
|
+
const state = guardBody(request, maxBytes);
|
|
9681
|
+
try {
|
|
9682
|
+
const response = await next();
|
|
9683
|
+
return state.exceeded ? tooLarge() : response;
|
|
9684
|
+
} catch (error) {
|
|
9685
|
+
if (state.exceeded || error instanceof PayloadTooLargeError)
|
|
9686
|
+
return tooLarge();
|
|
9687
|
+
throw error;
|
|
9192
9688
|
}
|
|
9193
|
-
return await next();
|
|
9194
9689
|
};
|
|
9195
9690
|
}
|
|
9196
9691
|
// ../../src/core/http/requestMetaContext.ts
|
|
@@ -10718,6 +11213,67 @@ function securedBindRouteModelByKey(param, resolver, authorization, handler) {
|
|
|
10718
11213
|
return response;
|
|
10719
11214
|
};
|
|
10720
11215
|
}
|
|
11216
|
+
// ../../src/core/runtime/boundedThrottleStore.ts
|
|
11217
|
+
class BoundedThrottleStore {
|
|
11218
|
+
decayMs;
|
|
11219
|
+
now;
|
|
11220
|
+
buckets = new Map;
|
|
11221
|
+
maxBuckets;
|
|
11222
|
+
pruneBatchSize;
|
|
11223
|
+
disposed = false;
|
|
11224
|
+
pruned = 0;
|
|
11225
|
+
constructor(decayMs, options = {}, now = () => performance.now()) {
|
|
11226
|
+
this.decayMs = decayMs;
|
|
11227
|
+
this.now = now;
|
|
11228
|
+
this.maxBuckets = options.maxBuckets ?? 1e4;
|
|
11229
|
+
this.pruneBatchSize = options.pruneBatchSize ?? 32;
|
|
11230
|
+
if (!Number.isFinite(decayMs) || decayMs < 0 || decayMs > 2147483647) {
|
|
11231
|
+
throw new Error("Invalid memory throttle window.");
|
|
11232
|
+
}
|
|
11233
|
+
if (!Number.isSafeInteger(this.maxBuckets) || this.maxBuckets < 1 || this.maxBuckets > 1e6 || !Number.isSafeInteger(this.pruneBatchSize) || this.pruneBatchSize < 1 || this.pruneBatchSize > 1024) {
|
|
11234
|
+
throw new Error("Invalid memory throttle storage bounds.");
|
|
11235
|
+
}
|
|
11236
|
+
}
|
|
11237
|
+
consume(key) {
|
|
11238
|
+
this.pruned = 0;
|
|
11239
|
+
if (this.disposed)
|
|
11240
|
+
return null;
|
|
11241
|
+
const now = this.now();
|
|
11242
|
+
const entries = this.buckets.entries();
|
|
11243
|
+
for (let i = 0;i < this.pruneBatchSize; i++) {
|
|
11244
|
+
const oldest = entries.next().value;
|
|
11245
|
+
if (!oldest || oldest[1].resetAt > now)
|
|
11246
|
+
break;
|
|
11247
|
+
this.buckets.delete(oldest[0]);
|
|
11248
|
+
this.pruned++;
|
|
11249
|
+
}
|
|
11250
|
+
const existing = this.buckets.get(key);
|
|
11251
|
+
if (existing && existing.resetAt > now) {
|
|
11252
|
+
existing.count = Math.min(Number.MAX_SAFE_INTEGER, existing.count + 1);
|
|
11253
|
+
return existing.count;
|
|
11254
|
+
}
|
|
11255
|
+
this.buckets.delete(key);
|
|
11256
|
+
if (this.buckets.size >= this.maxBuckets)
|
|
11257
|
+
return null;
|
|
11258
|
+
this.buckets.set(key, { count: 1, resetAt: now + this.decayMs });
|
|
11259
|
+
return 1;
|
|
11260
|
+
}
|
|
11261
|
+
clear() {
|
|
11262
|
+
this.buckets.clear();
|
|
11263
|
+
}
|
|
11264
|
+
dispose() {
|
|
11265
|
+
this.disposed = true;
|
|
11266
|
+
this.clear();
|
|
11267
|
+
}
|
|
11268
|
+
stats() {
|
|
11269
|
+
return {
|
|
11270
|
+
retainedBuckets: this.buckets.size,
|
|
11271
|
+
prunedOnLastConsume: this.pruned,
|
|
11272
|
+
disposed: this.disposed
|
|
11273
|
+
};
|
|
11274
|
+
}
|
|
11275
|
+
}
|
|
11276
|
+
|
|
10721
11277
|
// ../../src/core/http/throttleMiddleware.ts
|
|
10722
11278
|
import { createHash as createHash3 } from "crypto";
|
|
10723
11279
|
init_tenantContext();
|
|
@@ -10762,28 +11318,32 @@ if ttl < 0 then
|
|
|
10762
11318
|
end
|
|
10763
11319
|
return count
|
|
10764
11320
|
`;
|
|
10765
|
-
function redisThrottleKey(request, prefix, identity) {
|
|
11321
|
+
function redisThrottleKey(request, prefix, identity, tenantId = currentTenant()?.id ?? null) {
|
|
10766
11322
|
const bucket = JSON.stringify([
|
|
10767
|
-
|
|
11323
|
+
tenantId,
|
|
10768
11324
|
request.method,
|
|
10769
11325
|
currentRequestMeta().routeTemplate ?? "__unmatched__",
|
|
10770
11326
|
identity
|
|
10771
11327
|
]);
|
|
10772
11328
|
return namespacedRedisKey(`${prefix}:${createHash3("sha256").update(bucket).digest("hex")}`);
|
|
10773
11329
|
}
|
|
10774
|
-
async function consumeRedisThrottle(client, key, decaySeconds, timeoutMs = 1000) {
|
|
10775
|
-
|
|
11330
|
+
async function consumeRedisThrottle(client, key, decaySeconds, timeoutMs = 1000, signal) {
|
|
11331
|
+
const expiryMs = Math.max(1, Math.ceil(decaySeconds * 1000));
|
|
11332
|
+
if (!Number.isFinite(decaySeconds) || decaySeconds < 0 || !Number.isSafeInteger(expiryMs) || !Number.isSafeInteger(timeoutMs) || timeoutMs < 1) {
|
|
10776
11333
|
throw new Error("Invalid throttle expiry or command timeout.");
|
|
10777
11334
|
}
|
|
11335
|
+
if (signal?.aborted)
|
|
11336
|
+
throw new Error("Throttle consumer is disposed.");
|
|
11337
|
+
let onAbort;
|
|
10778
11338
|
let timer;
|
|
10779
11339
|
try {
|
|
11340
|
+
const cancelled = new Promise((_resolve, reject) => {
|
|
11341
|
+
onAbort = () => reject(new Error("Throttle consumer is disposed."));
|
|
11342
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
11343
|
+
});
|
|
10780
11344
|
const result = await Promise.race([
|
|
10781
|
-
|
|
10782
|
-
|
|
10783
|
-
"1",
|
|
10784
|
-
key,
|
|
10785
|
-
String(Math.max(1, Math.ceil(decaySeconds * 1000)))
|
|
10786
|
-
]),
|
|
11345
|
+
cancelled,
|
|
11346
|
+
client.send("EVAL", [CONSUME_THROTTLE, "1", key, String(expiryMs)]),
|
|
10787
11347
|
new Promise((_resolve, reject) => {
|
|
10788
11348
|
timer = setTimeout(() => reject(new Error("Throttle store deadline exceeded.")), timeoutMs);
|
|
10789
11349
|
})
|
|
@@ -10794,11 +11354,16 @@ async function consumeRedisThrottle(client, key, decaySeconds, timeoutMs = 1000)
|
|
|
10794
11354
|
return count;
|
|
10795
11355
|
} finally {
|
|
10796
11356
|
clearTimeout(timer);
|
|
11357
|
+
if (onAbort)
|
|
11358
|
+
signal?.removeEventListener("abort", onAbort);
|
|
10797
11359
|
}
|
|
10798
11360
|
}
|
|
10799
11361
|
function createRedisThrottleConsumer(options) {
|
|
10800
11362
|
let ownedClient;
|
|
10801
|
-
|
|
11363
|
+
const controller = new AbortController;
|
|
11364
|
+
const consume = async (key, decaySeconds) => {
|
|
11365
|
+
if (controller.signal.aborted)
|
|
11366
|
+
throw new Error("Throttle consumer is disposed.");
|
|
10802
11367
|
if (!options.redisClient && !ownedClient) {
|
|
10803
11368
|
ownedClient = new RedisClient2(options.redisUrl, { autoReconnect: false, maxRetries: 0 });
|
|
10804
11369
|
}
|
|
@@ -10806,7 +11371,10 @@ function createRedisThrottleConsumer(options) {
|
|
|
10806
11371
|
if (!client)
|
|
10807
11372
|
throw new Error("Throttle client is required.");
|
|
10808
11373
|
try {
|
|
10809
|
-
|
|
11374
|
+
const attempts = await consumeRedisThrottle(client, key, decaySeconds, options.commandTimeoutMs, controller.signal);
|
|
11375
|
+
if (controller.signal.aborted)
|
|
11376
|
+
throw new Error("Throttle consumer is disposed.");
|
|
11377
|
+
return attempts;
|
|
10810
11378
|
} catch (error) {
|
|
10811
11379
|
if (client === ownedClient) {
|
|
10812
11380
|
ownedClient.close();
|
|
@@ -10815,6 +11383,17 @@ function createRedisThrottleConsumer(options) {
|
|
|
10815
11383
|
throw error;
|
|
10816
11384
|
}
|
|
10817
11385
|
};
|
|
11386
|
+
return Object.assign(consume, {
|
|
11387
|
+
isDisposed: () => controller.signal.aborted,
|
|
11388
|
+
dispose() {
|
|
11389
|
+
if (controller.signal.aborted)
|
|
11390
|
+
return;
|
|
11391
|
+
controller.abort();
|
|
11392
|
+
const client = ownedClient;
|
|
11393
|
+
ownedClient = undefined;
|
|
11394
|
+
client?.close();
|
|
11395
|
+
}
|
|
11396
|
+
});
|
|
10818
11397
|
}
|
|
10819
11398
|
function throttleUnavailableResponse() {
|
|
10820
11399
|
return Response.json({ error: "Rate limiting is temporarily unavailable." }, {
|
|
@@ -10822,27 +11401,52 @@ function throttleUnavailableResponse() {
|
|
|
10822
11401
|
headers: { "retry-after": "1" }
|
|
10823
11402
|
});
|
|
10824
11403
|
}
|
|
11404
|
+
function resolveThrottleQuota(request, options, identity = resolveThrottleIdentity(request)) {
|
|
11405
|
+
const limit = options.quotaPolicy ? options.quotaPolicy({
|
|
11406
|
+
request,
|
|
11407
|
+
tenant: currentTenant(),
|
|
11408
|
+
identity,
|
|
11409
|
+
routeTemplate: currentRequestMeta().routeTemplate ?? "__unmatched__",
|
|
11410
|
+
maxAttempts: options.maxAttempts
|
|
11411
|
+
}) : options.maxAttempts;
|
|
11412
|
+
if (!Number.isSafeInteger(limit) || limit < 0) {
|
|
11413
|
+
throw new Error("Invalid application quota.");
|
|
11414
|
+
}
|
|
11415
|
+
return limit;
|
|
11416
|
+
}
|
|
10825
11417
|
function createThrottleMiddleware(options) {
|
|
11418
|
+
if (!Number.isSafeInteger(options.maxAttempts) || options.maxAttempts < 0) {
|
|
11419
|
+
throw new Error("Throttle maxAttempts must be a non-negative safe integer.");
|
|
11420
|
+
}
|
|
10826
11421
|
const consume = createRedisThrottleConsumer(options);
|
|
10827
11422
|
const prefix = options.keyPrefix ?? "throttle";
|
|
10828
|
-
|
|
10829
|
-
const
|
|
11423
|
+
const middleware = async (request, next) => {
|
|
11424
|
+
const identity = resolveThrottleIdentity(request);
|
|
11425
|
+
let maxAttempts;
|
|
11426
|
+
try {
|
|
11427
|
+
maxAttempts = resolveThrottleQuota(request, options, identity);
|
|
11428
|
+
} catch {
|
|
11429
|
+
return throttleUnavailableResponse();
|
|
11430
|
+
}
|
|
11431
|
+
const key = redisThrottleKey(request, prefix, identity);
|
|
10830
11432
|
let attempts;
|
|
10831
11433
|
try {
|
|
10832
11434
|
attempts = await consume(key, options.decaySeconds);
|
|
10833
11435
|
} catch {
|
|
10834
11436
|
return throttleUnavailableResponse();
|
|
10835
11437
|
}
|
|
10836
|
-
|
|
11438
|
+
if (consume.isDisposed())
|
|
11439
|
+
return throttleUnavailableResponse();
|
|
10837
11440
|
if (attempts > maxAttempts) {
|
|
10838
11441
|
return await tooManyRequestsResponse(request, "Too many requests.", options.decaySeconds);
|
|
10839
11442
|
}
|
|
10840
11443
|
return await next();
|
|
10841
11444
|
};
|
|
11445
|
+
return Object.assign(middleware, { dispose: () => consume.dispose() });
|
|
10842
11446
|
}
|
|
10843
11447
|
|
|
10844
11448
|
// ../../src/core/http/loginThrottleMiddleware.ts
|
|
10845
|
-
var
|
|
11449
|
+
var resetGeneration = 0;
|
|
10846
11450
|
function resolveLoginIdentity(request) {
|
|
10847
11451
|
return readClientIp(request) ?? "unknown";
|
|
10848
11452
|
}
|
|
@@ -10860,33 +11464,31 @@ async function resolveLoginEmail(request) {
|
|
|
10860
11464
|
return "unknown";
|
|
10861
11465
|
}
|
|
10862
11466
|
}
|
|
10863
|
-
function consumeMemoryAttempt(key, decaySeconds) {
|
|
10864
|
-
const now = Date.now();
|
|
10865
|
-
const existing = memoryLoginBuckets.get(key);
|
|
10866
|
-
if (!existing || existing.resetAt <= now) {
|
|
10867
|
-
memoryLoginBuckets.set(key, { count: 1, resetAt: now + decaySeconds * 1000 });
|
|
10868
|
-
return 1;
|
|
10869
|
-
}
|
|
10870
|
-
existing.count += 1;
|
|
10871
|
-
return existing.count;
|
|
10872
|
-
}
|
|
10873
11467
|
function createMemoryLoginThrottleMiddleware(options) {
|
|
10874
|
-
const
|
|
10875
|
-
|
|
10876
|
-
|
|
11468
|
+
const store = new BoundedThrottleStore(options.decaySeconds * 1000, options);
|
|
11469
|
+
if (!Number.isSafeInteger(options.maxAttempts) || options.maxAttempts < 0)
|
|
11470
|
+
throw new Error("Invalid memory throttle limit.");
|
|
11471
|
+
let generation = resetGeneration;
|
|
11472
|
+
const middleware = async (request, next) => {
|
|
11473
|
+
if (generation !== resetGeneration) {
|
|
11474
|
+
store.clear();
|
|
11475
|
+
generation = resetGeneration;
|
|
11476
|
+
}
|
|
10877
11477
|
const email = await resolveLoginEmail(request);
|
|
10878
|
-
const
|
|
10879
|
-
const attempts =
|
|
10880
|
-
if (attempts
|
|
11478
|
+
const key = redisThrottleKey(request, options.keyPrefix ?? "login-throttle", JSON.stringify([resolveLoginIdentity(request), email]));
|
|
11479
|
+
const attempts = store.consume(key);
|
|
11480
|
+
if (attempts === null)
|
|
11481
|
+
return throttleUnavailableResponse();
|
|
11482
|
+
if (attempts > options.maxAttempts)
|
|
10881
11483
|
return await tooManyRequestsResponse(request, "Too many login attempts. Try again later.", options.decaySeconds);
|
|
10882
|
-
}
|
|
10883
11484
|
return await next();
|
|
10884
11485
|
};
|
|
11486
|
+
return Object.assign(middleware, { dispose: () => store.dispose(), stats: () => store.stats() });
|
|
10885
11487
|
}
|
|
10886
11488
|
function createRedisLoginThrottleMiddleware(options) {
|
|
10887
11489
|
const consume = createRedisThrottleConsumer(options);
|
|
10888
11490
|
const prefix = options.keyPrefix ?? "login-throttle";
|
|
10889
|
-
|
|
11491
|
+
const middleware = async (request, next) => {
|
|
10890
11492
|
const identity = resolveLoginIdentity(request);
|
|
10891
11493
|
const email = await resolveLoginEmail(request);
|
|
10892
11494
|
const throttleKey = redisThrottleKey(request, prefix, JSON.stringify([identity, email]));
|
|
@@ -10896,11 +11498,14 @@ function createRedisLoginThrottleMiddleware(options) {
|
|
|
10896
11498
|
} catch {
|
|
10897
11499
|
return throttleUnavailableResponse();
|
|
10898
11500
|
}
|
|
11501
|
+
if (consume.isDisposed())
|
|
11502
|
+
return throttleUnavailableResponse();
|
|
10899
11503
|
if (attempts > options.maxAttempts) {
|
|
10900
11504
|
return await tooManyRequestsResponse(request, "Too many login attempts. Try again later.", options.decaySeconds);
|
|
10901
11505
|
}
|
|
10902
11506
|
return await next();
|
|
10903
11507
|
};
|
|
11508
|
+
return Object.assign(middleware, { dispose: () => consume.dispose() });
|
|
10904
11509
|
}
|
|
10905
11510
|
function createLoginThrottleMiddleware(options) {
|
|
10906
11511
|
const redisUrl = options.redisUrl?.trim() ?? "";
|
|
@@ -10910,35 +11515,37 @@ function createLoginThrottleMiddleware(options) {
|
|
|
10910
11515
|
return createMemoryLoginThrottleMiddleware(options);
|
|
10911
11516
|
}
|
|
10912
11517
|
function resetMemoryLoginThrottleForTests() {
|
|
10913
|
-
|
|
11518
|
+
resetGeneration++;
|
|
10914
11519
|
}
|
|
10915
11520
|
// ../../src/core/http/memoryThrottleMiddleware.ts
|
|
10916
|
-
var
|
|
11521
|
+
var resetGeneration2 = 0;
|
|
10917
11522
|
function createMemoryThrottleMiddleware(options) {
|
|
10918
|
-
const
|
|
10919
|
-
|
|
10920
|
-
|
|
10921
|
-
|
|
10922
|
-
|
|
10923
|
-
|
|
10924
|
-
|
|
10925
|
-
|
|
10926
|
-
|
|
10927
|
-
|
|
10928
|
-
|
|
10929
|
-
|
|
11523
|
+
const store = new BoundedThrottleStore(options.decaySeconds * 1000, options);
|
|
11524
|
+
if (!Number.isSafeInteger(options.maxAttempts) || options.maxAttempts < 0)
|
|
11525
|
+
throw new Error("Invalid memory throttle limit.");
|
|
11526
|
+
let generation = resetGeneration2;
|
|
11527
|
+
const middleware = async (request, next) => {
|
|
11528
|
+
if (generation !== resetGeneration2) {
|
|
11529
|
+
store.clear();
|
|
11530
|
+
generation = resetGeneration2;
|
|
11531
|
+
}
|
|
11532
|
+
let maxAttempts;
|
|
11533
|
+
try {
|
|
11534
|
+
maxAttempts = resolveThrottleQuota(request, options);
|
|
11535
|
+
} catch {
|
|
11536
|
+
return throttleUnavailableResponse();
|
|
10930
11537
|
}
|
|
10931
|
-
|
|
10932
|
-
if (
|
|
11538
|
+
const attempts = store.consume(redisThrottleKey(request, options.keyPrefix ?? "memory-throttle", resolveThrottleIdentity(request)));
|
|
11539
|
+
if (attempts === null)
|
|
11540
|
+
return throttleUnavailableResponse();
|
|
11541
|
+
if (attempts > maxAttempts)
|
|
10933
11542
|
return await tooManyRequestsResponse(request, "Too many requests.", options.decaySeconds);
|
|
10934
|
-
}
|
|
10935
11543
|
return await next();
|
|
10936
11544
|
};
|
|
11545
|
+
return Object.assign(middleware, { dispose: () => store.dispose(), stats: () => store.stats() });
|
|
10937
11546
|
}
|
|
10938
11547
|
function resetMemoryThrottleForTests() {
|
|
10939
|
-
|
|
10940
|
-
buckets.clear();
|
|
10941
|
-
}
|
|
11548
|
+
resetGeneration2++;
|
|
10942
11549
|
}
|
|
10943
11550
|
// ../../src/core/metrics/prometheus.ts
|
|
10944
11551
|
var HTTP_DURATION_BUCKETS_MS = [5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 1e4];
|
|
@@ -11208,8 +11815,6 @@ function createRequireWebAuthMiddleware(auth) {
|
|
|
11208
11815
|
}
|
|
11209
11816
|
// ../../src/core/http/scimThrottleMiddleware.ts
|
|
11210
11817
|
import { createHash as createHash4 } from "crypto";
|
|
11211
|
-
var {RedisClient: RedisClient3 } = globalThis.Bun;
|
|
11212
|
-
var memoryBuckets = new Map;
|
|
11213
11818
|
function resolveScimIdentity(request) {
|
|
11214
11819
|
const authorization = request.headers.get("authorization") ?? "";
|
|
11215
11820
|
const presented = authorization.startsWith("Bearer ") ? authorization.slice("Bearer ".length).trim() : "";
|
|
@@ -11218,40 +11823,44 @@ function resolveScimIdentity(request) {
|
|
|
11218
11823
|
}
|
|
11219
11824
|
return readClientIp(request) ?? "unknown";
|
|
11220
11825
|
}
|
|
11221
|
-
function throttleFromMemory(key, maxAttempts, decaySeconds) {
|
|
11222
|
-
const now = Date.now();
|
|
11223
|
-
const existing = memoryBuckets.get(key);
|
|
11224
|
-
if (!existing || existing.resetAt <= now) {
|
|
11225
|
-
memoryBuckets.set(key, { count: 1, resetAt: now + decaySeconds * 1000 });
|
|
11226
|
-
return null;
|
|
11227
|
-
}
|
|
11228
|
-
existing.count += 1;
|
|
11229
|
-
if (existing.count > maxAttempts) {
|
|
11230
|
-
return Response.json({ error: "Too many SCIM requests." }, { status: 429, headers: { "retry-after": String(decaySeconds) } });
|
|
11231
|
-
}
|
|
11232
|
-
return null;
|
|
11233
|
-
}
|
|
11234
11826
|
function createScimThrottleMiddleware(options) {
|
|
11235
|
-
|
|
11236
|
-
|
|
11827
|
+
if (!Number.isSafeInteger(options.maxAttempts) || options.maxAttempts < 0)
|
|
11828
|
+
throw new Error("Invalid SCIM throttle limit.");
|
|
11829
|
+
const redisUrl = options.redisUrl?.trim() ?? "";
|
|
11830
|
+
const distributed = Boolean(redisUrl || options.redisClient);
|
|
11831
|
+
const store = distributed ? null : new BoundedThrottleStore(options.decaySeconds * 1000, options);
|
|
11832
|
+
const consume = distributed ? createRedisThrottleConsumer({ ...options, redisUrl }) : null;
|
|
11833
|
+
let disposed = false;
|
|
11834
|
+
const middleware = async (request, next) => {
|
|
11835
|
+
if (disposed)
|
|
11836
|
+
return throttleUnavailableResponse();
|
|
11237
11837
|
const identity = resolveScimIdentity(request);
|
|
11238
|
-
const
|
|
11239
|
-
|
|
11240
|
-
|
|
11241
|
-
|
|
11242
|
-
|
|
11243
|
-
|
|
11244
|
-
|
|
11245
|
-
return Response.json({ error: "Too many SCIM requests." }, { status: 429, headers: { "retry-after": String(options.decaySeconds) } });
|
|
11246
|
-
}
|
|
11247
|
-
return await next();
|
|
11248
|
-
}
|
|
11249
|
-
const limited = throttleFromMemory(key, options.maxAttempts, options.decaySeconds);
|
|
11250
|
-
if (limited) {
|
|
11251
|
-
return limited;
|
|
11838
|
+
const token = request.headers.get("authorization")?.startsWith("Bearer ") ? request.headers.get("authorization")?.slice("Bearer ".length).trim() ?? "" : "";
|
|
11839
|
+
const key = redisThrottleKey(request, options.keyPrefix ?? "scim-throttle:v2", identity, resolveScimTenantFromToken(token) ?? undefined);
|
|
11840
|
+
let attempts;
|
|
11841
|
+
try {
|
|
11842
|
+
attempts = consume ? await consume(key, options.decaySeconds) : store?.consume(key);
|
|
11843
|
+
} catch {
|
|
11844
|
+
return throttleUnavailableResponse();
|
|
11252
11845
|
}
|
|
11846
|
+
if (disposed)
|
|
11847
|
+
return throttleUnavailableResponse();
|
|
11848
|
+
if (attempts === null || attempts === undefined)
|
|
11849
|
+
return throttleUnavailableResponse();
|
|
11850
|
+
if (attempts > options.maxAttempts)
|
|
11851
|
+
return Response.json({ error: "Too many SCIM requests." }, { status: 429, headers: { "retry-after": String(options.decaySeconds) } });
|
|
11253
11852
|
return await next();
|
|
11254
11853
|
};
|
|
11854
|
+
return Object.assign(middleware, {
|
|
11855
|
+
dispose: () => {
|
|
11856
|
+
if (disposed)
|
|
11857
|
+
return;
|
|
11858
|
+
disposed = true;
|
|
11859
|
+
store?.dispose();
|
|
11860
|
+
consume?.dispose();
|
|
11861
|
+
},
|
|
11862
|
+
stats: () => store?.stats()
|
|
11863
|
+
});
|
|
11255
11864
|
}
|
|
11256
11865
|
// ../../src/core/http/securityHeadersMiddleware.ts
|
|
11257
11866
|
function createSecurityHeadersMiddleware(options = {}) {
|
|
@@ -11419,47 +12028,150 @@ class WebFormRequest {
|
|
|
11419
12028
|
}
|
|
11420
12029
|
}
|
|
11421
12030
|
// ../../src/core/lifecycle/gracefulShutdown.ts
|
|
11422
|
-
|
|
11423
|
-
|
|
11424
|
-
|
|
11425
|
-
|
|
11426
|
-
|
|
11427
|
-
return
|
|
11428
|
-
shutdownHandlers.delete(name);
|
|
11429
|
-
};
|
|
12031
|
+
function resolveShutdownTimeoutMs() {
|
|
12032
|
+
const configured = process.env.SHUTDOWN_TIMEOUT_MS;
|
|
12033
|
+
const value = configured === undefined ? 30000 : Number(configured);
|
|
12034
|
+
if (!Number.isSafeInteger(value) || value <= 0 || value > 2147483647)
|
|
12035
|
+
throw new RangeError("SHUTDOWN_TIMEOUT_MS must be an integer between 1 and 2147483647.");
|
|
12036
|
+
return value;
|
|
11430
12037
|
}
|
|
11431
|
-
|
|
11432
|
-
|
|
11433
|
-
|
|
12038
|
+
|
|
12039
|
+
class LifecycleCoordinator {
|
|
12040
|
+
handlers = new Map;
|
|
12041
|
+
running;
|
|
12042
|
+
timeoutMs;
|
|
12043
|
+
constructor(options = {}) {
|
|
12044
|
+
this.timeoutMs = options.timeoutMs ?? resolveShutdownTimeoutMs();
|
|
12045
|
+
if (!Number.isSafeInteger(this.timeoutMs) || this.timeoutMs <= 0 || this.timeoutMs > 2147483647)
|
|
12046
|
+
throw new RangeError("Shutdown timeout must be an integer between 1 and 2147483647.");
|
|
12047
|
+
}
|
|
12048
|
+
get isShuttingDown() {
|
|
12049
|
+
return this.running !== undefined;
|
|
12050
|
+
}
|
|
12051
|
+
register(name, handler, phase = "close") {
|
|
12052
|
+
if (this.isShuttingDown)
|
|
12053
|
+
throw new Error("Cannot register resources during shutdown.");
|
|
12054
|
+
if (!["stop", "drain", "flush", "close", "force"].includes(phase))
|
|
12055
|
+
throw new TypeError("Unknown shutdown phase.");
|
|
12056
|
+
const registration = { name, handler, phase };
|
|
12057
|
+
this.handlers.set(name, registration);
|
|
12058
|
+
return () => {
|
|
12059
|
+
if (this.handlers.get(name) === registration)
|
|
12060
|
+
this.handlers.delete(name);
|
|
12061
|
+
};
|
|
11434
12062
|
}
|
|
11435
|
-
|
|
11436
|
-
|
|
11437
|
-
|
|
12063
|
+
shutdown(reason = "MANUAL") {
|
|
12064
|
+
if (this.running)
|
|
12065
|
+
return this.running;
|
|
12066
|
+
this.running = Promise.resolve().then(() => this.execute(reason));
|
|
12067
|
+
return this.running;
|
|
12068
|
+
}
|
|
12069
|
+
async execute(reason) {
|
|
12070
|
+
const registrations = [...this.handlers.values()];
|
|
12071
|
+
const controller = new AbortController;
|
|
12072
|
+
const context = { signal: controller.signal, reason };
|
|
12073
|
+
const errors = [];
|
|
12074
|
+
let timedOut = false;
|
|
12075
|
+
let forced = false;
|
|
12076
|
+
const record = (entry, error) => {
|
|
12077
|
+
errors.push({ name: entry.name, phase: entry.phase, error });
|
|
12078
|
+
console.error(`[shutdown] Failed ${entry.phase}:${entry.name}:`, error);
|
|
12079
|
+
};
|
|
12080
|
+
const force = () => {
|
|
12081
|
+
if (forced)
|
|
12082
|
+
return;
|
|
12083
|
+
forced = true;
|
|
12084
|
+
for (const entry of registrations.filter((entry) => entry.phase === "force")) {
|
|
12085
|
+
try {
|
|
12086
|
+
Promise.resolve(entry.handler(context)).catch((error) => record(entry, error));
|
|
12087
|
+
} catch (error) {
|
|
12088
|
+
record(entry, error);
|
|
12089
|
+
}
|
|
12090
|
+
}
|
|
12091
|
+
};
|
|
12092
|
+
let timer;
|
|
12093
|
+
const deadline = new Promise((resolve) => {
|
|
12094
|
+
timer = setTimeout(() => {
|
|
12095
|
+
timedOut = true;
|
|
12096
|
+
const error = new Error(`Shutdown deadline exceeded after ${this.timeoutMs}ms.`);
|
|
12097
|
+
errors.push({ name: "deadline", phase: "force", error });
|
|
12098
|
+
controller.abort(error);
|
|
12099
|
+
force();
|
|
12100
|
+
resolve();
|
|
12101
|
+
}, this.timeoutMs);
|
|
12102
|
+
});
|
|
12103
|
+
const sequence = (async () => {
|
|
12104
|
+
let unsafeToClose = false;
|
|
12105
|
+
for (const phase of ["stop", "drain", "flush", "close"]) {
|
|
12106
|
+
if (controller.signal.aborted)
|
|
12107
|
+
return;
|
|
12108
|
+
if (unsafeToClose && (phase === "flush" || phase === "close")) {
|
|
12109
|
+
controller.abort(new Error("Admission or drain failed; infrastructure remains open."));
|
|
12110
|
+
force();
|
|
12111
|
+
return;
|
|
12112
|
+
}
|
|
12113
|
+
for (const entry of registrations.filter((entry) => entry.phase === phase)) {
|
|
12114
|
+
if (controller.signal.aborted)
|
|
12115
|
+
return;
|
|
12116
|
+
try {
|
|
12117
|
+
await entry.handler(context);
|
|
12118
|
+
} catch (error) {
|
|
12119
|
+
record(entry, error);
|
|
12120
|
+
if (phase === "stop" || phase === "drain")
|
|
12121
|
+
unsafeToClose = true;
|
|
12122
|
+
}
|
|
12123
|
+
}
|
|
12124
|
+
}
|
|
12125
|
+
})();
|
|
11438
12126
|
try {
|
|
11439
|
-
await
|
|
11440
|
-
|
|
11441
|
-
|
|
11442
|
-
|
|
12127
|
+
await Promise.race([sequence, deadline]);
|
|
12128
|
+
} finally {
|
|
12129
|
+
clearTimeout(timer);
|
|
12130
|
+
this.handlers.clear();
|
|
11443
12131
|
}
|
|
12132
|
+
return { successful: errors.length === 0 && !timedOut, timedOut, errors: [...errors] };
|
|
11444
12133
|
}
|
|
11445
12134
|
}
|
|
11446
|
-
|
|
11447
|
-
|
|
11448
|
-
|
|
11449
|
-
|
|
11450
|
-
|
|
11451
|
-
|
|
11452
|
-
|
|
11453
|
-
|
|
11454
|
-
|
|
11455
|
-
|
|
12135
|
+
var defaultLifecycle = new LifecycleCoordinator;
|
|
12136
|
+
var installations = new Map;
|
|
12137
|
+
function registerShutdownHandler(name, handler, phase = "close") {
|
|
12138
|
+
return defaultLifecycle.register(name, handler, phase);
|
|
12139
|
+
}
|
|
12140
|
+
function runGracefulShutdown(signal) {
|
|
12141
|
+
return defaultLifecycle.shutdown(signal);
|
|
12142
|
+
}
|
|
12143
|
+
function installGracefulShutdownSignals(signals = ["SIGINT", "SIGTERM"], lifecycle = defaultLifecycle) {
|
|
12144
|
+
const existing = installations.get(lifecycle);
|
|
12145
|
+
if (existing)
|
|
12146
|
+
return existing;
|
|
12147
|
+
let exiting = false;
|
|
12148
|
+
const handler = (signal) => {
|
|
12149
|
+
if (exiting)
|
|
12150
|
+
return;
|
|
12151
|
+
exiting = true;
|
|
12152
|
+
lifecycle.shutdown(signal).then((result) => process.exit(result.successful ? 0 : 1), (error) => {
|
|
12153
|
+
console.error("[shutdown] Coordinator failed:", error);
|
|
12154
|
+
process.exit(1);
|
|
11456
12155
|
});
|
|
11457
|
-
}
|
|
12156
|
+
};
|
|
12157
|
+
const callbacks = signals.map((signal) => {
|
|
12158
|
+
const callback = () => handler(signal);
|
|
12159
|
+
process.on(signal, callback);
|
|
12160
|
+
return () => process.off(signal, callback);
|
|
12161
|
+
});
|
|
12162
|
+
const uninstall = () => {
|
|
12163
|
+
for (const remove of callbacks)
|
|
12164
|
+
remove();
|
|
12165
|
+
if (installations.get(lifecycle) === uninstall)
|
|
12166
|
+
installations.delete(lifecycle);
|
|
12167
|
+
};
|
|
12168
|
+
installations.set(lifecycle, uninstall);
|
|
12169
|
+
return uninstall;
|
|
11458
12170
|
}
|
|
11459
12171
|
function resetGracefulShutdownForTests() {
|
|
11460
|
-
|
|
11461
|
-
|
|
11462
|
-
|
|
12172
|
+
for (const remove of [...installations.values()])
|
|
12173
|
+
remove();
|
|
12174
|
+
defaultLifecycle = new LifecycleCoordinator;
|
|
11463
12175
|
}
|
|
11464
12176
|
// ../../src/core/logging/requestLoggingMiddleware.ts
|
|
11465
12177
|
function createRequestLoggingMiddleware() {
|
|
@@ -11738,12 +12450,37 @@ class SyncQueue {
|
|
|
11738
12450
|
}
|
|
11739
12451
|
|
|
11740
12452
|
class AsyncQueue {
|
|
12453
|
+
active = new Set;
|
|
12454
|
+
closing;
|
|
12455
|
+
closed = false;
|
|
12456
|
+
assertOpen() {
|
|
12457
|
+
if (this.closed)
|
|
12458
|
+
throw new Error("Async queue is closed.");
|
|
12459
|
+
}
|
|
12460
|
+
enqueue(task, onError) {
|
|
12461
|
+
this.assertOpen();
|
|
12462
|
+
const running = new Promise((resolve) => {
|
|
12463
|
+
setTimeout(() => {
|
|
12464
|
+
Promise.resolve().then(task).catch(onError).finally(resolve);
|
|
12465
|
+
}, 0);
|
|
12466
|
+
});
|
|
12467
|
+
this.active.add(running);
|
|
12468
|
+
running.then(() => {
|
|
12469
|
+
this.active.delete(running);
|
|
12470
|
+
});
|
|
12471
|
+
}
|
|
11741
12472
|
async dispatch(job, payload) {
|
|
11742
|
-
|
|
11743
|
-
|
|
11744
|
-
|
|
11745
|
-
|
|
11746
|
-
|
|
12473
|
+
this.enqueue(() => job.handle(payload), (error) => {
|
|
12474
|
+
console.error("[AsyncQueue] Job failed:", error);
|
|
12475
|
+
});
|
|
12476
|
+
}
|
|
12477
|
+
close() {
|
|
12478
|
+
this.closing ??= (async () => {
|
|
12479
|
+
while (this.active.size)
|
|
12480
|
+
await Promise.all([...this.active]);
|
|
12481
|
+
this.closed = true;
|
|
12482
|
+
})();
|
|
12483
|
+
return this.closing;
|
|
11747
12484
|
}
|
|
11748
12485
|
}
|
|
11749
12486
|
function createQueue(driver) {
|
|
@@ -11864,7 +12601,7 @@ async function runQueueJob(envelope, failedJobs, options = {}) {
|
|
|
11864
12601
|
}
|
|
11865
12602
|
|
|
11866
12603
|
// ../../src/core/queue/redisQueue.ts
|
|
11867
|
-
var {RedisClient:
|
|
12604
|
+
var {RedisClient: RedisClient3 } = globalThis.Bun;
|
|
11868
12605
|
function queueListKey() {
|
|
11869
12606
|
return namespacedRedisKey("queue:default");
|
|
11870
12607
|
}
|
|
@@ -12060,7 +12797,10 @@ async function reclaimExpiredQueueReservations(client, queueKeys = QUEUE_KEYS) {
|
|
|
12060
12797
|
class RedisQueue {
|
|
12061
12798
|
client;
|
|
12062
12799
|
constructor(redisUrl) {
|
|
12063
|
-
this.client = new
|
|
12800
|
+
this.client = new RedisClient3(redisUrl);
|
|
12801
|
+
}
|
|
12802
|
+
close() {
|
|
12803
|
+
this.client.close();
|
|
12064
12804
|
}
|
|
12065
12805
|
async dispatch(job, payload) {
|
|
12066
12806
|
const name = jobRegistry.resolveName(job);
|
|
@@ -12089,7 +12829,7 @@ class QueueWorker {
|
|
|
12089
12829
|
this.failedJobs = failedJobs;
|
|
12090
12830
|
this.timeoutSeconds = timeoutSeconds;
|
|
12091
12831
|
this.queueKeys = queueKeys;
|
|
12092
|
-
this.client = new
|
|
12832
|
+
this.client = new RedisClient3(redisUrl);
|
|
12093
12833
|
}
|
|
12094
12834
|
requestStop() {
|
|
12095
12835
|
this.stopping = true;
|
|
@@ -12179,14 +12919,16 @@ class QueueWorker {
|
|
|
12179
12919
|
}
|
|
12180
12920
|
|
|
12181
12921
|
// ../../src/core/queue/resilientQueue.ts
|
|
12182
|
-
class ResilientQueue {
|
|
12922
|
+
class ResilientQueue extends AsyncQueue {
|
|
12183
12923
|
failedJobs;
|
|
12184
12924
|
asyncDispatch;
|
|
12185
12925
|
constructor(failedJobs, asyncDispatch = false) {
|
|
12926
|
+
super();
|
|
12186
12927
|
this.failedJobs = failedJobs;
|
|
12187
12928
|
this.asyncDispatch = asyncDispatch;
|
|
12188
12929
|
}
|
|
12189
12930
|
async dispatch(job, payload) {
|
|
12931
|
+
this.assertOpen();
|
|
12190
12932
|
const name = jobRegistry.resolveName(job);
|
|
12191
12933
|
if (!name) {
|
|
12192
12934
|
throw new Error("Job is not registered with the queue worker registry.");
|
|
@@ -12197,11 +12939,9 @@ class ResilientQueue {
|
|
|
12197
12939
|
attempts: 0
|
|
12198
12940
|
};
|
|
12199
12941
|
if (this.asyncDispatch) {
|
|
12200
|
-
|
|
12201
|
-
|
|
12202
|
-
|
|
12203
|
-
});
|
|
12204
|
-
}, 0);
|
|
12942
|
+
this.enqueue(() => runQueueJob(envelope, this.failedJobs), (error) => {
|
|
12943
|
+
console.error("[ResilientQueue] Job failed:", error);
|
|
12944
|
+
});
|
|
12205
12945
|
return;
|
|
12206
12946
|
}
|
|
12207
12947
|
await runQueueJob(envelope, this.failedJobs);
|
|
@@ -12230,9 +12970,9 @@ function createQueueWorker(redisUrl, failedJobs = createFailedJobService()) {
|
|
|
12230
12970
|
return new QueueWorker(redisUrl, failedJobs);
|
|
12231
12971
|
}
|
|
12232
12972
|
// ../../src/core/queue/queueMetrics.ts
|
|
12233
|
-
var {RedisClient:
|
|
12973
|
+
var {RedisClient: RedisClient4 } = globalThis.Bun;
|
|
12234
12974
|
async function readRedisQueueDepth(redisUrl) {
|
|
12235
|
-
const client = new
|
|
12975
|
+
const client = new RedisClient4(redisUrl);
|
|
12236
12976
|
const [high, defaultQueue, low] = await Promise.all([
|
|
12237
12977
|
client.llen(QUEUE_HIGH_KEY),
|
|
12238
12978
|
client.llen(QUEUE_LIST_KEY),
|
|
@@ -12280,6 +13020,149 @@ async function collectQueueMetrics() {
|
|
|
12280
13020
|
failedCount
|
|
12281
13021
|
};
|
|
12282
13022
|
}
|
|
13023
|
+
// ../../src/core/scheduler/schedule.ts
|
|
13024
|
+
import { createHash as createHash6 } from "crypto";
|
|
13025
|
+
|
|
13026
|
+
// ../../src/core/scheduler/leases.ts
|
|
13027
|
+
import { createHash as createHash5 } from "crypto";
|
|
13028
|
+
var {RedisClient: RedisClient5 } = globalThis.Bun;
|
|
13029
|
+
var LEASE_SCRIPT = `
|
|
13030
|
+
for _, key in ipairs(KEYS) do
|
|
13031
|
+
local kind = redis.call('TYPE', key).ok
|
|
13032
|
+
if kind ~= 'none' and kind ~= 'string' then return redis.error_reply('Unexpected scheduler key type') end
|
|
13033
|
+
end
|
|
13034
|
+
local action = ARGV[1]
|
|
13035
|
+
local token = ARGV[2]
|
|
13036
|
+
local ttl = tonumber(ARGV[3])
|
|
13037
|
+
if action == 'acquire' then
|
|
13038
|
+
if redis.call('EXISTS', KEYS[3]) == 1 or redis.call('EXISTS', KEYS[1]) == 1 or redis.call('EXISTS', KEYS[2]) == 1 then return 0 end
|
|
13039
|
+
redis.call('SET', KEYS[1], token, 'PX', ttl)
|
|
13040
|
+
redis.call('SET', KEYS[2], token, 'PX', ttl)
|
|
13041
|
+
return 1
|
|
13042
|
+
end
|
|
13043
|
+
if redis.call('GET', KEYS[1]) ~= token or redis.call('GET', KEYS[2]) ~= token then return 0 end
|
|
13044
|
+
if action == 'renew' then
|
|
13045
|
+
redis.call('PEXPIRE', KEYS[1], ttl)
|
|
13046
|
+
redis.call('PEXPIRE', KEYS[2], ttl)
|
|
13047
|
+
elseif action == 'complete' then
|
|
13048
|
+
redis.call('SET', KEYS[3], 'completed', 'PX', ttl)
|
|
13049
|
+
redis.call('DEL', KEYS[1], KEYS[2])
|
|
13050
|
+
elseif action == 'release' then
|
|
13051
|
+
redis.call('DEL', KEYS[1], KEYS[2])
|
|
13052
|
+
else return redis.error_reply('Unexpected scheduler action') end
|
|
13053
|
+
return 1
|
|
13054
|
+
`;
|
|
13055
|
+
|
|
13056
|
+
class RedisSchedulerLeaseStore {
|
|
13057
|
+
client;
|
|
13058
|
+
owned;
|
|
13059
|
+
namespace;
|
|
13060
|
+
timeoutMs;
|
|
13061
|
+
constructor(options) {
|
|
13062
|
+
this.namespace = options.namespace.trim();
|
|
13063
|
+
if (!this.namespace || this.namespace.length > 512)
|
|
13064
|
+
throw new TypeError("Invalid scheduler namespace.");
|
|
13065
|
+
this.timeoutMs = options.commandTimeoutMs ?? 1000;
|
|
13066
|
+
if (!Number.isSafeInteger(this.timeoutMs) || this.timeoutMs < 1 || this.timeoutMs > 60000)
|
|
13067
|
+
throw new RangeError("Invalid scheduler command timeout.");
|
|
13068
|
+
if (options.client)
|
|
13069
|
+
this.client = options.client;
|
|
13070
|
+
else {
|
|
13071
|
+
const client = new RedisClient5(options.redisUrl, { autoReconnect: false, maxRetries: 0 });
|
|
13072
|
+
this.owned = client;
|
|
13073
|
+
this.client = client;
|
|
13074
|
+
}
|
|
13075
|
+
}
|
|
13076
|
+
keys(lease) {
|
|
13077
|
+
const task = createHash5("sha256").update(JSON.stringify([this.namespace, lease.taskId])).digest("hex");
|
|
13078
|
+
const occurrence = createHash5("sha256").update(lease.occurrenceId).digest("hex");
|
|
13079
|
+
const prefix = `scheduler:{${task}}`;
|
|
13080
|
+
return [
|
|
13081
|
+
`${prefix}:active`,
|
|
13082
|
+
`${prefix}:occurrence:${occurrence}`,
|
|
13083
|
+
`${prefix}:completed:${occurrence}`
|
|
13084
|
+
];
|
|
13085
|
+
}
|
|
13086
|
+
async execute(action, lease, ttl) {
|
|
13087
|
+
if (!Number.isSafeInteger(ttl) || ttl < 1 || ttl > 2147483647)
|
|
13088
|
+
throw new RangeError("Invalid scheduler TTL.");
|
|
13089
|
+
let timer;
|
|
13090
|
+
try {
|
|
13091
|
+
const result = await Promise.race([
|
|
13092
|
+
this.client.send("EVAL", [
|
|
13093
|
+
LEASE_SCRIPT,
|
|
13094
|
+
"3",
|
|
13095
|
+
...this.keys(lease),
|
|
13096
|
+
action,
|
|
13097
|
+
lease.token,
|
|
13098
|
+
String(ttl)
|
|
13099
|
+
]),
|
|
13100
|
+
new Promise((_, reject) => {
|
|
13101
|
+
timer = setTimeout(() => reject(new Error("Scheduler store deadline exceeded.")), this.timeoutMs);
|
|
13102
|
+
})
|
|
13103
|
+
]);
|
|
13104
|
+
if (result !== 0 && result !== 1)
|
|
13105
|
+
throw new Error("Invalid scheduler store response.");
|
|
13106
|
+
return result === 1;
|
|
13107
|
+
} catch (error) {
|
|
13108
|
+
this.owned?.close();
|
|
13109
|
+
throw error;
|
|
13110
|
+
} finally {
|
|
13111
|
+
clearTimeout(timer);
|
|
13112
|
+
}
|
|
13113
|
+
}
|
|
13114
|
+
acquire(lease, leaseMs) {
|
|
13115
|
+
return this.execute("acquire", lease, leaseMs);
|
|
13116
|
+
}
|
|
13117
|
+
renew(lease, leaseMs) {
|
|
13118
|
+
return this.execute("renew", lease, leaseMs);
|
|
13119
|
+
}
|
|
13120
|
+
complete(lease, retentionMs) {
|
|
13121
|
+
return this.execute("complete", lease, retentionMs);
|
|
13122
|
+
}
|
|
13123
|
+
async release(lease) {
|
|
13124
|
+
await this.execute("release", lease, 1);
|
|
13125
|
+
}
|
|
13126
|
+
close() {
|
|
13127
|
+
this.owned?.close();
|
|
13128
|
+
}
|
|
13129
|
+
}
|
|
13130
|
+
|
|
13131
|
+
class SingleRunnerLeaseStore {
|
|
13132
|
+
tasks = new Map;
|
|
13133
|
+
async acquire(lease, leaseMs) {
|
|
13134
|
+
const task = this.tasks.get(lease.taskId) ?? { expires: 0 };
|
|
13135
|
+
if (task.active && task.expires > performance.now())
|
|
13136
|
+
return false;
|
|
13137
|
+
if ((task.completed ?? -Infinity) >= Number(lease.occurrenceId))
|
|
13138
|
+
return false;
|
|
13139
|
+
task.active = lease;
|
|
13140
|
+
task.expires = performance.now() + leaseMs;
|
|
13141
|
+
this.tasks.set(lease.taskId, task);
|
|
13142
|
+
return true;
|
|
13143
|
+
}
|
|
13144
|
+
async renew(lease, leaseMs) {
|
|
13145
|
+
const task = this.tasks.get(lease.taskId);
|
|
13146
|
+
if (!task || task.active?.token !== lease.token || task.expires <= performance.now())
|
|
13147
|
+
return false;
|
|
13148
|
+
task.expires = performance.now() + leaseMs;
|
|
13149
|
+
return true;
|
|
13150
|
+
}
|
|
13151
|
+
async complete(lease) {
|
|
13152
|
+
const task = this.tasks.get(lease.taskId);
|
|
13153
|
+
if (!task || task.active?.token !== lease.token || task.expires <= performance.now())
|
|
13154
|
+
return false;
|
|
13155
|
+
task.completed = Number(lease.occurrenceId);
|
|
13156
|
+
task.active = undefined;
|
|
13157
|
+
return true;
|
|
13158
|
+
}
|
|
13159
|
+
async release(lease) {
|
|
13160
|
+
const task = this.tasks.get(lease.taskId);
|
|
13161
|
+
if (task?.active?.token === lease.token)
|
|
13162
|
+
task.active = undefined;
|
|
13163
|
+
}
|
|
13164
|
+
}
|
|
13165
|
+
|
|
12283
13166
|
// ../../src/core/scheduler/osCron.ts
|
|
12284
13167
|
function parseScheduleExpression(expression, from = new Date) {
|
|
12285
13168
|
return Bun.cron.parse(expression, from);
|
|
@@ -12310,11 +13193,25 @@ function assertScheduleExpression(expression) {
|
|
|
12310
13193
|
}
|
|
12311
13194
|
|
|
12312
13195
|
// ../../src/core/scheduler/schedule.ts
|
|
13196
|
+
class SchedulerLeaseLostError extends Error {
|
|
13197
|
+
constructor(taskName, options) {
|
|
13198
|
+
super(`Scheduler lease lost for task ${taskName}.`, options);
|
|
13199
|
+
this.name = "SchedulerLeaseLostError";
|
|
13200
|
+
}
|
|
13201
|
+
}
|
|
13202
|
+
|
|
12313
13203
|
class Schedule {
|
|
12314
13204
|
tasks = [];
|
|
12315
13205
|
command(expression, name, run) {
|
|
12316
13206
|
assertScheduleExpression(expression);
|
|
12317
|
-
|
|
13207
|
+
if (typeof run !== "function")
|
|
13208
|
+
throw new TypeError("Scheduled task handler must be a function.");
|
|
13209
|
+
const normalized = name.trim();
|
|
13210
|
+
if (!normalized || normalized.length > 200 || this.tasks.some((task) => task.name === normalized))
|
|
13211
|
+
throw new TypeError("Scheduled task names must be nonempty and unique (at most 200 characters).");
|
|
13212
|
+
if (this.tasks.length >= 4096)
|
|
13213
|
+
throw new RangeError("A schedule supports at most 4096 tasks.");
|
|
13214
|
+
this.tasks.push({ expression, name: normalized, run });
|
|
12318
13215
|
return this;
|
|
12319
13216
|
}
|
|
12320
13217
|
dueTasks(now = new Date) {
|
|
@@ -12325,12 +13222,125 @@ class Schedule {
|
|
|
12325
13222
|
}
|
|
12326
13223
|
}
|
|
12327
13224
|
var appSchedule = new Schedule;
|
|
12328
|
-
|
|
13225
|
+
var localStores = new WeakMap;
|
|
13226
|
+
function bounded(value, min, max, name) {
|
|
13227
|
+
if (!Number.isSafeInteger(value) || value < min || value > max)
|
|
13228
|
+
throw new RangeError(`Invalid scheduler ${name}.`);
|
|
13229
|
+
return value;
|
|
13230
|
+
}
|
|
13231
|
+
async function runDueScheduledTasks(schedule = appSchedule, now = new Date, options = {}) {
|
|
13232
|
+
const timestamp = now.getTime();
|
|
13233
|
+
if (!Number.isFinite(timestamp))
|
|
13234
|
+
throw new TypeError("Invalid scheduler date.");
|
|
12329
13235
|
const due = schedule.dueTasks(now);
|
|
12330
|
-
|
|
12331
|
-
|
|
13236
|
+
if (!due.length || options.signal?.aborted)
|
|
13237
|
+
return 0;
|
|
13238
|
+
const coordination = options.coordination ?? process.env.SCHEDULER_COORDINATION ?? (isProductionEnv() ? "redis" : "single-runner");
|
|
13239
|
+
if (coordination !== "redis" && coordination !== "single-runner")
|
|
13240
|
+
throw new TypeError("Unsupported scheduler coordination.");
|
|
13241
|
+
const leaseMs = bounded(options.leaseMs ?? 30000, 100, 3600000, "leaseMs");
|
|
13242
|
+
const renewalMs = bounded(options.renewalMs ?? Math.floor(leaseMs / 3), 1, Math.floor(leaseMs / 3), "renewalMs");
|
|
13243
|
+
const timeoutMs = bounded(options.commandTimeoutMs ?? Math.min(1000, Math.floor(leaseMs / 4)), 1, Math.floor(leaseMs / 3), "commandTimeoutMs");
|
|
13244
|
+
const retention = bounded(options.completedRetentionMs ?? 7 * 86400000, leaseMs, 2147483647, "completedRetentionMs");
|
|
13245
|
+
const namespace = (options.namespace ?? namespacedRedisKey("schedule")).trim();
|
|
13246
|
+
if (!namespace || namespace.length > 512)
|
|
13247
|
+
throw new TypeError("Invalid scheduler namespace.");
|
|
13248
|
+
let owned;
|
|
13249
|
+
let store = options.leaseStore;
|
|
13250
|
+
if (!store && coordination === "redis") {
|
|
13251
|
+
const redisUrl = options.redisUrl ?? process.env.REDIS_URL?.trim();
|
|
13252
|
+
if (!redisUrl)
|
|
13253
|
+
throw new Error("Redis scheduler coordination requires REDIS_URL.");
|
|
13254
|
+
owned = new RedisSchedulerLeaseStore({
|
|
13255
|
+
redisUrl,
|
|
13256
|
+
namespace,
|
|
13257
|
+
commandTimeoutMs: timeoutMs
|
|
13258
|
+
});
|
|
13259
|
+
store = owned;
|
|
13260
|
+
}
|
|
13261
|
+
if (!store) {
|
|
13262
|
+
let local = localStores.get(schedule);
|
|
13263
|
+
if (!local) {
|
|
13264
|
+
local = new SingleRunnerLeaseStore;
|
|
13265
|
+
localStores.set(schedule, local);
|
|
13266
|
+
}
|
|
13267
|
+
store = local;
|
|
13268
|
+
}
|
|
13269
|
+
const occurrenceId = String(Math.floor(timestamp / 60000) * 60000);
|
|
13270
|
+
let completed = 0;
|
|
13271
|
+
try {
|
|
13272
|
+
for (const task of due) {
|
|
13273
|
+
if (options.signal?.aborted)
|
|
13274
|
+
break;
|
|
13275
|
+
const lease = { taskId: task.name, occurrenceId, token: crypto.randomUUID() };
|
|
13276
|
+
if (!await store.acquire(lease, leaseMs))
|
|
13277
|
+
continue;
|
|
13278
|
+
if (options.signal?.aborted) {
|
|
13279
|
+
await store.release(lease);
|
|
13280
|
+
break;
|
|
13281
|
+
}
|
|
13282
|
+
const controller = new AbortController;
|
|
13283
|
+
let lost;
|
|
13284
|
+
const lose = (cause) => {
|
|
13285
|
+
lost ??= new SchedulerLeaseLostError(task.name, { cause });
|
|
13286
|
+
controller.abort(lost);
|
|
13287
|
+
return lost;
|
|
13288
|
+
};
|
|
13289
|
+
let timer;
|
|
13290
|
+
let renewal;
|
|
13291
|
+
let stopped = false;
|
|
13292
|
+
const renew = async () => {
|
|
13293
|
+
try {
|
|
13294
|
+
if (!await store.renew(lease, leaseMs))
|
|
13295
|
+
throw lose();
|
|
13296
|
+
} catch (error) {
|
|
13297
|
+
throw lose(error);
|
|
13298
|
+
}
|
|
13299
|
+
};
|
|
13300
|
+
const tick = () => {
|
|
13301
|
+
renewal = renew().catch(() => {}).finally(() => {
|
|
13302
|
+
if (!stopped && !lost)
|
|
13303
|
+
timer = setTimeout(tick, renewalMs);
|
|
13304
|
+
});
|
|
13305
|
+
};
|
|
13306
|
+
timer = setTimeout(tick, renewalMs);
|
|
13307
|
+
try {
|
|
13308
|
+
await task.run({
|
|
13309
|
+
taskName: task.name,
|
|
13310
|
+
occurrenceId: createHash6("sha256").update(JSON.stringify([namespace, task.name, occurrenceId])).digest("hex"),
|
|
13311
|
+
scheduledAt: new Date(Number(occurrenceId)),
|
|
13312
|
+
signal: controller.signal,
|
|
13313
|
+
async assertOwnership() {
|
|
13314
|
+
if (lost)
|
|
13315
|
+
throw lost;
|
|
13316
|
+
await renew();
|
|
13317
|
+
}
|
|
13318
|
+
});
|
|
13319
|
+
stopped = true;
|
|
13320
|
+
clearTimeout(timer);
|
|
13321
|
+
await renewal;
|
|
13322
|
+
if (lost || !await store.complete(lease, retention))
|
|
13323
|
+
throw lost ?? lose();
|
|
13324
|
+
completed++;
|
|
13325
|
+
} catch (error) {
|
|
13326
|
+
stopped = true;
|
|
13327
|
+
clearTimeout(timer);
|
|
13328
|
+
await renewal;
|
|
13329
|
+
try {
|
|
13330
|
+
await store.release(lease);
|
|
13331
|
+
} catch (releaseError) {
|
|
13332
|
+
throw new AggregateError([error, releaseError], "Scheduled task and lease release failed.");
|
|
13333
|
+
}
|
|
13334
|
+
throw error;
|
|
13335
|
+
} finally {
|
|
13336
|
+
stopped = true;
|
|
13337
|
+
clearTimeout(timer);
|
|
13338
|
+
}
|
|
13339
|
+
}
|
|
13340
|
+
return completed;
|
|
13341
|
+
} finally {
|
|
13342
|
+
owned?.close();
|
|
12332
13343
|
}
|
|
12333
|
-
return due.length;
|
|
12334
13344
|
}
|
|
12335
13345
|
// ../../src/core/security/publicReads.ts
|
|
12336
13346
|
function isPublicReadsEnabled() {
|
|
@@ -12452,7 +13462,7 @@ init_tenantContext();
|
|
|
12452
13462
|
init_tenantDatabaseScope();
|
|
12453
13463
|
|
|
12454
13464
|
// ../../src/core/tenant/tenantMiddleware.ts
|
|
12455
|
-
import { createHash as
|
|
13465
|
+
import { createHash as createHash7 } from "crypto";
|
|
12456
13466
|
init_repositoryConnection();
|
|
12457
13467
|
init_databaseTenantContext();
|
|
12458
13468
|
init_tenancyConfig();
|
|
@@ -12460,9 +13470,7 @@ init_tenantContext();
|
|
|
12460
13470
|
init_tenantDatabaseScope();
|
|
12461
13471
|
var DEFAULT_TENANT = {
|
|
12462
13472
|
id: 1,
|
|
12463
|
-
slug: "default"
|
|
12464
|
-
plan: "free",
|
|
12465
|
-
region: "eu"
|
|
13473
|
+
slug: "default"
|
|
12466
13474
|
};
|
|
12467
13475
|
async function resolveUserTenantId(userId) {
|
|
12468
13476
|
if (!isTenancyEnabled()) {
|
|
@@ -12479,9 +13487,9 @@ async function resolveUserTenantId(userId) {
|
|
|
12479
13487
|
});
|
|
12480
13488
|
}
|
|
12481
13489
|
function auditChecksum(payload) {
|
|
12482
|
-
return
|
|
13490
|
+
return createHash7("sha256").update(JSON.stringify(payload)).digest("hex");
|
|
12483
13491
|
}
|
|
12484
|
-
async function resolveTenantForRequest(request) {
|
|
13492
|
+
async function resolveTenantForRequest(request, resolver) {
|
|
12485
13493
|
const user = currentAuthUser();
|
|
12486
13494
|
const headerValue = request.headers.get("x-tenant-id")?.trim();
|
|
12487
13495
|
const parsedHeader = headerValue !== undefined && headerValue.length > 0 ? Number.parseInt(headerValue, 10) : Number.NaN;
|
|
@@ -12493,20 +13501,20 @@ async function resolveTenantForRequest(request) {
|
|
|
12493
13501
|
if (Number.isInteger(parsedHeader) && parsedHeader > 0 && parsedHeader !== userTenantId) {
|
|
12494
13502
|
throw new ForbiddenError("Tenant header does not match your account.");
|
|
12495
13503
|
}
|
|
12496
|
-
const memberTenant = await
|
|
13504
|
+
const memberTenant = await resolver(userTenantId);
|
|
12497
13505
|
if (memberTenant) {
|
|
12498
13506
|
return memberTenant;
|
|
12499
13507
|
}
|
|
12500
13508
|
return DEFAULT_TENANT;
|
|
12501
13509
|
}
|
|
12502
13510
|
if (Number.isInteger(parsedHeader) && parsedHeader > 0) {
|
|
12503
|
-
const headerTenant = await
|
|
13511
|
+
const headerTenant = await resolver(parsedHeader);
|
|
12504
13512
|
if (headerTenant) {
|
|
12505
13513
|
return headerTenant;
|
|
12506
13514
|
}
|
|
12507
13515
|
return DEFAULT_TENANT;
|
|
12508
13516
|
}
|
|
12509
|
-
const adminTenant = await
|
|
13517
|
+
const adminTenant = await resolver(userTenantId);
|
|
12510
13518
|
if (adminTenant) {
|
|
12511
13519
|
return adminTenant;
|
|
12512
13520
|
}
|
|
@@ -12515,13 +13523,14 @@ async function resolveTenantForRequest(request) {
|
|
|
12515
13523
|
}
|
|
12516
13524
|
const headerTenantId = Number.isInteger(parsedHeader) && parsedHeader > 0 ? parsedHeader : null;
|
|
12517
13525
|
const tenantId = isPublicReadsEnabled() && headerTenantId !== null ? headerTenantId : DEFAULT_TENANT.id;
|
|
12518
|
-
const guestTenant = await
|
|
13526
|
+
const guestTenant = await resolver(tenantId);
|
|
12519
13527
|
if (guestTenant) {
|
|
12520
13528
|
return guestTenant;
|
|
12521
13529
|
}
|
|
12522
13530
|
return DEFAULT_TENANT;
|
|
12523
13531
|
}
|
|
12524
|
-
function createTenantMiddleware() {
|
|
13532
|
+
function createTenantMiddleware(options = {}) {
|
|
13533
|
+
const resolver = options.resolveTenant ?? resolveTenant;
|
|
12525
13534
|
return async (request, next) => {
|
|
12526
13535
|
const pathname = new URL(request.url).pathname;
|
|
12527
13536
|
if (pathname.startsWith("/scim/")) {
|
|
@@ -12531,7 +13540,16 @@ function createTenantMiddleware() {
|
|
|
12531
13540
|
return await runWithTenant(DEFAULT_TENANT, next);
|
|
12532
13541
|
}
|
|
12533
13542
|
try {
|
|
12534
|
-
const tenant = await resolveTenantForRequest(request)
|
|
13543
|
+
const tenant = await resolveTenantForRequest(request, async (id) => {
|
|
13544
|
+
const tenant = await resolver(id);
|
|
13545
|
+
if (options.resolveTenant && tenant === null) {
|
|
13546
|
+
throw new ForbiddenError("Tenant not found.");
|
|
13547
|
+
}
|
|
13548
|
+
if (tenant && tenant.id !== id) {
|
|
13549
|
+
throw new Error("Tenant resolver returned a different tenant identity.");
|
|
13550
|
+
}
|
|
13551
|
+
return tenant;
|
|
13552
|
+
});
|
|
12535
13553
|
return await runWithTenantDatabase(tenant, async () => {
|
|
12536
13554
|
return await next();
|
|
12537
13555
|
});
|
|
@@ -12553,99 +13571,185 @@ function runWithTraceContext(context, callback) {
|
|
|
12553
13571
|
function currentTraceId() {
|
|
12554
13572
|
return traceContextStorage.getStore()?.traceId ?? null;
|
|
12555
13573
|
}
|
|
13574
|
+
// ../../src/core/tracing/tracingMiddleware.ts
|
|
13575
|
+
import { ROOT_CONTEXT, SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
|
|
13576
|
+
|
|
12556
13577
|
// ../../src/core/tracing/otel.ts
|
|
12557
|
-
import {
|
|
12558
|
-
|
|
12559
|
-
|
|
12560
|
-
}
|
|
12561
|
-
|
|
12562
|
-
|
|
12563
|
-
|
|
12564
|
-
|
|
12565
|
-
|
|
12566
|
-
|
|
12567
|
-
|
|
12568
|
-
|
|
12569
|
-
|
|
12570
|
-
|
|
12571
|
-
|
|
12572
|
-
|
|
12573
|
-
|
|
12574
|
-
|
|
13578
|
+
import { context } from "@opentelemetry/api";
|
|
13579
|
+
import { AsyncLocalStorageContextManager } from "@opentelemetry/context-async-hooks";
|
|
13580
|
+
import { W3CTraceContextPropagator } from "@opentelemetry/core";
|
|
13581
|
+
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
|
|
13582
|
+
import { resourceFromAttributes } from "@opentelemetry/resources";
|
|
13583
|
+
import {
|
|
13584
|
+
AlwaysOffSampler,
|
|
13585
|
+
AlwaysOnSampler,
|
|
13586
|
+
BatchSpanProcessor,
|
|
13587
|
+
ParentBasedSampler,
|
|
13588
|
+
TraceIdRatioBasedSampler
|
|
13589
|
+
} from "@opentelemetry/sdk-trace-base";
|
|
13590
|
+
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
|
|
13591
|
+
function numberOption(value, fallback, name, max = 1e6) {
|
|
13592
|
+
const result = value ?? fallback;
|
|
13593
|
+
if (!Number.isInteger(result) || result < 1 || result > max)
|
|
13594
|
+
throw new TypeError(`Invalid tracing ${name}.`);
|
|
13595
|
+
return result;
|
|
12575
13596
|
}
|
|
12576
|
-
|
|
12577
|
-
const
|
|
12578
|
-
|
|
13597
|
+
function ensureContextManager() {
|
|
13598
|
+
const key = Symbol.for("@getstrata/otel-context-installed");
|
|
13599
|
+
const state = globalThis;
|
|
13600
|
+
if (state[key])
|
|
12579
13601
|
return;
|
|
13602
|
+
const manager = new AsyncLocalStorageContextManager().enable();
|
|
13603
|
+
if (!context.setGlobalContextManager(manager))
|
|
13604
|
+
manager.disable();
|
|
13605
|
+
state[key] = true;
|
|
13606
|
+
}
|
|
13607
|
+
function createTracingRuntime(options = {}) {
|
|
13608
|
+
ensureContextManager();
|
|
13609
|
+
const disabled = process.env.OTEL_SDK_DISABLED === "true";
|
|
13610
|
+
const samplerName = options.sampleRatio === undefined ? process.env.OTEL_TRACES_SAMPLER ?? "parentbased_traceidratio" : "parentbased_traceidratio";
|
|
13611
|
+
const rootName = samplerName.replace(/^parentbased_/, "");
|
|
13612
|
+
if (!["always_on", "always_off", "traceidratio"].includes(rootName))
|
|
13613
|
+
throw new TypeError("Unsupported tracing sampler.");
|
|
13614
|
+
const ratio = options.sampleRatio ?? Number(process.env.OTEL_TRACES_SAMPLER_ARG ?? "1");
|
|
13615
|
+
if (!Number.isFinite(ratio) || ratio < 0 || ratio > 1)
|
|
13616
|
+
throw new TypeError("Invalid tracing sampleRatio.");
|
|
13617
|
+
const rootSampler = rootName === "always_on" ? new AlwaysOnSampler : rootName === "always_off" ? new AlwaysOffSampler : new TraceIdRatioBasedSampler(ratio);
|
|
13618
|
+
const sampler = disabled ? new AlwaysOffSampler : samplerName.startsWith("parentbased_") ? new ParentBasedSampler({ root: rootSampler }) : rootSampler;
|
|
13619
|
+
const queueSize = numberOption(options.maxQueueSize, 2048, "maxQueueSize");
|
|
13620
|
+
const batchSize = numberOption(options.maxExportBatchSize, Math.min(512, queueSize), "maxExportBatchSize", queueSize);
|
|
13621
|
+
const timeout = numberOption(options.exportTimeoutMillis, 5000, "exportTimeoutMillis", 60000);
|
|
13622
|
+
const delay = numberOption(options.scheduledDelayMillis, 5000, "scheduledDelayMillis", 60000);
|
|
13623
|
+
let endpoint = options.endpoint ?? process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT?.trim();
|
|
13624
|
+
if (!endpoint) {
|
|
13625
|
+
const base = process.env.OTEL_EXPORTER_OTLP_ENDPOINT?.trim();
|
|
13626
|
+
if (base)
|
|
13627
|
+
endpoint = base.endsWith("/v1/traces") ? base : `${base.replace(/\/$/, "")}/v1/traces`;
|
|
12580
13628
|
}
|
|
12581
|
-
|
|
12582
|
-
|
|
12583
|
-
|
|
12584
|
-
|
|
12585
|
-
|
|
12586
|
-
|
|
12587
|
-
|
|
12588
|
-
|
|
12589
|
-
|
|
12590
|
-
|
|
12591
|
-
|
|
12592
|
-
|
|
12593
|
-
|
|
12594
|
-
|
|
12595
|
-
|
|
12596
|
-
|
|
12597
|
-
|
|
12598
|
-
|
|
12599
|
-
|
|
12600
|
-
|
|
13629
|
+
if (endpoint) {
|
|
13630
|
+
let url;
|
|
13631
|
+
try {
|
|
13632
|
+
url = new URL(endpoint);
|
|
13633
|
+
} catch {
|
|
13634
|
+
throw new TypeError("Invalid tracing collector endpoint.");
|
|
13635
|
+
}
|
|
13636
|
+
if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.search || url.hash)
|
|
13637
|
+
throw new TypeError("Invalid tracing collector endpoint.");
|
|
13638
|
+
}
|
|
13639
|
+
const exporter = disabled ? undefined : options.exporter ?? (endpoint ? new OTLPTraceExporter({ url: endpoint, timeoutMillis: timeout, concurrencyLimit: 1 }) : undefined);
|
|
13640
|
+
const provider = new NodeTracerProvider({
|
|
13641
|
+
resource: resourceFromAttributes({ "service.name": options.serviceName ?? otelServiceName() }),
|
|
13642
|
+
sampler,
|
|
13643
|
+
spanProcessors: exporter ? [
|
|
13644
|
+
new BatchSpanProcessor(exporter, {
|
|
13645
|
+
maxQueueSize: queueSize,
|
|
13646
|
+
maxExportBatchSize: batchSize,
|
|
13647
|
+
scheduledDelayMillis: delay,
|
|
13648
|
+
exportTimeoutMillis: timeout
|
|
13649
|
+
})
|
|
13650
|
+
] : [],
|
|
13651
|
+
spanLimits: {
|
|
13652
|
+
attributeCountLimit: 16,
|
|
13653
|
+
attributeValueLengthLimit: 256,
|
|
13654
|
+
eventCountLimit: 8,
|
|
13655
|
+
linkCountLimit: 8
|
|
13656
|
+
},
|
|
13657
|
+
forceFlushTimeoutMillis: timeout
|
|
12601
13658
|
});
|
|
13659
|
+
let shutdown;
|
|
13660
|
+
return {
|
|
13661
|
+
tracer: provider.getTracer("@getstrata/http"),
|
|
13662
|
+
propagator: new W3CTraceContextPropagator,
|
|
13663
|
+
run: (parent, callback) => context.with(parent, callback),
|
|
13664
|
+
forceFlush: () => provider.forceFlush(),
|
|
13665
|
+
shutdown: () => shutdown ??= provider.shutdown()
|
|
13666
|
+
};
|
|
13667
|
+
}
|
|
13668
|
+
var runtimeKey = Symbol.for("@getstrata/otel-runtime");
|
|
13669
|
+
function runtimeState() {
|
|
13670
|
+
const state = globalThis;
|
|
13671
|
+
state[runtimeKey] ??= { owners: 0 };
|
|
13672
|
+
return state[runtimeKey];
|
|
13673
|
+
}
|
|
13674
|
+
function getTracingRuntime() {
|
|
13675
|
+
const state = runtimeState();
|
|
13676
|
+
state.runtime ??= createTracingRuntime();
|
|
13677
|
+
return state.runtime;
|
|
13678
|
+
}
|
|
13679
|
+
function acquireTracingRuntime() {
|
|
13680
|
+
const state = runtimeState();
|
|
13681
|
+
const runtime = getTracingRuntime();
|
|
13682
|
+
state.owners++;
|
|
13683
|
+
let released = false;
|
|
13684
|
+
return {
|
|
13685
|
+
flush: () => runtime.forceFlush(),
|
|
13686
|
+
async release() {
|
|
13687
|
+
if (released)
|
|
13688
|
+
return;
|
|
13689
|
+
released = true;
|
|
13690
|
+
if (--state.owners === 0) {
|
|
13691
|
+
if (state.runtime === runtime)
|
|
13692
|
+
state.runtime = undefined;
|
|
13693
|
+
await runtime.shutdown();
|
|
13694
|
+
}
|
|
13695
|
+
}
|
|
13696
|
+
};
|
|
12602
13697
|
}
|
|
12603
13698
|
|
|
12604
13699
|
// ../../src/core/tracing/tracingMiddleware.ts
|
|
12605
|
-
|
|
12606
|
-
|
|
12607
|
-
|
|
12608
|
-
|
|
12609
|
-
|
|
12610
|
-
|
|
12611
|
-
|
|
12612
|
-
|
|
12613
|
-
|
|
12614
|
-
|
|
12615
|
-
|
|
12616
|
-
|
|
13700
|
+
var headerGetter = {
|
|
13701
|
+
keys: (headers) => [...headers.keys()],
|
|
13702
|
+
get: (headers, key) => headers.get(key) ?? undefined
|
|
13703
|
+
};
|
|
13704
|
+
var methods = new Set([
|
|
13705
|
+
"GET",
|
|
13706
|
+
"POST",
|
|
13707
|
+
"PUT",
|
|
13708
|
+
"PATCH",
|
|
13709
|
+
"DELETE",
|
|
13710
|
+
"HEAD",
|
|
13711
|
+
"OPTIONS",
|
|
13712
|
+
"CONNECT",
|
|
13713
|
+
"TRACE"
|
|
13714
|
+
]);
|
|
13715
|
+
function createTracingMiddleware(runtime) {
|
|
12617
13716
|
return async (request, next) => {
|
|
12618
|
-
const
|
|
12619
|
-
const
|
|
12620
|
-
const
|
|
12621
|
-
const
|
|
12622
|
-
|
|
12623
|
-
|
|
12624
|
-
|
|
12625
|
-
|
|
12626
|
-
|
|
12627
|
-
|
|
12628
|
-
|
|
12629
|
-
|
|
12630
|
-
|
|
12631
|
-
|
|
12632
|
-
|
|
12633
|
-
|
|
12634
|
-
|
|
12635
|
-
|
|
12636
|
-
|
|
12637
|
-
|
|
12638
|
-
|
|
12639
|
-
|
|
12640
|
-
|
|
12641
|
-
|
|
12642
|
-
|
|
12643
|
-
|
|
12644
|
-
|
|
12645
|
-
|
|
12646
|
-
|
|
12647
|
-
})
|
|
12648
|
-
|
|
13717
|
+
const telemetry = runtime ?? getTracingRuntime();
|
|
13718
|
+
const method = methods.has(request.method) ? request.method : "_OTHER";
|
|
13719
|
+
const route = currentRequestMeta().routeTemplate ?? "__unmatched__";
|
|
13720
|
+
const parent = telemetry.propagator.extract(ROOT_CONTEXT, request.headers, headerGetter);
|
|
13721
|
+
const span = telemetry.tracer.startSpan(`${method} ${route}`, {
|
|
13722
|
+
kind: SpanKind.SERVER,
|
|
13723
|
+
attributes: { "http.request.method": method, "http.route": route }
|
|
13724
|
+
}, parent);
|
|
13725
|
+
const identity = span.spanContext();
|
|
13726
|
+
const started = performance.now();
|
|
13727
|
+
return await telemetry.run(trace.setSpan(parent, span), () => runWithTraceContext({ traceId: identity.traceId, spanId: identity.spanId }, async () => {
|
|
13728
|
+
try {
|
|
13729
|
+
const response = await next();
|
|
13730
|
+
span.setAttribute("http.response.status_code", response.status);
|
|
13731
|
+
if (response.status >= 500)
|
|
13732
|
+
span.setStatus({ code: SpanStatusCode.ERROR });
|
|
13733
|
+
const headers = new Headers(response.headers);
|
|
13734
|
+
headers.delete("tracestate");
|
|
13735
|
+
headers.set("x-trace-id", identity.traceId);
|
|
13736
|
+
headers.set("x-span-id", identity.spanId);
|
|
13737
|
+
telemetry.propagator.inject(trace.setSpan(parent, span), headers, {
|
|
13738
|
+
set: (carrier, key, value) => carrier.set(key, value)
|
|
13739
|
+
});
|
|
13740
|
+
headers.set("server-timing", `app;dur=${(performance.now() - started).toFixed(2)}`);
|
|
13741
|
+
return new Response(response.body, {
|
|
13742
|
+
status: response.status,
|
|
13743
|
+
statusText: response.statusText,
|
|
13744
|
+
headers
|
|
13745
|
+
});
|
|
13746
|
+
} catch (error) {
|
|
13747
|
+
span.setStatus({ code: SpanStatusCode.ERROR });
|
|
13748
|
+
throw error;
|
|
13749
|
+
} finally {
|
|
13750
|
+
span.end();
|
|
13751
|
+
}
|
|
13752
|
+
}));
|
|
12649
13753
|
};
|
|
12650
13754
|
}
|
|
12651
13755
|
// ../../src/core/validation/rules.ts
|
|
@@ -13056,6 +14160,7 @@ export {
|
|
|
13056
14160
|
JobRegistry,
|
|
13057
14161
|
JsonResource,
|
|
13058
14162
|
JwtGuard,
|
|
14163
|
+
LifecycleCoordinator,
|
|
13059
14164
|
LocalStorageDriver,
|
|
13060
14165
|
LogMailDriver,
|
|
13061
14166
|
MAX_PER_PAGE,
|
|
@@ -13081,12 +14186,14 @@ export {
|
|
|
13081
14186
|
QueueWorker,
|
|
13082
14187
|
RLS_HELPER_SQL,
|
|
13083
14188
|
RedisQueue,
|
|
14189
|
+
RedisSchedulerLeaseStore,
|
|
13084
14190
|
RepositoryQuery,
|
|
13085
14191
|
ResilientQueue,
|
|
13086
14192
|
ResourceCollection,
|
|
13087
14193
|
SamlProvider,
|
|
13088
14194
|
SamlServiceProvider,
|
|
13089
14195
|
Schedule,
|
|
14196
|
+
SchedulerLeaseLostError,
|
|
13090
14197
|
Schema,
|
|
13091
14198
|
ServiceContainer,
|
|
13092
14199
|
SqlOutbox,
|
|
@@ -13100,6 +14207,7 @@ export {
|
|
|
13100
14207
|
WebFormRequest,
|
|
13101
14208
|
WhereBuilder,
|
|
13102
14209
|
absoluteTemporarySignedUrl,
|
|
14210
|
+
acquireTracingRuntime,
|
|
13103
14211
|
apiPrefix,
|
|
13104
14212
|
appCookieName,
|
|
13105
14213
|
appDevSecret,
|
|
@@ -13130,6 +14238,7 @@ export {
|
|
|
13130
14238
|
bindBunSql,
|
|
13131
14239
|
bindDatabaseConnection,
|
|
13132
14240
|
bindRouteModel,
|
|
14241
|
+
bootModels,
|
|
13133
14242
|
buildAdvancedWhereClause,
|
|
13134
14243
|
buildCountQuery,
|
|
13135
14244
|
buildDeleteByIdQuery,
|
|
@@ -13149,6 +14258,7 @@ export {
|
|
|
13149
14258
|
buildUpdateQuery,
|
|
13150
14259
|
buildWhereClause,
|
|
13151
14260
|
cache,
|
|
14261
|
+
clearActiveApplicationContext,
|
|
13152
14262
|
collectQueueMetrics,
|
|
13153
14263
|
compileBlueprint,
|
|
13154
14264
|
completePasswordLogin,
|
|
@@ -13205,6 +14315,7 @@ export {
|
|
|
13205
14315
|
createThrottleMiddleware,
|
|
13206
14316
|
createTokenAbilityChecker,
|
|
13207
14317
|
createTracingMiddleware,
|
|
14318
|
+
createTracingRuntime,
|
|
13208
14319
|
createTrackedJob,
|
|
13209
14320
|
createValidateSignatureMiddleware,
|
|
13210
14321
|
createdResponse,
|
|
@@ -13217,6 +14328,7 @@ export {
|
|
|
13217
14328
|
currentTenant,
|
|
13218
14329
|
currentTenantId,
|
|
13219
14330
|
currentTraceId,
|
|
14331
|
+
defineModel,
|
|
13220
14332
|
defineTable,
|
|
13221
14333
|
dehydrateValue,
|
|
13222
14334
|
dialectFor,
|
|
@@ -13248,6 +14360,7 @@ export {
|
|
|
13248
14360
|
getNamedConnection,
|
|
13249
14361
|
getQueryParams,
|
|
13250
14362
|
getRouteParams,
|
|
14363
|
+
getTracingRuntime,
|
|
13251
14364
|
grammarForDriver,
|
|
13252
14365
|
guestCanViewResource,
|
|
13253
14366
|
hasActiveDatabaseConnection,
|
|
@@ -13331,7 +14444,6 @@ export {
|
|
|
13331
14444
|
qualifyColumn,
|
|
13332
14445
|
queue,
|
|
13333
14446
|
quoteIdentifier,
|
|
13334
|
-
rateLimitMultiplierForPlan,
|
|
13335
14447
|
readBunRequestCookie,
|
|
13336
14448
|
readClientIp,
|
|
13337
14449
|
readOptionalEnum,
|