@getstrata/core 2.0.9 → 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 +37 -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/container.d.ts +28 -10
- package/dist/core/contracts/di.d.ts +7 -4
- 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 +123 -42
- 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/middleware.d.ts +2 -2
- package/dist/core/http/response.d.ts +2 -0
- package/dist/core/http/routeMiddleware.d.ts +3 -2
- 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/container.js +8 -1
- package/dist/entries/contracts/serviceTokens.js +83 -0
- package/dist/entries/database/factory.js +0 -3
- package/dist/entries/database/model.js +1 -2465
- 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/routeMiddleware.js +1 -4
- 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 +14 -10
- package/dist/index.js +1770 -638
- 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";
|
|
@@ -1124,6 +1209,12 @@ function readBasicCredentials(request) {
|
|
|
1124
1209
|
}
|
|
1125
1210
|
|
|
1126
1211
|
// ../../src/core/auth/password.ts
|
|
1212
|
+
async function hashPassword(password) {
|
|
1213
|
+
return await Bun.password.hash(password, {
|
|
1214
|
+
algorithm: "bcrypt",
|
|
1215
|
+
cost: 12
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1127
1218
|
async function verifyPassword(password, passwordHash) {
|
|
1128
1219
|
return await Bun.password.verify(password, passwordHash);
|
|
1129
1220
|
}
|
|
@@ -2048,6 +2139,12 @@ function setActiveApplicationContext(context) {
|
|
|
2048
2139
|
activeContext = context;
|
|
2049
2140
|
globalThis[APPLICATION_CONTEXT_KEY] = context;
|
|
2050
2141
|
}
|
|
2142
|
+
function clearActiveApplicationContext(context) {
|
|
2143
|
+
if (readStoredApplicationContext() !== context)
|
|
2144
|
+
return;
|
|
2145
|
+
activeContext = undefined;
|
|
2146
|
+
delete globalThis[APPLICATION_CONTEXT_KEY];
|
|
2147
|
+
}
|
|
2051
2148
|
function requireActiveApplicationContext() {
|
|
2052
2149
|
const context = readStoredApplicationContext();
|
|
2053
2150
|
if (!context) {
|
|
@@ -3241,19 +3338,17 @@ async function resolveTenant(tenantId) {
|
|
|
3241
3338
|
if (!isTenancyEnabled()) {
|
|
3242
3339
|
return {
|
|
3243
3340
|
id: tenantId,
|
|
3244
|
-
slug: "default"
|
|
3245
|
-
plan: "free",
|
|
3246
|
-
region: "eu"
|
|
3341
|
+
slug: "default"
|
|
3247
3342
|
};
|
|
3248
3343
|
}
|
|
3249
3344
|
const rows = await repositoryConnection`
|
|
3250
|
-
SELECT id, slug
|
|
3345
|
+
SELECT id, slug
|
|
3251
3346
|
FROM tenant
|
|
3252
3347
|
WHERE id = ${tenantId}
|
|
3253
3348
|
LIMIT 1
|
|
3254
3349
|
`;
|
|
3255
3350
|
const row = rows[0];
|
|
3256
|
-
return row ? { id: row.id, slug: row.slug
|
|
3351
|
+
return row ? { id: row.id, slug: row.slug } : null;
|
|
3257
3352
|
}
|
|
3258
3353
|
|
|
3259
3354
|
// ../../src/core/auth/scimAuthMiddleware.ts
|
|
@@ -3313,151 +3408,397 @@ function createTokenAbilityChecker() {
|
|
|
3313
3408
|
}
|
|
3314
3409
|
// ../../src/core/cache/redisCacheStore.ts
|
|
3315
3410
|
var {RedisClient } = globalThis.Bun;
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
return
|
|
3321
|
-
|
|
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
|
+
`;
|
|
3322
3609
|
|
|
3323
3610
|
class RedisCacheStore {
|
|
3324
3611
|
ttlMs;
|
|
3325
3612
|
maxEntries;
|
|
3326
3613
|
client;
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3614
|
+
keys;
|
|
3615
|
+
options;
|
|
3616
|
+
fills = new Set;
|
|
3617
|
+
closed = false;
|
|
3618
|
+
constructor(redisUrl, ttlMs, maxEntries, options = {}) {
|
|
3330
3619
|
this.ttlMs = ttlMs;
|
|
3331
3620
|
this.maxEntries = maxEntries;
|
|
3332
|
-
|
|
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();
|
|
3333
3653
|
}
|
|
3334
3654
|
async get(key) {
|
|
3335
|
-
const
|
|
3336
|
-
|
|
3337
|
-
return;
|
|
3338
|
-
}
|
|
3339
|
-
return JSON.parse(raw);
|
|
3655
|
+
const result = await this.execute("get", key);
|
|
3656
|
+
return result.kind === "value" ? JSON.parse(result.payload ?? "null") : undefined;
|
|
3340
3657
|
}
|
|
3341
3658
|
async set(key, value, ttlMs) {
|
|
3342
|
-
|
|
3343
|
-
const payload = JSON.stringify(value);
|
|
3344
|
-
if (resolvedTtlMs > 0) {
|
|
3345
|
-
await this.client.psetex(this.storageKey(key), resolvedTtlMs, payload);
|
|
3346
|
-
} else {
|
|
3347
|
-
await this.client.set(this.storageKey(key), payload);
|
|
3348
|
-
}
|
|
3349
|
-
await this.enforceMaxEntries();
|
|
3659
|
+
await this.execute("set", key, { payload: this.serialize(value), ttlMs: ttlMs ?? this.ttlMs });
|
|
3350
3660
|
}
|
|
3351
3661
|
async getOrSet(key, loader, ttlMs) {
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
const
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
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.");
|
|
3368
3678
|
}
|
|
3369
3679
|
async attachTags(key, tags) {
|
|
3370
|
-
|
|
3371
|
-
return;
|
|
3372
|
-
}
|
|
3373
|
-
let tagsForKey = this.keyTags.get(key);
|
|
3374
|
-
if (!tagsForKey) {
|
|
3375
|
-
tagsForKey = new Set;
|
|
3376
|
-
this.keyTags.set(key, tagsForKey);
|
|
3377
|
-
}
|
|
3378
|
-
for (const tag of tags) {
|
|
3379
|
-
tagsForKey.add(tag);
|
|
3380
|
-
await this.client.sadd(this.tagKey(tag), key);
|
|
3381
|
-
}
|
|
3680
|
+
await this.execute("attach", key, { tags });
|
|
3382
3681
|
}
|
|
3383
3682
|
async flushTags(tags) {
|
|
3384
|
-
|
|
3385
|
-
for (const tag of tags) {
|
|
3386
|
-
const members = await this.client.smembers(this.tagKey(tag));
|
|
3387
|
-
for (const member of members) {
|
|
3388
|
-
keysToRemove.add(member);
|
|
3389
|
-
}
|
|
3390
|
-
}
|
|
3391
|
-
let removed = 0;
|
|
3392
|
-
for (const key of keysToRemove) {
|
|
3393
|
-
if (await this.invalidate(key)) {
|
|
3394
|
-
removed += 1;
|
|
3395
|
-
}
|
|
3396
|
-
}
|
|
3397
|
-
for (const tag of tags) {
|
|
3398
|
-
await this.client.del(this.tagKey(tag));
|
|
3399
|
-
}
|
|
3400
|
-
return removed;
|
|
3683
|
+
return (await this.execute("tags", "", { tags })).removed ?? 0;
|
|
3401
3684
|
}
|
|
3402
3685
|
async invalidate(key) {
|
|
3403
|
-
|
|
3404
|
-
await this.detachKeyFromTags(key);
|
|
3405
|
-
return deleted > 0;
|
|
3686
|
+
return ((await this.execute("invalidate", key)).removed ?? 0) > 0;
|
|
3406
3687
|
}
|
|
3407
3688
|
async invalidateByPrefix(prefix) {
|
|
3408
|
-
|
|
3409
|
-
let removed = 0;
|
|
3410
|
-
for (const storageKey of keys) {
|
|
3411
|
-
const key = storageKey.slice(cacheKeyPrefix().length);
|
|
3412
|
-
if (key === prefix || key.startsWith(`${prefix}?`)) {
|
|
3413
|
-
if (await this.invalidate(key)) {
|
|
3414
|
-
removed += 1;
|
|
3415
|
-
}
|
|
3416
|
-
}
|
|
3417
|
-
}
|
|
3418
|
-
return removed;
|
|
3689
|
+
return (await this.execute("prefix", prefix)).removed ?? 0;
|
|
3419
3690
|
}
|
|
3420
3691
|
async clear() {
|
|
3421
|
-
|
|
3422
|
-
if (keys.length > 0) {
|
|
3423
|
-
await this.client.del(...keys);
|
|
3424
|
-
}
|
|
3425
|
-
const tagKeys = await this.client.keys(`${cacheTagPrefix()}*`);
|
|
3426
|
-
if (tagKeys.length > 0) {
|
|
3427
|
-
await this.client.del(...tagKeys);
|
|
3428
|
-
}
|
|
3429
|
-
this.inflight.clear();
|
|
3430
|
-
this.keyTags.clear();
|
|
3692
|
+
await this.execute("clear", "");
|
|
3431
3693
|
}
|
|
3432
3694
|
async size() {
|
|
3433
|
-
|
|
3434
|
-
return keys.length;
|
|
3695
|
+
return (await this.execute("size", "")).size ?? 0;
|
|
3435
3696
|
}
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
return `${cacheTagPrefix()}${tag}`;
|
|
3441
|
-
}
|
|
3442
|
-
async detachKeyFromTags(key) {
|
|
3443
|
-
const tags = this.keyTags.get(key);
|
|
3444
|
-
if (!tags) {
|
|
3445
|
-
return;
|
|
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.");
|
|
3446
3701
|
}
|
|
3447
|
-
|
|
3448
|
-
|
|
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(() => {});
|
|
3449
3747
|
}
|
|
3450
|
-
this.keyTags.delete(key);
|
|
3451
3748
|
}
|
|
3452
|
-
|
|
3453
|
-
const
|
|
3454
|
-
if (
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
if (
|
|
3460
|
-
|
|
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);
|
|
3461
3802
|
}
|
|
3462
3803
|
}
|
|
3463
3804
|
}
|
|
@@ -3683,7 +4024,7 @@ function createCacheStore(options) {
|
|
|
3683
4024
|
if (!options.redisUrl) {
|
|
3684
4025
|
throw new Error('CACHE_DRIVER="redis" requires REDIS_URL to be set.');
|
|
3685
4026
|
}
|
|
3686
|
-
return new redisCacheStore_default(options.redisUrl, options.ttlMs, options.maxEntries);
|
|
4027
|
+
return new redisCacheStore_default(options.redisUrl, options.ttlMs, options.maxEntries, options.redis);
|
|
3687
4028
|
}
|
|
3688
4029
|
return new simpleCacheStore_default(new simpleCache_default(options.ttlMs, options.maxEntries));
|
|
3689
4030
|
}
|
|
@@ -3696,6 +4037,8 @@ class TaggedCache {
|
|
|
3696
4037
|
this.tags = tags;
|
|
3697
4038
|
}
|
|
3698
4039
|
async remember(key, callback, ttlMs) {
|
|
4040
|
+
if (this.store.getOrSetTagged)
|
|
4041
|
+
return this.store.getOrSetTagged(key, callback, this.tags, ttlMs);
|
|
3699
4042
|
const value = await this.store.getOrSet(key, callback, ttlMs);
|
|
3700
4043
|
await this.store.attachTags(key, this.tags);
|
|
3701
4044
|
return value;
|
|
@@ -3712,6 +4055,9 @@ class CacheRepository {
|
|
|
3712
4055
|
constructor(store) {
|
|
3713
4056
|
this.store = store;
|
|
3714
4057
|
}
|
|
4058
|
+
async close() {
|
|
4059
|
+
await this.store.close?.();
|
|
4060
|
+
}
|
|
3715
4061
|
async get(key) {
|
|
3716
4062
|
return this.store.get(key);
|
|
3717
4063
|
}
|
|
@@ -3759,78 +4105,8 @@ var CACHE_TAGS = {
|
|
|
3759
4105
|
careers: "careers",
|
|
3760
4106
|
offers: "offers"
|
|
3761
4107
|
};
|
|
3762
|
-
// ../../src/core/
|
|
3763
|
-
|
|
3764
|
-
services = new Map;
|
|
3765
|
-
singletonFactories = new Map;
|
|
3766
|
-
bindings = new Map;
|
|
3767
|
-
set(key, value) {
|
|
3768
|
-
this.singletonFactories.delete(key);
|
|
3769
|
-
this.bindings.delete(key);
|
|
3770
|
-
this.services.set(key, value);
|
|
3771
|
-
return value;
|
|
3772
|
-
}
|
|
3773
|
-
singleton(key, factory) {
|
|
3774
|
-
this.bindings.delete(key);
|
|
3775
|
-
this.services.delete(key);
|
|
3776
|
-
this.singletonFactories.set(key, factory);
|
|
3777
|
-
}
|
|
3778
|
-
bind(key, factory) {
|
|
3779
|
-
this.singletonFactories.delete(key);
|
|
3780
|
-
this.services.delete(key);
|
|
3781
|
-
this.bindings.set(key, factory);
|
|
3782
|
-
}
|
|
3783
|
-
get(key) {
|
|
3784
|
-
if (this.services.has(key)) {
|
|
3785
|
-
return this.services.get(key);
|
|
3786
|
-
}
|
|
3787
|
-
const singletonFactory = this.singletonFactories.get(key);
|
|
3788
|
-
if (singletonFactory) {
|
|
3789
|
-
const value = singletonFactory(this);
|
|
3790
|
-
this.services.set(key, value);
|
|
3791
|
-
return value;
|
|
3792
|
-
}
|
|
3793
|
-
const binding = this.bindings.get(key);
|
|
3794
|
-
if (binding) {
|
|
3795
|
-
return binding(this);
|
|
3796
|
-
}
|
|
3797
|
-
throw new Error(`Service "${key}" is not registered.`);
|
|
3798
|
-
}
|
|
3799
|
-
resolve(key) {
|
|
3800
|
-
return this.get(key);
|
|
3801
|
-
}
|
|
3802
|
-
make(key) {
|
|
3803
|
-
return this.resolve(key);
|
|
3804
|
-
}
|
|
3805
|
-
instance(key, value) {
|
|
3806
|
-
return this.set(key, value);
|
|
3807
|
-
}
|
|
3808
|
-
has(key) {
|
|
3809
|
-
return this.services.has(key) || this.singletonFactories.has(key) || this.bindings.has(key);
|
|
3810
|
-
}
|
|
3811
|
-
}
|
|
3812
|
-
|
|
3813
|
-
class ConfigStore {
|
|
3814
|
-
values = new Map;
|
|
3815
|
-
set(key, value) {
|
|
3816
|
-
this.values.set(key, value);
|
|
3817
|
-
return value;
|
|
3818
|
-
}
|
|
3819
|
-
get(key) {
|
|
3820
|
-
return this.values.get(key);
|
|
3821
|
-
}
|
|
3822
|
-
require(key) {
|
|
3823
|
-
if (!this.values.has(key)) {
|
|
3824
|
-
throw new Error(`Config key "${key}" is not defined.`);
|
|
3825
|
-
}
|
|
3826
|
-
return this.values.get(key);
|
|
3827
|
-
}
|
|
3828
|
-
has(key) {
|
|
3829
|
-
return this.values.has(key);
|
|
3830
|
-
}
|
|
3831
|
-
}
|
|
3832
|
-
// ../../src/core/database/baseRepository.ts
|
|
3833
|
-
init_eventBus();
|
|
4108
|
+
// ../../src/core/database/baseRepository.ts
|
|
4109
|
+
init_eventBus();
|
|
3834
4110
|
|
|
3835
4111
|
// ../../src/core/events/index.ts
|
|
3836
4112
|
init_deferredModelEvents();
|
|
@@ -3852,6 +4128,7 @@ function buildPaginationMeta(input) {
|
|
|
3852
4128
|
|
|
3853
4129
|
// ../../src/core/database/baseRepository.ts
|
|
3854
4130
|
init_connectionContext();
|
|
4131
|
+
init_dialect();
|
|
3855
4132
|
|
|
3856
4133
|
// ../../src/core/database/query.ts
|
|
3857
4134
|
init_dialect();
|
|
@@ -4672,6 +4949,7 @@ class RepositoryQuery {
|
|
|
4672
4949
|
queryOptions;
|
|
4673
4950
|
eagerLoads = [];
|
|
4674
4951
|
whereNodes = [];
|
|
4952
|
+
protectedWhere = [];
|
|
4675
4953
|
constructor(repository, whereClause = {}, queryOptions = {}) {
|
|
4676
4954
|
this.repository = repository;
|
|
4677
4955
|
this.whereClause = whereClause;
|
|
@@ -4681,10 +4959,23 @@ class RepositoryQuery {
|
|
|
4681
4959
|
if (typeof input === "function") {
|
|
4682
4960
|
const builder = new WhereBuilder;
|
|
4683
4961
|
input(builder);
|
|
4684
|
-
|
|
4962
|
+
if (builder.nodes.length > 0)
|
|
4963
|
+
this.whereNodes.push({ kind: "and", group: builder.nodes });
|
|
4685
4964
|
return this;
|
|
4686
4965
|
}
|
|
4687
|
-
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;
|
|
4688
4979
|
return this;
|
|
4689
4980
|
}
|
|
4690
4981
|
orWhere(input) {
|
|
@@ -4798,9 +5089,10 @@ class RepositoryQuery {
|
|
|
4798
5089
|
});
|
|
4799
5090
|
return this;
|
|
4800
5091
|
}
|
|
4801
|
-
withMorphTo(as, relation, repositoriesByType, options = {}) {
|
|
5092
|
+
withMorphTo(as, relation, repositoriesByType, options = {}, optionsByType) {
|
|
4802
5093
|
this.eagerLoads.push({
|
|
4803
5094
|
kind: "morphTo",
|
|
5095
|
+
morphOptions: optionsByType,
|
|
4804
5096
|
as,
|
|
4805
5097
|
relation,
|
|
4806
5098
|
repository: this.repository,
|
|
@@ -4871,6 +5163,19 @@ class RepositoryQuery {
|
|
|
4871
5163
|
}
|
|
4872
5164
|
return row[column];
|
|
4873
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
|
+
}
|
|
4874
5179
|
async attachToRows(rows) {
|
|
4875
5180
|
return await this.attach(rows);
|
|
4876
5181
|
}
|
|
@@ -4881,11 +5186,24 @@ class RepositoryQuery {
|
|
|
4881
5186
|
perPage: options.perPage
|
|
4882
5187
|
});
|
|
4883
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
|
+
}
|
|
4884
5199
|
buildOptions() {
|
|
5200
|
+
if (this.protectedWhere.length === 0) {
|
|
5201
|
+
return { ...this.queryOptions, where: this.whereClause, whereNodes: this.whereNodes };
|
|
5202
|
+
}
|
|
4885
5203
|
return {
|
|
4886
5204
|
...this.queryOptions,
|
|
4887
|
-
where:
|
|
4888
|
-
whereNodes: this.whereNodes
|
|
5205
|
+
where: {},
|
|
5206
|
+
whereNodes: [...this.protectedWhere, { kind: "and", group: [...this.whereNodes] }]
|
|
4889
5207
|
};
|
|
4890
5208
|
}
|
|
4891
5209
|
addJoin(type, left, right) {
|
|
@@ -4962,10 +5280,27 @@ class RepositoryQuery {
|
|
|
4962
5280
|
}
|
|
4963
5281
|
if (load.kind === "morphTo") {
|
|
4964
5282
|
const relation = load.relation;
|
|
4965
|
-
const
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
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
|
+
}));
|
|
4969
5304
|
return;
|
|
4970
5305
|
}
|
|
4971
5306
|
const relation = load.relation;
|
|
@@ -5087,14 +5422,20 @@ class BaseRepository {
|
|
|
5087
5422
|
if (!Number.isInteger(perPage) || perPage <= 0) {
|
|
5088
5423
|
throw new Error("Cursor page size must be a positive integer.");
|
|
5089
5424
|
}
|
|
5090
|
-
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 }];
|
|
5091
5430
|
if (cursor !== undefined) {
|
|
5431
|
+
const cursorWhere = {};
|
|
5092
5432
|
cursorWhere[cursorColumn] = direction === "asc" ? { gt: cursor } : { lt: cursor };
|
|
5433
|
+
cursorFilters.push({ kind: "and", where: cursorWhere });
|
|
5093
5434
|
}
|
|
5094
5435
|
const rows = await this.findAll({
|
|
5095
5436
|
...queryOptions,
|
|
5096
|
-
where:
|
|
5097
|
-
whereNodes,
|
|
5437
|
+
where: {},
|
|
5438
|
+
whereNodes: cursorFilters,
|
|
5098
5439
|
orderBy: { [cursorColumn]: direction },
|
|
5099
5440
|
limit: perPage + 1
|
|
5100
5441
|
});
|
|
@@ -5318,46 +5659,29 @@ class BaseRepository {
|
|
|
5318
5659
|
if (parents.length === 0) {
|
|
5319
5660
|
return indexHasManyThroughRelation(parents, [], relation);
|
|
5320
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
|
+
}
|
|
5321
5665
|
const parentIds = [...new Set(parents.map((parent) => parent[relation.localKey]))];
|
|
5322
5666
|
const throughParentKey = relation.throughParentKey ?? "__through_parent_id";
|
|
5323
5667
|
const farTable = this.table.name;
|
|
5324
5668
|
const columns = this.table.columns.map((column) => `${qualifyColumn(farTable, column)}`).join(", ");
|
|
5325
|
-
const placeholders = parentIds.map((_, index) =>
|
|
5326
|
-
const { text: extraWhere, params: extraParams } = this.buildThroughWhere(options, parentIds
|
|
5327
|
-
const
|
|
5328
|
-
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}`;
|
|
5329
5672
|
const children = await this.connection.unsafe(sql, [
|
|
5330
5673
|
...parentIds,
|
|
5331
5674
|
...extraParams
|
|
5332
5675
|
]);
|
|
5333
5676
|
return indexHasManyThroughRelation(parents, children, relation);
|
|
5334
5677
|
}
|
|
5335
|
-
|
|
5336
|
-
const
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
return ` AND ${qualified} IS NOT NULL`;
|
|
5343
|
-
}
|
|
5344
|
-
if (options.withTrashed) {
|
|
5345
|
-
return "";
|
|
5346
|
-
}
|
|
5347
|
-
return ` AND ${qualified} IS NULL`;
|
|
5348
|
-
}
|
|
5349
|
-
buildThroughWhere(options, paramOffset = 1) {
|
|
5350
|
-
const where = options.where ?? {};
|
|
5351
|
-
const entries = Object.entries(where);
|
|
5352
|
-
if (entries.length === 0) {
|
|
5353
|
-
return { text: "", params: [] };
|
|
5354
|
-
}
|
|
5355
|
-
const params = [];
|
|
5356
|
-
const clauses = entries.map(([column, value], index) => {
|
|
5357
|
-
params.push(value);
|
|
5358
|
-
return `${qualifyColumn(this.table.name, column)} = $${paramOffset + index + 1}`;
|
|
5359
|
-
});
|
|
5360
|
-
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
|
+
};
|
|
5361
5685
|
}
|
|
5362
5686
|
async loadBelongsToForParents(children, relation, parentRepository, options = {}) {
|
|
5363
5687
|
if (children.length === 0) {
|
|
@@ -5424,7 +5748,7 @@ class BaseRepository {
|
|
|
5424
5748
|
return indexBelongsToManyRelation(parents, [], [], relation);
|
|
5425
5749
|
}
|
|
5426
5750
|
const parentIds = [...new Set(parents.map((parent) => parent[relation.parentKey]))];
|
|
5427
|
-
const placeholders = parentIds.map((_, index) =>
|
|
5751
|
+
const placeholders = parentIds.map((_, index) => currentSqlDialect().placeholder(index + 1)).join(", ");
|
|
5428
5752
|
const pivotRows = await this.connection.unsafe(`SELECT * FROM ${relation.pivotTable} WHERE ${String(relation.foreignPivotKey)} IN (${placeholders})`, parentIds);
|
|
5429
5753
|
if (pivotRows.length === 0) {
|
|
5430
5754
|
return indexBelongsToManyRelation(parents, [], [], relation);
|
|
@@ -5782,7 +6106,12 @@ async function freshDatabase(db, migrations, options = {}) {
|
|
|
5782
6106
|
}
|
|
5783
6107
|
await runFresh();
|
|
5784
6108
|
}
|
|
6109
|
+
// ../../src/core/database/model.ts
|
|
6110
|
+
import { isDeepStrictEqual } from "util";
|
|
6111
|
+
init_deferredModelEvents();
|
|
6112
|
+
|
|
5785
6113
|
// ../../src/core/database/relationQuery.ts
|
|
6114
|
+
init_dialect();
|
|
5786
6115
|
function asWhere(where) {
|
|
5787
6116
|
return where;
|
|
5788
6117
|
}
|
|
@@ -5804,13 +6133,48 @@ function pluckFromQuery(query, column, keyBy) {
|
|
|
5804
6133
|
}
|
|
5805
6134
|
return keyBy === undefined ? query.pluck(column) : query.pluck(column, keyBy);
|
|
5806
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
|
+
}
|
|
5807
6171
|
|
|
5808
6172
|
class HasManyRelationQuery {
|
|
5809
6173
|
parent;
|
|
5810
6174
|
related;
|
|
5811
6175
|
relation;
|
|
5812
6176
|
kind = "hasMany";
|
|
5813
|
-
extraWhere =
|
|
6177
|
+
extraWhere = [];
|
|
5814
6178
|
extraOptions = {};
|
|
5815
6179
|
constructor(parent, related, relation) {
|
|
5816
6180
|
this.parent = parent;
|
|
@@ -5818,7 +6182,7 @@ class HasManyRelationQuery {
|
|
|
5818
6182
|
this.relation = relation;
|
|
5819
6183
|
}
|
|
5820
6184
|
where(where) {
|
|
5821
|
-
this.extraWhere
|
|
6185
|
+
this.extraWhere.push({ ...where });
|
|
5822
6186
|
return this;
|
|
5823
6187
|
}
|
|
5824
6188
|
orderBy(orderBy) {
|
|
@@ -5830,7 +6194,7 @@ class HasManyRelationQuery {
|
|
|
5830
6194
|
return this;
|
|
5831
6195
|
}
|
|
5832
6196
|
applyEagerLoad(query, alias) {
|
|
5833
|
-
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));
|
|
5834
6198
|
}
|
|
5835
6199
|
hydrateEager(row, alias) {
|
|
5836
6200
|
const value = row[alias] ?? [];
|
|
@@ -5839,24 +6203,12 @@ class HasManyRelationQuery {
|
|
|
5839
6203
|
}
|
|
5840
6204
|
toExistsClause(parentTable) {
|
|
5841
6205
|
const childTable = this.related.repository().getTable().name;
|
|
5842
|
-
const extra =
|
|
5843
|
-
const
|
|
5844
|
-
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}`;
|
|
5845
6208
|
return { sql, params: extra.params };
|
|
5846
6209
|
}
|
|
5847
6210
|
scopedQuery() {
|
|
5848
|
-
|
|
5849
|
-
let query = repository.query(asWhere({
|
|
5850
|
-
[this.relation.foreignKey]: this.parent.get(this.relation.localKey),
|
|
5851
|
-
...this.extraWhere
|
|
5852
|
-
}));
|
|
5853
|
-
if (this.extraOptions.orderBy) {
|
|
5854
|
-
query = query.orderBy(this.extraOptions.orderBy);
|
|
5855
|
-
}
|
|
5856
|
-
if (this.extraOptions.limit !== undefined) {
|
|
5857
|
-
query = query.limit(this.extraOptions.limit);
|
|
5858
|
-
}
|
|
5859
|
-
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);
|
|
5860
6212
|
}
|
|
5861
6213
|
async get() {
|
|
5862
6214
|
const rows = await this.scopedQuery().get();
|
|
@@ -5922,7 +6274,7 @@ class HasOneRelationQuery {
|
|
|
5922
6274
|
return this;
|
|
5923
6275
|
}
|
|
5924
6276
|
applyEagerLoad(query, alias) {
|
|
5925
|
-
this.inner.
|
|
6277
|
+
this.inner.applyEagerLoad(query, alias);
|
|
5926
6278
|
}
|
|
5927
6279
|
hydrateEager(row, alias) {
|
|
5928
6280
|
const hydrated = this.inner.hydrateEager(row, alias);
|
|
@@ -5963,7 +6315,7 @@ class BelongsToRelationQuery {
|
|
|
5963
6315
|
related;
|
|
5964
6316
|
relation;
|
|
5965
6317
|
kind = "belongsTo";
|
|
5966
|
-
extraWhere =
|
|
6318
|
+
extraWhere = [];
|
|
5967
6319
|
extraOptions = {};
|
|
5968
6320
|
constructor(parent, related, relation) {
|
|
5969
6321
|
this.parent = parent;
|
|
@@ -5971,7 +6323,7 @@ class BelongsToRelationQuery {
|
|
|
5971
6323
|
this.relation = relation;
|
|
5972
6324
|
}
|
|
5973
6325
|
where(where) {
|
|
5974
|
-
this.extraWhere
|
|
6326
|
+
this.extraWhere.push({ ...where });
|
|
5975
6327
|
return this;
|
|
5976
6328
|
}
|
|
5977
6329
|
orderBy(orderBy) {
|
|
@@ -5979,7 +6331,7 @@ class BelongsToRelationQuery {
|
|
|
5979
6331
|
return this;
|
|
5980
6332
|
}
|
|
5981
6333
|
applyEagerLoad(query, alias) {
|
|
5982
|
-
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));
|
|
5983
6335
|
}
|
|
5984
6336
|
hydrateEager(row, alias) {
|
|
5985
6337
|
const value = row[alias];
|
|
@@ -5987,9 +6339,8 @@ class BelongsToRelationQuery {
|
|
|
5987
6339
|
}
|
|
5988
6340
|
toExistsClause(parentTable) {
|
|
5989
6341
|
const relatedTable = this.related.repository().getTable().name;
|
|
5990
|
-
const extra =
|
|
5991
|
-
const
|
|
5992
|
-
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}`;
|
|
5993
6344
|
return { sql, params: extra.params };
|
|
5994
6345
|
}
|
|
5995
6346
|
async get() {
|
|
@@ -6014,12 +6365,7 @@ class BelongsToRelationQuery {
|
|
|
6014
6365
|
if (foreign === null || foreign === undefined) {
|
|
6015
6366
|
return null;
|
|
6016
6367
|
}
|
|
6017
|
-
|
|
6018
|
-
let query = repository.query(asWhere({ [this.relation.ownerKey]: foreign, ...this.extraWhere }));
|
|
6019
|
-
if (this.extraOptions.orderBy) {
|
|
6020
|
-
query = query.orderBy(this.extraOptions.orderBy);
|
|
6021
|
-
}
|
|
6022
|
-
return query;
|
|
6368
|
+
return relatedRead(this.related, this.parent.getRepository().getConnection(), asWhere({ [this.relation.ownerKey]: foreign }), this.extraWhere, this.extraOptions);
|
|
6023
6369
|
}
|
|
6024
6370
|
then(onfulfilled, onrejected) {
|
|
6025
6371
|
return thenGet(() => this.get(), onfulfilled, onrejected);
|
|
@@ -6041,7 +6387,7 @@ class BelongsToManyRelationQuery {
|
|
|
6041
6387
|
related;
|
|
6042
6388
|
relation;
|
|
6043
6389
|
kind = "belongsToMany";
|
|
6044
|
-
extraWhere =
|
|
6390
|
+
extraWhere = [];
|
|
6045
6391
|
extraOptions = {};
|
|
6046
6392
|
pivotValues = {};
|
|
6047
6393
|
constructor(parent, related, relation) {
|
|
@@ -6050,7 +6396,7 @@ class BelongsToManyRelationQuery {
|
|
|
6050
6396
|
this.relation = relation;
|
|
6051
6397
|
}
|
|
6052
6398
|
where(where) {
|
|
6053
|
-
this.extraWhere
|
|
6399
|
+
this.extraWhere.push({ ...where });
|
|
6054
6400
|
return this;
|
|
6055
6401
|
}
|
|
6056
6402
|
orderBy(orderBy) {
|
|
@@ -6058,7 +6404,7 @@ class BelongsToManyRelationQuery {
|
|
|
6058
6404
|
return this;
|
|
6059
6405
|
}
|
|
6060
6406
|
applyEagerLoad(query, alias) {
|
|
6061
|
-
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));
|
|
6062
6408
|
}
|
|
6063
6409
|
withPivotValues(values) {
|
|
6064
6410
|
this.pivotValues = { ...this.pivotValues, ...values };
|
|
@@ -6071,31 +6417,16 @@ class BelongsToManyRelationQuery {
|
|
|
6071
6417
|
}
|
|
6072
6418
|
toExistsClause(parentTable) {
|
|
6073
6419
|
const relatedTable = this.related.repository().getTable().name;
|
|
6074
|
-
const extra =
|
|
6075
|
-
const
|
|
6076
|
-
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}`;
|
|
6077
6422
|
return { sql, params: extra.params };
|
|
6078
6423
|
}
|
|
6079
6424
|
connection() {
|
|
6080
6425
|
return this.parent.getRepository().getConnection();
|
|
6081
6426
|
}
|
|
6082
6427
|
async get() {
|
|
6083
|
-
const
|
|
6084
|
-
const
|
|
6085
|
-
if (pivotRows.length === 0) {
|
|
6086
|
-
return [];
|
|
6087
|
-
}
|
|
6088
|
-
const relatedIds = [
|
|
6089
|
-
...new Set(pivotRows.map((row) => row[this.relation.relatedPivotKey]))
|
|
6090
|
-
];
|
|
6091
|
-
const repository = this.related.repository().withConnection(this.connection());
|
|
6092
|
-
const rows = await repository.findAll({
|
|
6093
|
-
...this.extraOptions,
|
|
6094
|
-
where: asWhere({
|
|
6095
|
-
[this.relation.relatedKey]: relatedIds,
|
|
6096
|
-
...this.extraWhere
|
|
6097
|
-
})
|
|
6098
|
-
});
|
|
6428
|
+
const query = await this.relatedQuery();
|
|
6429
|
+
const rows = query ? await query.get() : [];
|
|
6099
6430
|
return rows.map((row) => this.related.newFromRecord(row));
|
|
6100
6431
|
}
|
|
6101
6432
|
async first() {
|
|
@@ -6103,9 +6434,8 @@ class BelongsToManyRelationQuery {
|
|
|
6103
6434
|
return rows[0] ?? null;
|
|
6104
6435
|
}
|
|
6105
6436
|
async count() {
|
|
6106
|
-
const
|
|
6107
|
-
|
|
6108
|
-
return Number(rows[0]?.count ?? 0);
|
|
6437
|
+
const query = await this.relatedQuery();
|
|
6438
|
+
return query ? query.count() : 0;
|
|
6109
6439
|
}
|
|
6110
6440
|
async pluck(column, keyBy) {
|
|
6111
6441
|
return pluckFromQuery(await this.relatedQuery(), column, keyBy);
|
|
@@ -6116,21 +6446,14 @@ class BelongsToManyRelationQuery {
|
|
|
6116
6446
|
}
|
|
6117
6447
|
async relatedQuery() {
|
|
6118
6448
|
const parentId = this.parent.get(this.relation.parentKey);
|
|
6119
|
-
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]);
|
|
6120
6450
|
if (pivotRows.length === 0) {
|
|
6121
6451
|
return null;
|
|
6122
6452
|
}
|
|
6123
6453
|
const relatedIds = [
|
|
6124
6454
|
...new Set(pivotRows.map((row) => row[this.relation.relatedPivotKey]))
|
|
6125
6455
|
];
|
|
6126
|
-
|
|
6127
|
-
[this.relation.relatedKey]: relatedIds,
|
|
6128
|
-
...this.extraWhere
|
|
6129
|
-
}));
|
|
6130
|
-
if (this.extraOptions.orderBy) {
|
|
6131
|
-
query = query.orderBy(this.extraOptions.orderBy);
|
|
6132
|
-
}
|
|
6133
|
-
return query;
|
|
6456
|
+
return relatedRead(this.related, this.connection(), asWhere({ [this.relation.relatedKey]: relatedIds }), this.extraWhere, this.extraOptions);
|
|
6134
6457
|
}
|
|
6135
6458
|
then(onfulfilled, onrejected) {
|
|
6136
6459
|
return thenGet(() => this.get(), onfulfilled, onrejected);
|
|
@@ -6186,7 +6509,7 @@ class MorphManyRelationQuery {
|
|
|
6186
6509
|
related;
|
|
6187
6510
|
relation;
|
|
6188
6511
|
kind = "morphMany";
|
|
6189
|
-
extraWhere =
|
|
6512
|
+
extraWhere = [];
|
|
6190
6513
|
extraOptions = {};
|
|
6191
6514
|
constructor(parent, related, relation) {
|
|
6192
6515
|
this.parent = parent;
|
|
@@ -6194,11 +6517,11 @@ class MorphManyRelationQuery {
|
|
|
6194
6517
|
this.relation = relation;
|
|
6195
6518
|
}
|
|
6196
6519
|
where(where) {
|
|
6197
|
-
this.extraWhere
|
|
6520
|
+
this.extraWhere.push({ ...where });
|
|
6198
6521
|
return this;
|
|
6199
6522
|
}
|
|
6200
6523
|
applyEagerLoad(query, alias) {
|
|
6201
|
-
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));
|
|
6202
6525
|
}
|
|
6203
6526
|
hydrateEager(row, alias) {
|
|
6204
6527
|
const value = row[alias] ?? [];
|
|
@@ -6207,20 +6530,18 @@ class MorphManyRelationQuery {
|
|
|
6207
6530
|
}
|
|
6208
6531
|
toExistsClause(parentTable) {
|
|
6209
6532
|
const childTable = this.related.repository().getTable().name;
|
|
6210
|
-
const extra =
|
|
6211
|
-
[this.relation.morphTypeKey]: this.relation.morphType,
|
|
6533
|
+
const extra = relatedConditions(this.related, [
|
|
6534
|
+
asWhere({ [this.relation.morphTypeKey]: this.relation.morphType }),
|
|
6212
6535
|
...this.extraWhere
|
|
6213
|
-
|
|
6214
|
-
const
|
|
6215
|
-
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}`;
|
|
6216
6538
|
return { sql, params: extra.params };
|
|
6217
6539
|
}
|
|
6218
6540
|
scopedQuery() {
|
|
6219
|
-
return this.related
|
|
6541
|
+
return relatedRead(this.related, this.parent.getRepository().getConnection(), asWhere({
|
|
6220
6542
|
[this.relation.morphTypeKey]: this.relation.morphType,
|
|
6221
|
-
[this.relation.morphIdKey]: this.parent.get(this.relation.localKey)
|
|
6222
|
-
|
|
6223
|
-
}));
|
|
6543
|
+
[this.relation.morphIdKey]: this.parent.get(this.relation.localKey)
|
|
6544
|
+
}), this.extraWhere);
|
|
6224
6545
|
}
|
|
6225
6546
|
async get() {
|
|
6226
6547
|
const rows = await this.scopedQuery().get();
|
|
@@ -6307,14 +6628,14 @@ class MorphToRelationQuery {
|
|
|
6307
6628
|
relatedByType;
|
|
6308
6629
|
relation;
|
|
6309
6630
|
kind = "morphTo";
|
|
6310
|
-
extraWhere =
|
|
6631
|
+
extraWhere = [];
|
|
6311
6632
|
constructor(parent, relatedByType, relation) {
|
|
6312
6633
|
this.parent = parent;
|
|
6313
6634
|
this.relatedByType = relatedByType;
|
|
6314
6635
|
this.relation = relation;
|
|
6315
6636
|
}
|
|
6316
6637
|
where(where) {
|
|
6317
|
-
this.extraWhere
|
|
6638
|
+
this.extraWhere.push({ ...where });
|
|
6318
6639
|
return this;
|
|
6319
6640
|
}
|
|
6320
6641
|
applyEagerLoad(query, alias) {
|
|
@@ -6322,23 +6643,30 @@ class MorphToRelationQuery {
|
|
|
6322
6643
|
type,
|
|
6323
6644
|
model.repository()
|
|
6324
6645
|
]));
|
|
6325
|
-
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
|
+
])));
|
|
6326
6650
|
}
|
|
6327
6651
|
hydrateEager(row, alias) {
|
|
6328
6652
|
return row[alias];
|
|
6329
6653
|
}
|
|
6330
6654
|
toExistsClause(parentTable) {
|
|
6331
|
-
const
|
|
6332
|
-
const
|
|
6333
|
-
if (!
|
|
6334
|
-
return { sql: "SELECT 1 WHERE 1 = 0", params
|
|
6335
|
-
}
|
|
6336
|
-
const
|
|
6337
|
-
|
|
6338
|
-
|
|
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
|
+
});
|
|
6339
6667
|
return {
|
|
6340
|
-
sql: `SELECT 1
|
|
6341
|
-
params
|
|
6668
|
+
sql: `SELECT 1 WHERE ${branches.length > 0 ? branches.join(" OR ") : "1 = 0"}`,
|
|
6669
|
+
params
|
|
6342
6670
|
};
|
|
6343
6671
|
}
|
|
6344
6672
|
async get() {
|
|
@@ -6366,7 +6694,7 @@ class MorphToRelationQuery {
|
|
|
6366
6694
|
return null;
|
|
6367
6695
|
}
|
|
6368
6696
|
const table = related.repository().getTable();
|
|
6369
|
-
return related
|
|
6697
|
+
return relatedRead(related, this.parent.getRepository().getConnection(), asWhere({ [table.primaryKey]: id }), this.extraWhere);
|
|
6370
6698
|
}
|
|
6371
6699
|
then(onfulfilled, onrejected) {
|
|
6372
6700
|
return thenGet(() => this.get(), onfulfilled, onrejected);
|
|
@@ -6378,7 +6706,7 @@ class HasManyThroughRelationQuery {
|
|
|
6378
6706
|
related;
|
|
6379
6707
|
relation;
|
|
6380
6708
|
kind = "hasManyThrough";
|
|
6381
|
-
extraWhere =
|
|
6709
|
+
extraWhere = [];
|
|
6382
6710
|
extraOptions = {};
|
|
6383
6711
|
constructor(parent, related, relation) {
|
|
6384
6712
|
this.parent = parent;
|
|
@@ -6386,7 +6714,7 @@ class HasManyThroughRelationQuery {
|
|
|
6386
6714
|
this.relation = relation;
|
|
6387
6715
|
}
|
|
6388
6716
|
where(where) {
|
|
6389
|
-
this.extraWhere
|
|
6717
|
+
this.extraWhere.push({ ...where });
|
|
6390
6718
|
return this;
|
|
6391
6719
|
}
|
|
6392
6720
|
orderBy(orderBy) {
|
|
@@ -6398,7 +6726,7 @@ class HasManyThroughRelationQuery {
|
|
|
6398
6726
|
return this;
|
|
6399
6727
|
}
|
|
6400
6728
|
applyEagerLoad(query, alias) {
|
|
6401
|
-
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));
|
|
6402
6730
|
}
|
|
6403
6731
|
hydrateEager(row, alias) {
|
|
6404
6732
|
const value = row[alias] ?? [];
|
|
@@ -6407,9 +6735,8 @@ class HasManyThroughRelationQuery {
|
|
|
6407
6735
|
}
|
|
6408
6736
|
toExistsClause(parentTable) {
|
|
6409
6737
|
const farTable = this.related.repository().getTable().name;
|
|
6410
|
-
const extra =
|
|
6411
|
-
const
|
|
6412
|
-
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}`;
|
|
6413
6740
|
return { sql, params: extra.params };
|
|
6414
6741
|
}
|
|
6415
6742
|
async get() {
|
|
@@ -6434,8 +6761,7 @@ class HasManyThroughRelationQuery {
|
|
|
6434
6761
|
}
|
|
6435
6762
|
async farRows() {
|
|
6436
6763
|
return this.related.repository().withConnection(this.parent.getRepository().getConnection()).findHasManyThrough(this.parent.get(this.relation.localKey), this.relation, {
|
|
6437
|
-
...this.extraOptions
|
|
6438
|
-
where: this.extraWhere
|
|
6764
|
+
...relatedOptions(this.related, this.extraWhere, this.extraOptions)
|
|
6439
6765
|
});
|
|
6440
6766
|
}
|
|
6441
6767
|
then(onfulfilled, onrejected) {
|
|
@@ -6444,7 +6770,17 @@ class HasManyThroughRelationQuery {
|
|
|
6444
6770
|
}
|
|
6445
6771
|
|
|
6446
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
|
+
}
|
|
6447
6782
|
var modelRepositories = new WeakMap;
|
|
6783
|
+
var modelRepositoryFactories = new WeakMap;
|
|
6448
6784
|
var namedModels = new Map;
|
|
6449
6785
|
var modelGlobalScopes = new WeakMap;
|
|
6450
6786
|
var modelObservers = new WeakMap;
|
|
@@ -6535,12 +6871,54 @@ async function eagerLoadOnModels(models, paths) {
|
|
|
6535
6871
|
async function loadNested(model, path) {
|
|
6536
6872
|
await eagerLoadOnModels([model], [path]);
|
|
6537
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
|
+
}
|
|
6538
6881
|
function resolveModelRepository(model) {
|
|
6539
|
-
const
|
|
6540
|
-
if (
|
|
6541
|
-
|
|
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);
|
|
6542
6921
|
}
|
|
6543
|
-
return repository;
|
|
6544
6922
|
}
|
|
6545
6923
|
function registerModelClass(name, model) {
|
|
6546
6924
|
namedModels.set(name, model);
|
|
@@ -6591,10 +6969,26 @@ function ensureBooted(model) {
|
|
|
6591
6969
|
if (modelBooted.has(model)) {
|
|
6592
6970
|
return;
|
|
6593
6971
|
}
|
|
6972
|
+
nameModel(model);
|
|
6594
6973
|
modelBooted.add(model);
|
|
6974
|
+
const scopes = modelGlobalScopes.get(model);
|
|
6975
|
+
const observers = modelObservers.get(model);
|
|
6595
6976
|
const boot = model.boot;
|
|
6596
6977
|
if (typeof boot === "function") {
|
|
6597
|
-
|
|
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
|
+
}
|
|
6598
6992
|
}
|
|
6599
6993
|
}
|
|
6600
6994
|
function getGlobalScopes(model) {
|
|
@@ -6604,12 +6998,14 @@ var PASSWORD_HASH_PATTERN = /^\$(?:2[aby]?|argon2(?:i|d|id)?)\$/;
|
|
|
6604
6998
|
function isAlreadyHashed(value) {
|
|
6605
6999
|
return PASSWORD_HASH_PATTERN.test(value);
|
|
6606
7000
|
}
|
|
6607
|
-
function hashCastValue(value) {
|
|
7001
|
+
async function hashCastValue(value) {
|
|
7002
|
+
if (value === null || value === undefined)
|
|
7003
|
+
return value;
|
|
6608
7004
|
const plain = String(value);
|
|
6609
7005
|
if (isAlreadyHashed(plain)) {
|
|
6610
7006
|
return plain;
|
|
6611
7007
|
}
|
|
6612
|
-
return
|
|
7008
|
+
return await hashPassword(plain);
|
|
6613
7009
|
}
|
|
6614
7010
|
function hydrateValue(value, cast) {
|
|
6615
7011
|
if (value === null || value === undefined) {
|
|
@@ -6634,6 +7030,8 @@ function hydrateValue(value, cast) {
|
|
|
6634
7030
|
}
|
|
6635
7031
|
}
|
|
6636
7032
|
function dehydrateValue(value, cast) {
|
|
7033
|
+
if (cast === "hashed")
|
|
7034
|
+
return hashCastValue(value);
|
|
6637
7035
|
if (value === null || value === undefined) {
|
|
6638
7036
|
return value;
|
|
6639
7037
|
}
|
|
@@ -6649,8 +7047,6 @@ function dehydrateValue(value, cast) {
|
|
|
6649
7047
|
case "integer":
|
|
6650
7048
|
case "int":
|
|
6651
7049
|
return value === "" ? null : Number(value);
|
|
6652
|
-
case "hashed":
|
|
6653
|
-
return hashCastValue(value);
|
|
6654
7050
|
default:
|
|
6655
7051
|
return value;
|
|
6656
7052
|
}
|
|
@@ -6671,17 +7067,25 @@ function filterMassAssignable(fillable, guarded, input) {
|
|
|
6671
7067
|
return Object.fromEntries(Object.entries(input).filter(([key]) => !blocked.has(key)));
|
|
6672
7068
|
}
|
|
6673
7069
|
function applyCasts(values, casts, direction) {
|
|
6674
|
-
|
|
6675
|
-
|
|
7070
|
+
const entries = Object.entries(casts);
|
|
7071
|
+
if (entries.length === 0) {
|
|
7072
|
+
return direction === "hydrate" ? values : Promise.resolve(values);
|
|
6676
7073
|
}
|
|
6677
7074
|
const result = { ...values };
|
|
6678
|
-
|
|
6679
|
-
|
|
6680
|
-
|
|
6681
|
-
|
|
7075
|
+
if (direction === "hydrate") {
|
|
7076
|
+
for (const [key, cast] of entries) {
|
|
7077
|
+
if (key in result && cast)
|
|
7078
|
+
result[key] = hydrateValue(result[key], cast);
|
|
6682
7079
|
}
|
|
7080
|
+
return result;
|
|
6683
7081
|
}
|
|
6684
|
-
return
|
|
7082
|
+
return (async () => {
|
|
7083
|
+
for (const [key, cast] of entries) {
|
|
7084
|
+
if (key in result && cast)
|
|
7085
|
+
result[key] = await dehydrateValue(result[key], cast);
|
|
7086
|
+
}
|
|
7087
|
+
return result;
|
|
7088
|
+
})();
|
|
6685
7089
|
}
|
|
6686
7090
|
function castPluckedValue(modelClass, column, value) {
|
|
6687
7091
|
const cast = modelStatics(modelClass).$casts?.[column];
|
|
@@ -6712,6 +7116,19 @@ function applyTimestampsOnUpdate(columns, values, enabled) {
|
|
|
6712
7116
|
return result;
|
|
6713
7117
|
}
|
|
6714
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
|
+
|
|
6715
7132
|
class ModelQuery {
|
|
6716
7133
|
modelClass;
|
|
6717
7134
|
query;
|
|
@@ -6744,6 +7161,16 @@ class ModelQuery {
|
|
|
6744
7161
|
this.query.orWhere(input);
|
|
6745
7162
|
return this;
|
|
6746
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
|
+
}
|
|
6747
7174
|
orderBy(orderBy) {
|
|
6748
7175
|
this.query.orderBy(orderBy);
|
|
6749
7176
|
return this;
|
|
@@ -6892,7 +7319,7 @@ class ModelQuery {
|
|
|
6892
7319
|
}
|
|
6893
7320
|
async find(id) {
|
|
6894
7321
|
const primaryKey = resolveModelRepository(this.modelClass).getTable().primaryKey;
|
|
6895
|
-
return this.
|
|
7322
|
+
return this.whereDynamic({ [primaryKey]: id }).first();
|
|
6896
7323
|
}
|
|
6897
7324
|
async findOrFail(id, errorFactory) {
|
|
6898
7325
|
const model = await this.find(id);
|
|
@@ -6949,6 +7376,7 @@ class Model {
|
|
|
6949
7376
|
static $appends;
|
|
6950
7377
|
static $morphClass;
|
|
6951
7378
|
_exists;
|
|
7379
|
+
originalAttributes;
|
|
6952
7380
|
loadedRelations = {};
|
|
6953
7381
|
hiddenOverrides = [];
|
|
6954
7382
|
visibleOverrides = [];
|
|
@@ -6958,6 +7386,24 @@ class Model {
|
|
|
6958
7386
|
this.repository = repository;
|
|
6959
7387
|
this._exists = exists;
|
|
6960
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
|
+
});
|
|
6961
7407
|
}
|
|
6962
7408
|
getRepository() {
|
|
6963
7409
|
return this.repository;
|
|
@@ -7050,21 +7496,30 @@ class Model {
|
|
|
7050
7496
|
]);
|
|
7051
7497
|
}
|
|
7052
7498
|
static repository() {
|
|
7053
|
-
|
|
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;
|
|
7054
7509
|
}
|
|
7055
7510
|
static query() {
|
|
7056
7511
|
ensureBooted(this);
|
|
7057
7512
|
const repository = resolveModelRepository(this);
|
|
7058
|
-
|
|
7059
|
-
|
|
7060
|
-
query = scope(query);
|
|
7061
|
-
}
|
|
7062
|
-
return new ModelQuery(this, query);
|
|
7513
|
+
const scopeQuery = Model.scopeQuery;
|
|
7514
|
+
return new ModelQuery(this, scopeQuery.call(this, repository.query()));
|
|
7063
7515
|
}
|
|
7064
7516
|
static newFromRecord(record, exists = true) {
|
|
7065
7517
|
const repository = resolveModelRepository(this);
|
|
7066
7518
|
return modelStatics(this).fromRecord(record, repository, exists);
|
|
7067
7519
|
}
|
|
7520
|
+
static newFromTrustedRecord(record, exists = true) {
|
|
7521
|
+
return modelStatics(this).newFromRecord(record, exists);
|
|
7522
|
+
}
|
|
7068
7523
|
static async create(attributes, forced = {}) {
|
|
7069
7524
|
const statics = modelStatics(this);
|
|
7070
7525
|
ensureBooted(this);
|
|
@@ -7076,7 +7531,7 @@ class Model {
|
|
|
7076
7531
|
...forced
|
|
7077
7532
|
};
|
|
7078
7533
|
const withTimestamps = applyTimestampsOnCreate(table.columns, assignable, timestamps);
|
|
7079
|
-
const payload = statics.dehydrateAttributes(withTimestamps);
|
|
7534
|
+
const payload = await statics.dehydrateAttributes(withTimestamps);
|
|
7080
7535
|
const pending = statics.newFromRecord({ ...payload }, false);
|
|
7081
7536
|
if (await runObservers(pending, "saving") === false) {
|
|
7082
7537
|
throw new Error(`${this.name}.create() was cancelled by an observer.`);
|
|
@@ -7102,14 +7557,14 @@ class Model {
|
|
|
7102
7557
|
static async chunk(count, callback) {
|
|
7103
7558
|
const statics = modelStatics(this);
|
|
7104
7559
|
ensureBooted(this);
|
|
7105
|
-
await
|
|
7560
|
+
await Model.query.call(this).query.chunk(count, async (rows) => {
|
|
7106
7561
|
return await callback(rows.map((row) => statics.newFromRecord(row, true)));
|
|
7107
7562
|
});
|
|
7108
7563
|
}
|
|
7109
7564
|
static async cursorPaginate(options) {
|
|
7110
7565
|
const statics = modelStatics(this);
|
|
7111
7566
|
ensureBooted(this);
|
|
7112
|
-
const page = await
|
|
7567
|
+
const page = await Model.query.call(this).query.cursorPaginate({
|
|
7113
7568
|
perPage: options.perPage,
|
|
7114
7569
|
cursor: options.cursor
|
|
7115
7570
|
});
|
|
@@ -7217,19 +7672,23 @@ class Model {
|
|
|
7217
7672
|
return this;
|
|
7218
7673
|
}
|
|
7219
7674
|
if (updating) {
|
|
7220
|
-
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);
|
|
7221
7681
|
const record = await this.repository.updateByIdOrThrow(this.id, changes);
|
|
7222
|
-
this.
|
|
7682
|
+
this.acceptPersistedAttributes(record);
|
|
7223
7683
|
await runObservers(this, "updated");
|
|
7224
7684
|
await runObservers(this, "saved");
|
|
7225
7685
|
return this;
|
|
7226
7686
|
}
|
|
7227
7687
|
const assignable = filterMassAssignable(ModelClass.$fillable, ModelClass.$guarded, this.attributes);
|
|
7228
7688
|
const withTimestamps = applyTimestampsOnCreate(table.columns, assignable, timestamps);
|
|
7229
|
-
const payload = ModelClass.dehydrateAttributes(withTimestamps);
|
|
7689
|
+
const payload = await ModelClass.dehydrateAttributes(withTimestamps);
|
|
7230
7690
|
const record = await this.repository.create(payload);
|
|
7231
|
-
this.
|
|
7232
|
-
this._exists = true;
|
|
7691
|
+
this.acceptPersistedAttributes(record);
|
|
7233
7692
|
await runObservers(this, "created");
|
|
7234
7693
|
await runObservers(this, "saved");
|
|
7235
7694
|
return this;
|
|
@@ -7254,12 +7713,11 @@ class Model {
|
|
|
7254
7713
|
return await this.repository.forceDeleteById(this.id);
|
|
7255
7714
|
}
|
|
7256
7715
|
async restore() {
|
|
7257
|
-
const ModelClass = modelStatics(this.constructor);
|
|
7258
7716
|
const record = await this.repository.restoreById(this.id);
|
|
7259
7717
|
if (!record) {
|
|
7260
7718
|
return null;
|
|
7261
7719
|
}
|
|
7262
|
-
this.
|
|
7720
|
+
this.acceptPersistedAttributes(record);
|
|
7263
7721
|
return this;
|
|
7264
7722
|
}
|
|
7265
7723
|
async loadHasMany(as, relation, childRepository, options = {}) {
|
|
@@ -7414,14 +7872,7 @@ class Model {
|
|
|
7414
7872
|
}
|
|
7415
7873
|
function registerModelRepository(model, repository) {
|
|
7416
7874
|
modelRepositories.set(model, repository);
|
|
7417
|
-
|
|
7418
|
-
if (name) {
|
|
7419
|
-
namedModels.set(name, model);
|
|
7420
|
-
}
|
|
7421
|
-
const morphClass = model.$morphClass;
|
|
7422
|
-
if (morphClass) {
|
|
7423
|
-
namedModels.set(morphClass, model);
|
|
7424
|
-
}
|
|
7875
|
+
nameModel(model);
|
|
7425
7876
|
ensureBooted(model);
|
|
7426
7877
|
return model;
|
|
7427
7878
|
}
|
|
@@ -9146,29 +9597,95 @@ function storageFacade() {
|
|
|
9146
9597
|
return storage();
|
|
9147
9598
|
}
|
|
9148
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
|
+
}
|
|
9149
9657
|
var DEFAULT_MAX_BODY_BYTES = 1048576;
|
|
9150
9658
|
function resolveMaxBodyBytes() {
|
|
9151
9659
|
const raw = process.env.MAX_REQUEST_BODY_BYTES?.trim();
|
|
9152
9660
|
if (!raw) {
|
|
9153
9661
|
return DEFAULT_MAX_BODY_BYTES;
|
|
9154
9662
|
}
|
|
9155
|
-
const parsed = Number
|
|
9156
|
-
if (!Number.
|
|
9663
|
+
const parsed = Number(raw);
|
|
9664
|
+
if (!Number.isSafeInteger(parsed) || parsed <= 0) {
|
|
9157
9665
|
return DEFAULT_MAX_BODY_BYTES;
|
|
9158
9666
|
}
|
|
9159
9667
|
return parsed;
|
|
9160
9668
|
}
|
|
9161
9669
|
function createBodySizeLimitMiddleware(maxBytes = resolveMaxBodyBytes()) {
|
|
9670
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0)
|
|
9671
|
+
throw new RangeError("Body limit must be a positive safe integer.");
|
|
9162
9672
|
return async (request, next) => {
|
|
9163
|
-
const
|
|
9164
|
-
|
|
9165
|
-
|
|
9166
|
-
|
|
9167
|
-
|
|
9168
|
-
|
|
9169
|
-
|
|
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;
|
|
9170
9688
|
}
|
|
9171
|
-
return await next();
|
|
9172
9689
|
};
|
|
9173
9690
|
}
|
|
9174
9691
|
// ../../src/core/http/requestMetaContext.ts
|
|
@@ -10617,10 +11134,7 @@ function getRouteParams(request) {
|
|
|
10617
11134
|
}
|
|
10618
11135
|
// ../../src/core/http/routeMiddleware.ts
|
|
10619
11136
|
function withMiddleware(...middleware) {
|
|
10620
|
-
|
|
10621
|
-
return (handler) => {
|
|
10622
|
-
return wrap(handler);
|
|
10623
|
-
};
|
|
11137
|
+
return composeMiddleware(...middleware);
|
|
10624
11138
|
}
|
|
10625
11139
|
// ../../src/core/http/routeModelBinding.ts
|
|
10626
11140
|
function bindRouteModel(param, resolver, handler) {
|
|
@@ -10699,6 +11213,67 @@ function securedBindRouteModelByKey(param, resolver, authorization, handler) {
|
|
|
10699
11213
|
return response;
|
|
10700
11214
|
};
|
|
10701
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
|
+
|
|
10702
11277
|
// ../../src/core/http/throttleMiddleware.ts
|
|
10703
11278
|
import { createHash as createHash3 } from "crypto";
|
|
10704
11279
|
init_tenantContext();
|
|
@@ -10743,28 +11318,32 @@ if ttl < 0 then
|
|
|
10743
11318
|
end
|
|
10744
11319
|
return count
|
|
10745
11320
|
`;
|
|
10746
|
-
function redisThrottleKey(request, prefix, identity) {
|
|
11321
|
+
function redisThrottleKey(request, prefix, identity, tenantId = currentTenant()?.id ?? null) {
|
|
10747
11322
|
const bucket = JSON.stringify([
|
|
10748
|
-
|
|
11323
|
+
tenantId,
|
|
10749
11324
|
request.method,
|
|
10750
11325
|
currentRequestMeta().routeTemplate ?? "__unmatched__",
|
|
10751
11326
|
identity
|
|
10752
11327
|
]);
|
|
10753
11328
|
return namespacedRedisKey(`${prefix}:${createHash3("sha256").update(bucket).digest("hex")}`);
|
|
10754
11329
|
}
|
|
10755
|
-
async function consumeRedisThrottle(client, key, decaySeconds, timeoutMs = 1000) {
|
|
10756
|
-
|
|
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) {
|
|
10757
11333
|
throw new Error("Invalid throttle expiry or command timeout.");
|
|
10758
11334
|
}
|
|
11335
|
+
if (signal?.aborted)
|
|
11336
|
+
throw new Error("Throttle consumer is disposed.");
|
|
11337
|
+
let onAbort;
|
|
10759
11338
|
let timer;
|
|
10760
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
|
+
});
|
|
10761
11344
|
const result = await Promise.race([
|
|
10762
|
-
|
|
10763
|
-
|
|
10764
|
-
"1",
|
|
10765
|
-
key,
|
|
10766
|
-
String(Math.max(1, Math.ceil(decaySeconds * 1000)))
|
|
10767
|
-
]),
|
|
11345
|
+
cancelled,
|
|
11346
|
+
client.send("EVAL", [CONSUME_THROTTLE, "1", key, String(expiryMs)]),
|
|
10768
11347
|
new Promise((_resolve, reject) => {
|
|
10769
11348
|
timer = setTimeout(() => reject(new Error("Throttle store deadline exceeded.")), timeoutMs);
|
|
10770
11349
|
})
|
|
@@ -10775,11 +11354,16 @@ async function consumeRedisThrottle(client, key, decaySeconds, timeoutMs = 1000)
|
|
|
10775
11354
|
return count;
|
|
10776
11355
|
} finally {
|
|
10777
11356
|
clearTimeout(timer);
|
|
11357
|
+
if (onAbort)
|
|
11358
|
+
signal?.removeEventListener("abort", onAbort);
|
|
10778
11359
|
}
|
|
10779
11360
|
}
|
|
10780
11361
|
function createRedisThrottleConsumer(options) {
|
|
10781
11362
|
let ownedClient;
|
|
10782
|
-
|
|
11363
|
+
const controller = new AbortController;
|
|
11364
|
+
const consume = async (key, decaySeconds) => {
|
|
11365
|
+
if (controller.signal.aborted)
|
|
11366
|
+
throw new Error("Throttle consumer is disposed.");
|
|
10783
11367
|
if (!options.redisClient && !ownedClient) {
|
|
10784
11368
|
ownedClient = new RedisClient2(options.redisUrl, { autoReconnect: false, maxRetries: 0 });
|
|
10785
11369
|
}
|
|
@@ -10787,7 +11371,10 @@ function createRedisThrottleConsumer(options) {
|
|
|
10787
11371
|
if (!client)
|
|
10788
11372
|
throw new Error("Throttle client is required.");
|
|
10789
11373
|
try {
|
|
10790
|
-
|
|
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;
|
|
10791
11378
|
} catch (error) {
|
|
10792
11379
|
if (client === ownedClient) {
|
|
10793
11380
|
ownedClient.close();
|
|
@@ -10796,6 +11383,17 @@ function createRedisThrottleConsumer(options) {
|
|
|
10796
11383
|
throw error;
|
|
10797
11384
|
}
|
|
10798
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
|
+
});
|
|
10799
11397
|
}
|
|
10800
11398
|
function throttleUnavailableResponse() {
|
|
10801
11399
|
return Response.json({ error: "Rate limiting is temporarily unavailable." }, {
|
|
@@ -10803,27 +11401,52 @@ function throttleUnavailableResponse() {
|
|
|
10803
11401
|
headers: { "retry-after": "1" }
|
|
10804
11402
|
});
|
|
10805
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
|
+
}
|
|
10806
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
|
+
}
|
|
10807
11421
|
const consume = createRedisThrottleConsumer(options);
|
|
10808
11422
|
const prefix = options.keyPrefix ?? "throttle";
|
|
10809
|
-
|
|
10810
|
-
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);
|
|
10811
11432
|
let attempts;
|
|
10812
11433
|
try {
|
|
10813
11434
|
attempts = await consume(key, options.decaySeconds);
|
|
10814
11435
|
} catch {
|
|
10815
11436
|
return throttleUnavailableResponse();
|
|
10816
11437
|
}
|
|
10817
|
-
|
|
11438
|
+
if (consume.isDisposed())
|
|
11439
|
+
return throttleUnavailableResponse();
|
|
10818
11440
|
if (attempts > maxAttempts) {
|
|
10819
11441
|
return await tooManyRequestsResponse(request, "Too many requests.", options.decaySeconds);
|
|
10820
11442
|
}
|
|
10821
11443
|
return await next();
|
|
10822
11444
|
};
|
|
11445
|
+
return Object.assign(middleware, { dispose: () => consume.dispose() });
|
|
10823
11446
|
}
|
|
10824
11447
|
|
|
10825
11448
|
// ../../src/core/http/loginThrottleMiddleware.ts
|
|
10826
|
-
var
|
|
11449
|
+
var resetGeneration = 0;
|
|
10827
11450
|
function resolveLoginIdentity(request) {
|
|
10828
11451
|
return readClientIp(request) ?? "unknown";
|
|
10829
11452
|
}
|
|
@@ -10841,33 +11464,31 @@ async function resolveLoginEmail(request) {
|
|
|
10841
11464
|
return "unknown";
|
|
10842
11465
|
}
|
|
10843
11466
|
}
|
|
10844
|
-
function consumeMemoryAttempt(key, decaySeconds) {
|
|
10845
|
-
const now = Date.now();
|
|
10846
|
-
const existing = memoryLoginBuckets.get(key);
|
|
10847
|
-
if (!existing || existing.resetAt <= now) {
|
|
10848
|
-
memoryLoginBuckets.set(key, { count: 1, resetAt: now + decaySeconds * 1000 });
|
|
10849
|
-
return 1;
|
|
10850
|
-
}
|
|
10851
|
-
existing.count += 1;
|
|
10852
|
-
return existing.count;
|
|
10853
|
-
}
|
|
10854
11467
|
function createMemoryLoginThrottleMiddleware(options) {
|
|
10855
|
-
const
|
|
10856
|
-
|
|
10857
|
-
|
|
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
|
+
}
|
|
10858
11477
|
const email = await resolveLoginEmail(request);
|
|
10859
|
-
const
|
|
10860
|
-
const attempts =
|
|
10861
|
-
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)
|
|
10862
11483
|
return await tooManyRequestsResponse(request, "Too many login attempts. Try again later.", options.decaySeconds);
|
|
10863
|
-
}
|
|
10864
11484
|
return await next();
|
|
10865
11485
|
};
|
|
11486
|
+
return Object.assign(middleware, { dispose: () => store.dispose(), stats: () => store.stats() });
|
|
10866
11487
|
}
|
|
10867
11488
|
function createRedisLoginThrottleMiddleware(options) {
|
|
10868
11489
|
const consume = createRedisThrottleConsumer(options);
|
|
10869
11490
|
const prefix = options.keyPrefix ?? "login-throttle";
|
|
10870
|
-
|
|
11491
|
+
const middleware = async (request, next) => {
|
|
10871
11492
|
const identity = resolveLoginIdentity(request);
|
|
10872
11493
|
const email = await resolveLoginEmail(request);
|
|
10873
11494
|
const throttleKey = redisThrottleKey(request, prefix, JSON.stringify([identity, email]));
|
|
@@ -10877,11 +11498,14 @@ function createRedisLoginThrottleMiddleware(options) {
|
|
|
10877
11498
|
} catch {
|
|
10878
11499
|
return throttleUnavailableResponse();
|
|
10879
11500
|
}
|
|
11501
|
+
if (consume.isDisposed())
|
|
11502
|
+
return throttleUnavailableResponse();
|
|
10880
11503
|
if (attempts > options.maxAttempts) {
|
|
10881
11504
|
return await tooManyRequestsResponse(request, "Too many login attempts. Try again later.", options.decaySeconds);
|
|
10882
11505
|
}
|
|
10883
11506
|
return await next();
|
|
10884
11507
|
};
|
|
11508
|
+
return Object.assign(middleware, { dispose: () => consume.dispose() });
|
|
10885
11509
|
}
|
|
10886
11510
|
function createLoginThrottleMiddleware(options) {
|
|
10887
11511
|
const redisUrl = options.redisUrl?.trim() ?? "";
|
|
@@ -10891,35 +11515,37 @@ function createLoginThrottleMiddleware(options) {
|
|
|
10891
11515
|
return createMemoryLoginThrottleMiddleware(options);
|
|
10892
11516
|
}
|
|
10893
11517
|
function resetMemoryLoginThrottleForTests() {
|
|
10894
|
-
|
|
11518
|
+
resetGeneration++;
|
|
10895
11519
|
}
|
|
10896
11520
|
// ../../src/core/http/memoryThrottleMiddleware.ts
|
|
10897
|
-
var
|
|
11521
|
+
var resetGeneration2 = 0;
|
|
10898
11522
|
function createMemoryThrottleMiddleware(options) {
|
|
10899
|
-
const
|
|
10900
|
-
|
|
10901
|
-
|
|
10902
|
-
|
|
10903
|
-
|
|
10904
|
-
|
|
10905
|
-
|
|
10906
|
-
|
|
10907
|
-
|
|
10908
|
-
|
|
10909
|
-
|
|
10910
|
-
|
|
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();
|
|
10911
11537
|
}
|
|
10912
|
-
|
|
10913
|
-
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)
|
|
10914
11542
|
return await tooManyRequestsResponse(request, "Too many requests.", options.decaySeconds);
|
|
10915
|
-
}
|
|
10916
11543
|
return await next();
|
|
10917
11544
|
};
|
|
11545
|
+
return Object.assign(middleware, { dispose: () => store.dispose(), stats: () => store.stats() });
|
|
10918
11546
|
}
|
|
10919
11547
|
function resetMemoryThrottleForTests() {
|
|
10920
|
-
|
|
10921
|
-
buckets.clear();
|
|
10922
|
-
}
|
|
11548
|
+
resetGeneration2++;
|
|
10923
11549
|
}
|
|
10924
11550
|
// ../../src/core/metrics/prometheus.ts
|
|
10925
11551
|
var HTTP_DURATION_BUCKETS_MS = [5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 1e4];
|
|
@@ -11189,8 +11815,6 @@ function createRequireWebAuthMiddleware(auth) {
|
|
|
11189
11815
|
}
|
|
11190
11816
|
// ../../src/core/http/scimThrottleMiddleware.ts
|
|
11191
11817
|
import { createHash as createHash4 } from "crypto";
|
|
11192
|
-
var {RedisClient: RedisClient3 } = globalThis.Bun;
|
|
11193
|
-
var memoryBuckets = new Map;
|
|
11194
11818
|
function resolveScimIdentity(request) {
|
|
11195
11819
|
const authorization = request.headers.get("authorization") ?? "";
|
|
11196
11820
|
const presented = authorization.startsWith("Bearer ") ? authorization.slice("Bearer ".length).trim() : "";
|
|
@@ -11199,40 +11823,44 @@ function resolveScimIdentity(request) {
|
|
|
11199
11823
|
}
|
|
11200
11824
|
return readClientIp(request) ?? "unknown";
|
|
11201
11825
|
}
|
|
11202
|
-
function throttleFromMemory(key, maxAttempts, decaySeconds) {
|
|
11203
|
-
const now = Date.now();
|
|
11204
|
-
const existing = memoryBuckets.get(key);
|
|
11205
|
-
if (!existing || existing.resetAt <= now) {
|
|
11206
|
-
memoryBuckets.set(key, { count: 1, resetAt: now + decaySeconds * 1000 });
|
|
11207
|
-
return null;
|
|
11208
|
-
}
|
|
11209
|
-
existing.count += 1;
|
|
11210
|
-
if (existing.count > maxAttempts) {
|
|
11211
|
-
return Response.json({ error: "Too many SCIM requests." }, { status: 429, headers: { "retry-after": String(decaySeconds) } });
|
|
11212
|
-
}
|
|
11213
|
-
return null;
|
|
11214
|
-
}
|
|
11215
11826
|
function createScimThrottleMiddleware(options) {
|
|
11216
|
-
|
|
11217
|
-
|
|
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();
|
|
11218
11837
|
const identity = resolveScimIdentity(request);
|
|
11219
|
-
const
|
|
11220
|
-
|
|
11221
|
-
|
|
11222
|
-
|
|
11223
|
-
|
|
11224
|
-
|
|
11225
|
-
|
|
11226
|
-
return Response.json({ error: "Too many SCIM requests." }, { status: 429, headers: { "retry-after": String(options.decaySeconds) } });
|
|
11227
|
-
}
|
|
11228
|
-
return await next();
|
|
11229
|
-
}
|
|
11230
|
-
const limited = throttleFromMemory(key, options.maxAttempts, options.decaySeconds);
|
|
11231
|
-
if (limited) {
|
|
11232
|
-
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();
|
|
11233
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) } });
|
|
11234
11852
|
return await next();
|
|
11235
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
|
+
});
|
|
11236
11864
|
}
|
|
11237
11865
|
// ../../src/core/http/securityHeadersMiddleware.ts
|
|
11238
11866
|
function createSecurityHeadersMiddleware(options = {}) {
|
|
@@ -11400,47 +12028,150 @@ class WebFormRequest {
|
|
|
11400
12028
|
}
|
|
11401
12029
|
}
|
|
11402
12030
|
// ../../src/core/lifecycle/gracefulShutdown.ts
|
|
11403
|
-
|
|
11404
|
-
|
|
11405
|
-
|
|
11406
|
-
|
|
11407
|
-
|
|
11408
|
-
return
|
|
11409
|
-
shutdownHandlers.delete(name);
|
|
11410
|
-
};
|
|
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;
|
|
11411
12037
|
}
|
|
11412
|
-
|
|
11413
|
-
|
|
11414
|
-
|
|
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
|
+
};
|
|
11415
12062
|
}
|
|
11416
|
-
|
|
11417
|
-
|
|
11418
|
-
|
|
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
|
+
})();
|
|
11419
12126
|
try {
|
|
11420
|
-
await
|
|
11421
|
-
|
|
11422
|
-
|
|
11423
|
-
|
|
12127
|
+
await Promise.race([sequence, deadline]);
|
|
12128
|
+
} finally {
|
|
12129
|
+
clearTimeout(timer);
|
|
12130
|
+
this.handlers.clear();
|
|
11424
12131
|
}
|
|
12132
|
+
return { successful: errors.length === 0 && !timedOut, timedOut, errors: [...errors] };
|
|
11425
12133
|
}
|
|
11426
12134
|
}
|
|
11427
|
-
|
|
11428
|
-
|
|
11429
|
-
|
|
11430
|
-
|
|
11431
|
-
|
|
11432
|
-
|
|
11433
|
-
|
|
11434
|
-
|
|
11435
|
-
|
|
11436
|
-
|
|
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);
|
|
11437
12155
|
});
|
|
11438
|
-
}
|
|
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;
|
|
11439
12170
|
}
|
|
11440
12171
|
function resetGracefulShutdownForTests() {
|
|
11441
|
-
|
|
11442
|
-
|
|
11443
|
-
|
|
12172
|
+
for (const remove of [...installations.values()])
|
|
12173
|
+
remove();
|
|
12174
|
+
defaultLifecycle = new LifecycleCoordinator;
|
|
11444
12175
|
}
|
|
11445
12176
|
// ../../src/core/logging/requestLoggingMiddleware.ts
|
|
11446
12177
|
function createRequestLoggingMiddleware() {
|
|
@@ -11719,12 +12450,37 @@ class SyncQueue {
|
|
|
11719
12450
|
}
|
|
11720
12451
|
|
|
11721
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
|
+
}
|
|
11722
12472
|
async dispatch(job, payload) {
|
|
11723
|
-
|
|
11724
|
-
|
|
11725
|
-
|
|
11726
|
-
|
|
11727
|
-
|
|
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;
|
|
11728
12484
|
}
|
|
11729
12485
|
}
|
|
11730
12486
|
function createQueue(driver) {
|
|
@@ -11845,7 +12601,7 @@ async function runQueueJob(envelope, failedJobs, options = {}) {
|
|
|
11845
12601
|
}
|
|
11846
12602
|
|
|
11847
12603
|
// ../../src/core/queue/redisQueue.ts
|
|
11848
|
-
var {RedisClient:
|
|
12604
|
+
var {RedisClient: RedisClient3 } = globalThis.Bun;
|
|
11849
12605
|
function queueListKey() {
|
|
11850
12606
|
return namespacedRedisKey("queue:default");
|
|
11851
12607
|
}
|
|
@@ -12041,7 +12797,10 @@ async function reclaimExpiredQueueReservations(client, queueKeys = QUEUE_KEYS) {
|
|
|
12041
12797
|
class RedisQueue {
|
|
12042
12798
|
client;
|
|
12043
12799
|
constructor(redisUrl) {
|
|
12044
|
-
this.client = new
|
|
12800
|
+
this.client = new RedisClient3(redisUrl);
|
|
12801
|
+
}
|
|
12802
|
+
close() {
|
|
12803
|
+
this.client.close();
|
|
12045
12804
|
}
|
|
12046
12805
|
async dispatch(job, payload) {
|
|
12047
12806
|
const name = jobRegistry.resolveName(job);
|
|
@@ -12070,7 +12829,7 @@ class QueueWorker {
|
|
|
12070
12829
|
this.failedJobs = failedJobs;
|
|
12071
12830
|
this.timeoutSeconds = timeoutSeconds;
|
|
12072
12831
|
this.queueKeys = queueKeys;
|
|
12073
|
-
this.client = new
|
|
12832
|
+
this.client = new RedisClient3(redisUrl);
|
|
12074
12833
|
}
|
|
12075
12834
|
requestStop() {
|
|
12076
12835
|
this.stopping = true;
|
|
@@ -12160,14 +12919,16 @@ class QueueWorker {
|
|
|
12160
12919
|
}
|
|
12161
12920
|
|
|
12162
12921
|
// ../../src/core/queue/resilientQueue.ts
|
|
12163
|
-
class ResilientQueue {
|
|
12922
|
+
class ResilientQueue extends AsyncQueue {
|
|
12164
12923
|
failedJobs;
|
|
12165
12924
|
asyncDispatch;
|
|
12166
12925
|
constructor(failedJobs, asyncDispatch = false) {
|
|
12926
|
+
super();
|
|
12167
12927
|
this.failedJobs = failedJobs;
|
|
12168
12928
|
this.asyncDispatch = asyncDispatch;
|
|
12169
12929
|
}
|
|
12170
12930
|
async dispatch(job, payload) {
|
|
12931
|
+
this.assertOpen();
|
|
12171
12932
|
const name = jobRegistry.resolveName(job);
|
|
12172
12933
|
if (!name) {
|
|
12173
12934
|
throw new Error("Job is not registered with the queue worker registry.");
|
|
@@ -12178,11 +12939,9 @@ class ResilientQueue {
|
|
|
12178
12939
|
attempts: 0
|
|
12179
12940
|
};
|
|
12180
12941
|
if (this.asyncDispatch) {
|
|
12181
|
-
|
|
12182
|
-
|
|
12183
|
-
|
|
12184
|
-
});
|
|
12185
|
-
}, 0);
|
|
12942
|
+
this.enqueue(() => runQueueJob(envelope, this.failedJobs), (error) => {
|
|
12943
|
+
console.error("[ResilientQueue] Job failed:", error);
|
|
12944
|
+
});
|
|
12186
12945
|
return;
|
|
12187
12946
|
}
|
|
12188
12947
|
await runQueueJob(envelope, this.failedJobs);
|
|
@@ -12211,9 +12970,9 @@ function createQueueWorker(redisUrl, failedJobs = createFailedJobService()) {
|
|
|
12211
12970
|
return new QueueWorker(redisUrl, failedJobs);
|
|
12212
12971
|
}
|
|
12213
12972
|
// ../../src/core/queue/queueMetrics.ts
|
|
12214
|
-
var {RedisClient:
|
|
12973
|
+
var {RedisClient: RedisClient4 } = globalThis.Bun;
|
|
12215
12974
|
async function readRedisQueueDepth(redisUrl) {
|
|
12216
|
-
const client = new
|
|
12975
|
+
const client = new RedisClient4(redisUrl);
|
|
12217
12976
|
const [high, defaultQueue, low] = await Promise.all([
|
|
12218
12977
|
client.llen(QUEUE_HIGH_KEY),
|
|
12219
12978
|
client.llen(QUEUE_LIST_KEY),
|
|
@@ -12261,6 +13020,149 @@ async function collectQueueMetrics() {
|
|
|
12261
13020
|
failedCount
|
|
12262
13021
|
};
|
|
12263
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
|
+
|
|
12264
13166
|
// ../../src/core/scheduler/osCron.ts
|
|
12265
13167
|
function parseScheduleExpression(expression, from = new Date) {
|
|
12266
13168
|
return Bun.cron.parse(expression, from);
|
|
@@ -12291,11 +13193,25 @@ function assertScheduleExpression(expression) {
|
|
|
12291
13193
|
}
|
|
12292
13194
|
|
|
12293
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
|
+
|
|
12294
13203
|
class Schedule {
|
|
12295
13204
|
tasks = [];
|
|
12296
13205
|
command(expression, name, run) {
|
|
12297
13206
|
assertScheduleExpression(expression);
|
|
12298
|
-
|
|
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 });
|
|
12299
13215
|
return this;
|
|
12300
13216
|
}
|
|
12301
13217
|
dueTasks(now = new Date) {
|
|
@@ -12306,12 +13222,125 @@ class Schedule {
|
|
|
12306
13222
|
}
|
|
12307
13223
|
}
|
|
12308
13224
|
var appSchedule = new Schedule;
|
|
12309
|
-
|
|
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.");
|
|
12310
13235
|
const due = schedule.dueTasks(now);
|
|
12311
|
-
|
|
12312
|
-
|
|
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();
|
|
12313
13343
|
}
|
|
12314
|
-
return due.length;
|
|
12315
13344
|
}
|
|
12316
13345
|
// ../../src/core/security/publicReads.ts
|
|
12317
13346
|
function isPublicReadsEnabled() {
|
|
@@ -12433,7 +13462,7 @@ init_tenantContext();
|
|
|
12433
13462
|
init_tenantDatabaseScope();
|
|
12434
13463
|
|
|
12435
13464
|
// ../../src/core/tenant/tenantMiddleware.ts
|
|
12436
|
-
import { createHash as
|
|
13465
|
+
import { createHash as createHash7 } from "crypto";
|
|
12437
13466
|
init_repositoryConnection();
|
|
12438
13467
|
init_databaseTenantContext();
|
|
12439
13468
|
init_tenancyConfig();
|
|
@@ -12441,9 +13470,7 @@ init_tenantContext();
|
|
|
12441
13470
|
init_tenantDatabaseScope();
|
|
12442
13471
|
var DEFAULT_TENANT = {
|
|
12443
13472
|
id: 1,
|
|
12444
|
-
slug: "default"
|
|
12445
|
-
plan: "free",
|
|
12446
|
-
region: "eu"
|
|
13473
|
+
slug: "default"
|
|
12447
13474
|
};
|
|
12448
13475
|
async function resolveUserTenantId(userId) {
|
|
12449
13476
|
if (!isTenancyEnabled()) {
|
|
@@ -12460,9 +13487,9 @@ async function resolveUserTenantId(userId) {
|
|
|
12460
13487
|
});
|
|
12461
13488
|
}
|
|
12462
13489
|
function auditChecksum(payload) {
|
|
12463
|
-
return
|
|
13490
|
+
return createHash7("sha256").update(JSON.stringify(payload)).digest("hex");
|
|
12464
13491
|
}
|
|
12465
|
-
async function resolveTenantForRequest(request) {
|
|
13492
|
+
async function resolveTenantForRequest(request, resolver) {
|
|
12466
13493
|
const user = currentAuthUser();
|
|
12467
13494
|
const headerValue = request.headers.get("x-tenant-id")?.trim();
|
|
12468
13495
|
const parsedHeader = headerValue !== undefined && headerValue.length > 0 ? Number.parseInt(headerValue, 10) : Number.NaN;
|
|
@@ -12474,20 +13501,20 @@ async function resolveTenantForRequest(request) {
|
|
|
12474
13501
|
if (Number.isInteger(parsedHeader) && parsedHeader > 0 && parsedHeader !== userTenantId) {
|
|
12475
13502
|
throw new ForbiddenError("Tenant header does not match your account.");
|
|
12476
13503
|
}
|
|
12477
|
-
const memberTenant = await
|
|
13504
|
+
const memberTenant = await resolver(userTenantId);
|
|
12478
13505
|
if (memberTenant) {
|
|
12479
13506
|
return memberTenant;
|
|
12480
13507
|
}
|
|
12481
13508
|
return DEFAULT_TENANT;
|
|
12482
13509
|
}
|
|
12483
13510
|
if (Number.isInteger(parsedHeader) && parsedHeader > 0) {
|
|
12484
|
-
const headerTenant = await
|
|
13511
|
+
const headerTenant = await resolver(parsedHeader);
|
|
12485
13512
|
if (headerTenant) {
|
|
12486
13513
|
return headerTenant;
|
|
12487
13514
|
}
|
|
12488
13515
|
return DEFAULT_TENANT;
|
|
12489
13516
|
}
|
|
12490
|
-
const adminTenant = await
|
|
13517
|
+
const adminTenant = await resolver(userTenantId);
|
|
12491
13518
|
if (adminTenant) {
|
|
12492
13519
|
return adminTenant;
|
|
12493
13520
|
}
|
|
@@ -12496,13 +13523,14 @@ async function resolveTenantForRequest(request) {
|
|
|
12496
13523
|
}
|
|
12497
13524
|
const headerTenantId = Number.isInteger(parsedHeader) && parsedHeader > 0 ? parsedHeader : null;
|
|
12498
13525
|
const tenantId = isPublicReadsEnabled() && headerTenantId !== null ? headerTenantId : DEFAULT_TENANT.id;
|
|
12499
|
-
const guestTenant = await
|
|
13526
|
+
const guestTenant = await resolver(tenantId);
|
|
12500
13527
|
if (guestTenant) {
|
|
12501
13528
|
return guestTenant;
|
|
12502
13529
|
}
|
|
12503
13530
|
return DEFAULT_TENANT;
|
|
12504
13531
|
}
|
|
12505
|
-
function createTenantMiddleware() {
|
|
13532
|
+
function createTenantMiddleware(options = {}) {
|
|
13533
|
+
const resolver = options.resolveTenant ?? resolveTenant;
|
|
12506
13534
|
return async (request, next) => {
|
|
12507
13535
|
const pathname = new URL(request.url).pathname;
|
|
12508
13536
|
if (pathname.startsWith("/scim/")) {
|
|
@@ -12512,7 +13540,16 @@ function createTenantMiddleware() {
|
|
|
12512
13540
|
return await runWithTenant(DEFAULT_TENANT, next);
|
|
12513
13541
|
}
|
|
12514
13542
|
try {
|
|
12515
|
-
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
|
+
});
|
|
12516
13553
|
return await runWithTenantDatabase(tenant, async () => {
|
|
12517
13554
|
return await next();
|
|
12518
13555
|
});
|
|
@@ -12534,99 +13571,185 @@ function runWithTraceContext(context, callback) {
|
|
|
12534
13571
|
function currentTraceId() {
|
|
12535
13572
|
return traceContextStorage.getStore()?.traceId ?? null;
|
|
12536
13573
|
}
|
|
13574
|
+
// ../../src/core/tracing/tracingMiddleware.ts
|
|
13575
|
+
import { ROOT_CONTEXT, SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
|
|
13576
|
+
|
|
12537
13577
|
// ../../src/core/tracing/otel.ts
|
|
12538
|
-
import {
|
|
12539
|
-
|
|
12540
|
-
|
|
12541
|
-
}
|
|
12542
|
-
|
|
12543
|
-
|
|
12544
|
-
|
|
12545
|
-
|
|
12546
|
-
|
|
12547
|
-
|
|
12548
|
-
|
|
12549
|
-
|
|
12550
|
-
|
|
12551
|
-
|
|
12552
|
-
|
|
12553
|
-
|
|
12554
|
-
|
|
12555
|
-
|
|
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;
|
|
12556
13596
|
}
|
|
12557
|
-
|
|
12558
|
-
const
|
|
12559
|
-
|
|
13597
|
+
function ensureContextManager() {
|
|
13598
|
+
const key = Symbol.for("@getstrata/otel-context-installed");
|
|
13599
|
+
const state = globalThis;
|
|
13600
|
+
if (state[key])
|
|
12560
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`;
|
|
12561
13628
|
}
|
|
12562
|
-
|
|
12563
|
-
|
|
12564
|
-
|
|
12565
|
-
|
|
12566
|
-
|
|
12567
|
-
|
|
12568
|
-
|
|
12569
|
-
|
|
12570
|
-
|
|
12571
|
-
|
|
12572
|
-
|
|
12573
|
-
|
|
12574
|
-
|
|
12575
|
-
|
|
12576
|
-
|
|
12577
|
-
|
|
12578
|
-
|
|
12579
|
-
|
|
12580
|
-
|
|
12581
|
-
|
|
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
|
|
12582
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
|
+
};
|
|
12583
13697
|
}
|
|
12584
13698
|
|
|
12585
13699
|
// ../../src/core/tracing/tracingMiddleware.ts
|
|
12586
|
-
|
|
12587
|
-
|
|
12588
|
-
|
|
12589
|
-
|
|
12590
|
-
|
|
12591
|
-
|
|
12592
|
-
|
|
12593
|
-
|
|
12594
|
-
|
|
12595
|
-
|
|
12596
|
-
|
|
12597
|
-
|
|
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) {
|
|
12598
13716
|
return async (request, next) => {
|
|
12599
|
-
const
|
|
12600
|
-
const
|
|
12601
|
-
const
|
|
12602
|
-
const
|
|
12603
|
-
|
|
12604
|
-
|
|
12605
|
-
|
|
12606
|
-
|
|
12607
|
-
|
|
12608
|
-
|
|
12609
|
-
|
|
12610
|
-
|
|
12611
|
-
|
|
12612
|
-
|
|
12613
|
-
|
|
12614
|
-
|
|
12615
|
-
|
|
12616
|
-
|
|
12617
|
-
|
|
12618
|
-
|
|
12619
|
-
|
|
12620
|
-
|
|
12621
|
-
|
|
12622
|
-
|
|
12623
|
-
|
|
12624
|
-
|
|
12625
|
-
|
|
12626
|
-
|
|
12627
|
-
|
|
12628
|
-
})
|
|
12629
|
-
|
|
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
|
+
}));
|
|
12630
13753
|
};
|
|
12631
13754
|
}
|
|
12632
13755
|
// ../../src/core/validation/rules.ts
|
|
@@ -13037,6 +14160,7 @@ export {
|
|
|
13037
14160
|
JobRegistry,
|
|
13038
14161
|
JsonResource,
|
|
13039
14162
|
JwtGuard,
|
|
14163
|
+
LifecycleCoordinator,
|
|
13040
14164
|
LocalStorageDriver,
|
|
13041
14165
|
LogMailDriver,
|
|
13042
14166
|
MAX_PER_PAGE,
|
|
@@ -13062,12 +14186,14 @@ export {
|
|
|
13062
14186
|
QueueWorker,
|
|
13063
14187
|
RLS_HELPER_SQL,
|
|
13064
14188
|
RedisQueue,
|
|
14189
|
+
RedisSchedulerLeaseStore,
|
|
13065
14190
|
RepositoryQuery,
|
|
13066
14191
|
ResilientQueue,
|
|
13067
14192
|
ResourceCollection,
|
|
13068
14193
|
SamlProvider,
|
|
13069
14194
|
SamlServiceProvider,
|
|
13070
14195
|
Schedule,
|
|
14196
|
+
SchedulerLeaseLostError,
|
|
13071
14197
|
Schema,
|
|
13072
14198
|
ServiceContainer,
|
|
13073
14199
|
SqlOutbox,
|
|
@@ -13081,6 +14207,7 @@ export {
|
|
|
13081
14207
|
WebFormRequest,
|
|
13082
14208
|
WhereBuilder,
|
|
13083
14209
|
absoluteTemporarySignedUrl,
|
|
14210
|
+
acquireTracingRuntime,
|
|
13084
14211
|
apiPrefix,
|
|
13085
14212
|
appCookieName,
|
|
13086
14213
|
appDevSecret,
|
|
@@ -13111,6 +14238,7 @@ export {
|
|
|
13111
14238
|
bindBunSql,
|
|
13112
14239
|
bindDatabaseConnection,
|
|
13113
14240
|
bindRouteModel,
|
|
14241
|
+
bootModels,
|
|
13114
14242
|
buildAdvancedWhereClause,
|
|
13115
14243
|
buildCountQuery,
|
|
13116
14244
|
buildDeleteByIdQuery,
|
|
@@ -13130,6 +14258,7 @@ export {
|
|
|
13130
14258
|
buildUpdateQuery,
|
|
13131
14259
|
buildWhereClause,
|
|
13132
14260
|
cache,
|
|
14261
|
+
clearActiveApplicationContext,
|
|
13133
14262
|
collectQueueMetrics,
|
|
13134
14263
|
compileBlueprint,
|
|
13135
14264
|
completePasswordLogin,
|
|
@@ -13179,12 +14308,14 @@ export {
|
|
|
13179
14308
|
createScimAuthMiddleware,
|
|
13180
14309
|
createScimThrottleMiddleware,
|
|
13181
14310
|
createSecurityHeadersMiddleware,
|
|
14311
|
+
createServiceToken,
|
|
13182
14312
|
createSqliteConnection,
|
|
13183
14313
|
createStorageDriver,
|
|
13184
14314
|
createTenantMiddleware,
|
|
13185
14315
|
createThrottleMiddleware,
|
|
13186
14316
|
createTokenAbilityChecker,
|
|
13187
14317
|
createTracingMiddleware,
|
|
14318
|
+
createTracingRuntime,
|
|
13188
14319
|
createTrackedJob,
|
|
13189
14320
|
createValidateSignatureMiddleware,
|
|
13190
14321
|
createdResponse,
|
|
@@ -13197,6 +14328,7 @@ export {
|
|
|
13197
14328
|
currentTenant,
|
|
13198
14329
|
currentTenantId,
|
|
13199
14330
|
currentTraceId,
|
|
14331
|
+
defineModel,
|
|
13200
14332
|
defineTable,
|
|
13201
14333
|
dehydrateValue,
|
|
13202
14334
|
dialectFor,
|
|
@@ -13228,6 +14360,7 @@ export {
|
|
|
13228
14360
|
getNamedConnection,
|
|
13229
14361
|
getQueryParams,
|
|
13230
14362
|
getRouteParams,
|
|
14363
|
+
getTracingRuntime,
|
|
13231
14364
|
grammarForDriver,
|
|
13232
14365
|
guestCanViewResource,
|
|
13233
14366
|
hasActiveDatabaseConnection,
|
|
@@ -13311,7 +14444,6 @@ export {
|
|
|
13311
14444
|
qualifyColumn,
|
|
13312
14445
|
queue,
|
|
13313
14446
|
quoteIdentifier,
|
|
13314
|
-
rateLimitMultiplierForPlan,
|
|
13315
14447
|
readBunRequestCookie,
|
|
13316
14448
|
readClientIp,
|
|
13317
14449
|
readOptionalEnum,
|