@getstrata/core 2.2.31 → 2.2.33
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 +8 -0
- package/dist/entries/audit/exportAuditLogs.js +14 -0
- package/dist/entries/auth/basicAuthGuard.js +14 -0
- package/dist/entries/auth/scimAuthMiddleware.js +2330 -194
- package/dist/entries/jobs/exportAuditLogsJob.js +14 -0
- package/dist/entries/queue/queueMetrics.js +14 -0
- package/dist/entries/tenant/databaseTenantContext.js +14 -0
- package/dist/entries/tenant/tenantDatabaseScope.js +14 -0
- package/dist/index.js +1977 -1982
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3660,1836 +3660,1034 @@ function resolveScimTenantFromToken(token) {
|
|
|
3660
3660
|
return null;
|
|
3661
3661
|
}
|
|
3662
3662
|
|
|
3663
|
-
// ../../src/core/
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
};
|
|
3672
|
-
}
|
|
3673
|
-
const rows = await repositoryConnection`
|
|
3674
|
-
SELECT id, slug
|
|
3675
|
-
FROM tenant
|
|
3676
|
-
WHERE id = ${tenantId}
|
|
3677
|
-
LIMIT 1
|
|
3678
|
-
`;
|
|
3679
|
-
const row = rows[0];
|
|
3680
|
-
return row ? { id: row.id, slug: row.slug } : null;
|
|
3663
|
+
// ../../src/core/database/baseRepository.ts
|
|
3664
|
+
init_eventBus();
|
|
3665
|
+
|
|
3666
|
+
// ../../src/core/events/index.ts
|
|
3667
|
+
init_deferredModelEvents();
|
|
3668
|
+
init_eventBus();
|
|
3669
|
+
function modelEventName(tableName, action) {
|
|
3670
|
+
return `${tableName}.${action}`;
|
|
3681
3671
|
}
|
|
3682
3672
|
|
|
3683
|
-
// ../../src/core/
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
return
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
const token = authorization.slice("Bearer ".length).trim();
|
|
3692
|
-
const tenantId = resolveScimTenantFromToken(token);
|
|
3693
|
-
if (tenantId === null) {
|
|
3694
|
-
return jsonScimError("Invalid SCIM bearer token.", 401);
|
|
3695
|
-
}
|
|
3696
|
-
const tenant = await resolveTenant(tenantId);
|
|
3697
|
-
if (!tenant) {
|
|
3698
|
-
return jsonScimError("SCIM tenant not found.", 401);
|
|
3699
|
-
}
|
|
3700
|
-
return await runWithTenantDatabase(tenant, async () => {
|
|
3701
|
-
bindDatabaseConnection(getActiveDatabaseConnection(getDefaultDatabasePool()));
|
|
3702
|
-
try {
|
|
3703
|
-
return await next();
|
|
3704
|
-
} finally {
|
|
3705
|
-
resetBoundDatabaseConnection();
|
|
3706
|
-
}
|
|
3707
|
-
});
|
|
3673
|
+
// ../../src/core/pagination/index.ts
|
|
3674
|
+
function buildPaginationMeta(input) {
|
|
3675
|
+
const lastPage = Math.max(1, Math.ceil(input.total / input.perPage));
|
|
3676
|
+
return {
|
|
3677
|
+
page: input.page,
|
|
3678
|
+
per_page: input.perPage,
|
|
3679
|
+
total: input.total,
|
|
3680
|
+
last_page: lastPage
|
|
3708
3681
|
};
|
|
3709
3682
|
}
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3683
|
+
|
|
3684
|
+
// ../../src/core/database/baseRepository.ts
|
|
3685
|
+
init_connectionContext();
|
|
3686
|
+
init_dialect();
|
|
3687
|
+
|
|
3688
|
+
// ../../src/core/database/keyset.ts
|
|
3689
|
+
init_dialect();
|
|
3690
|
+
var KEYSET_ALIAS_PREFIX = "__strata_keyset_";
|
|
3691
|
+
function validateKeyset(table, options) {
|
|
3692
|
+
if (!Number.isInteger(options.perPage) || options.perPage < 1 || options.perPage > 1000)
|
|
3693
|
+
throw new RangeError("Keyset page size must be between 1 and 1000.");
|
|
3694
|
+
if (!Array.isArray(options.orderBy) || options.orderBy.length < 1 || options.orderBy.length > 8)
|
|
3695
|
+
throw new TypeError("Keyset ordering requires between 1 and 8 columns.");
|
|
3696
|
+
if (table.columns.some((column) => column.toLowerCase().startsWith(KEYSET_ALIAS_PREFIX)))
|
|
3697
|
+
throw new TypeError("Table columns conflict with reserved keyset aliases.");
|
|
3698
|
+
const seen = new Set;
|
|
3699
|
+
const order = Array.from(options.orderBy).map(({ column, direction }) => {
|
|
3700
|
+
if (!table.columns.includes(column) || seen.has(column) || !["asc", "desc"].includes(direction))
|
|
3701
|
+
throw new TypeError("Invalid or duplicate keyset ordering column/direction.");
|
|
3702
|
+
seen.add(column);
|
|
3703
|
+
return { column, direction };
|
|
3718
3704
|
});
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3705
|
+
if (order.at(-1)?.column !== table.primaryKey)
|
|
3706
|
+
throw new TypeError("Keyset ordering must end with the table primary key.");
|
|
3707
|
+
if (options.cursor !== undefined) {
|
|
3708
|
+
const cursor = options.cursor;
|
|
3709
|
+
if (cursor?.version !== 1 || !Array.isArray(cursor.order) || cursor.order.length !== order.length || !Array.isArray(cursor.values) || cursor.values.length !== order.length || Array.from(cursor.order).some((item, i) => !item || item.column !== order[i]?.column || item.direction !== order[i]?.direction) || Array.from(cursor.values).some((value) => typeof value !== "string" || value.length > 4096))
|
|
3710
|
+
throw new TypeError("Invalid keyset cursor or ordering mismatch.");
|
|
3724
3711
|
}
|
|
3725
|
-
|
|
3726
|
-
return abilities.includes("*") || abilities.includes(ability);
|
|
3712
|
+
return order;
|
|
3727
3713
|
}
|
|
3728
|
-
function
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3714
|
+
function keysetBoundary(order, cursor) {
|
|
3715
|
+
if (!cursor)
|
|
3716
|
+
return [];
|
|
3717
|
+
if (currentSqlDialect().driver === "pgsql" && order.length > 1 && order.every((item) => item.direction === order[0]?.direction)) {
|
|
3718
|
+
return [
|
|
3719
|
+
{
|
|
3720
|
+
kind: "and",
|
|
3721
|
+
compareRow: {
|
|
3722
|
+
columns: order.map((item) => item.column),
|
|
3723
|
+
operator: order[0]?.direction === "asc" ? "gt" : "lt",
|
|
3724
|
+
values: [...cursor.values]
|
|
3725
|
+
}
|
|
3734
3726
|
}
|
|
3727
|
+
];
|
|
3728
|
+
}
|
|
3729
|
+
const branches = order.map((item, i) => {
|
|
3730
|
+
const where = {};
|
|
3731
|
+
order.slice(0, i).forEach((prefix, j) => {
|
|
3732
|
+
where[prefix.column] = cursor.values[j];
|
|
3733
|
+
});
|
|
3734
|
+
where[item.column] = { [item.direction === "asc" ? "gt" : "lt"]: cursor.values[i] };
|
|
3735
|
+
return { kind: i === 0 ? "and" : "or", where };
|
|
3736
|
+
});
|
|
3737
|
+
return [{ kind: "and", group: branches }];
|
|
3738
|
+
}
|
|
3739
|
+
|
|
3740
|
+
// ../../src/core/database/query.ts
|
|
3741
|
+
init_dialect();
|
|
3742
|
+
function quoteIdentifier(identifier) {
|
|
3743
|
+
return currentSqlDialect().quoteIdentifier(identifier);
|
|
3744
|
+
}
|
|
3745
|
+
function returningSuffix(columns) {
|
|
3746
|
+
return currentSqlDialect().returningClause(columns);
|
|
3747
|
+
}
|
|
3748
|
+
function qualifyColumn(tableName, column) {
|
|
3749
|
+
return `${quoteIdentifier(tableName)}.${quoteIdentifier(column)}`;
|
|
3750
|
+
}
|
|
3751
|
+
function resolveQualifiedColumn(defaultTable, columnName) {
|
|
3752
|
+
if (columnName.includes(".")) {
|
|
3753
|
+
const [table, column] = columnName.split(".", 2);
|
|
3754
|
+
if (!table || !column) {
|
|
3755
|
+
throw new Error(`Invalid qualified column: ${columnName}`);
|
|
3735
3756
|
}
|
|
3736
|
-
|
|
3757
|
+
return qualifyColumn(table, column);
|
|
3758
|
+
}
|
|
3759
|
+
return qualifyColumn(defaultTable, columnName);
|
|
3737
3760
|
}
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
for i, key in ipairs(KEYS) do
|
|
3743
|
-
local kind = redis.call('TYPE', key).ok
|
|
3744
|
-
if kind ~= 'none' and kind ~= expected[i] then return redis.error_reply('Unexpected cache key type') end
|
|
3745
|
-
end
|
|
3746
|
-
-- Redis eviction can remove one container without its companions. Never read
|
|
3747
|
-
-- orphaned values or leases after losing the expiry/order/generation indexes.
|
|
3748
|
-
local entries = redis.call('HLEN', KEYS[1])
|
|
3749
|
-
local order = redis.call('ZCARD', KEYS[3])
|
|
3750
|
-
if redis.call('ZCARD', KEYS[2]) ~= order or entries ~= (order > 0 and order + 1 or 0) then
|
|
3751
|
-
redis.call('DEL', KEYS[1], KEYS[2], KEYS[3])
|
|
3752
|
-
end
|
|
3753
|
-
if redis.call('HLEN', KEYS[4]) ~= redis.call('ZCARD', KEYS[5]) then
|
|
3754
|
-
redis.call('DEL', KEYS[4], KEYS[5])
|
|
3755
|
-
end
|
|
3756
|
-
local r = cjson.decode(ARGV[1])
|
|
3757
|
-
local time = redis.call('TIME')
|
|
3758
|
-
local now = tonumber(time[1]) * 1000 + tonumber(time[2]) / 1000
|
|
3759
|
-
local function removeEntry(key)
|
|
3760
|
-
local removed = redis.call('HDEL', KEYS[1], 'e:' .. key)
|
|
3761
|
-
redis.call('ZREM', KEYS[2], key)
|
|
3762
|
-
redis.call('ZREM', KEYS[3], key)
|
|
3763
|
-
return removed
|
|
3764
|
-
end
|
|
3765
|
-
local function removeMeta(field)
|
|
3766
|
-
redis.call('HDEL', KEYS[4], field)
|
|
3767
|
-
redis.call('ZREM', KEYS[5], field)
|
|
3768
|
-
end
|
|
3769
|
-
for _, key in ipairs(redis.call('ZRANGEBYSCORE', KEYS[2], '-inf', now, 'LIMIT', 0, r.capacity)) do removeEntry(key) end
|
|
3770
|
-
for _, field in ipairs(redis.call('ZRANGEBYSCORE', KEYS[5], '-inf', now, 'LIMIT', 0, r.metadataLimit)) do removeMeta(field) end
|
|
3771
|
-
local function expireContainers()
|
|
3772
|
-
local last = redis.call('ZREVRANGE', KEYS[2], 0, 0, 'WITHSCORES')
|
|
3773
|
-
if #last == 0 then
|
|
3774
|
-
redis.call('DEL', KEYS[1], KEYS[2], KEYS[3])
|
|
3775
|
-
elseif last[2] == 'inf' then
|
|
3776
|
-
for i = 1, 3 do redis.call('PERSIST', KEYS[i]) end
|
|
3777
|
-
else
|
|
3778
|
-
local ttl = math.max(1, math.ceil(tonumber(last[2]) - now))
|
|
3779
|
-
for i = 1, 3 do redis.call('PEXPIRE', KEYS[i], ttl) end
|
|
3780
|
-
end
|
|
3781
|
-
for i = 4, 5 do redis.call('PEXPIRE', KEYS[i], r.metadataTtlMs) end
|
|
3782
|
-
end
|
|
3783
|
-
local function response(value)
|
|
3784
|
-
expireContainers()
|
|
3785
|
-
return cjson.encode(value)
|
|
3786
|
-
end
|
|
3787
|
-
local function addTags(existing, added)
|
|
3788
|
-
local seen = {}
|
|
3789
|
-
local result = {}
|
|
3790
|
-
for _, tags in ipairs({existing, added}) do
|
|
3791
|
-
for _, tag in ipairs(tags) do
|
|
3792
|
-
if not seen[tag] then seen[tag] = true; table.insert(result, tag) end
|
|
3793
|
-
end
|
|
3794
|
-
end
|
|
3795
|
-
if #result > 32 then error('Cache entries support at most 32 tags') end
|
|
3796
|
-
return result
|
|
3797
|
-
end
|
|
3798
|
-
local function touch(key)
|
|
3799
|
-
local sequence = redis.call('HINCRBY', KEYS[1], '_sequence', 1)
|
|
3800
|
-
redis.call('ZADD', KEYS[3], sequence, key)
|
|
3801
|
-
end
|
|
3802
|
-
local function read(key, tags)
|
|
3803
|
-
local raw = redis.call('HGET', KEYS[1], 'e:' .. key)
|
|
3804
|
-
if not raw then return nil end
|
|
3805
|
-
local entry = cjson.decode(raw)
|
|
3806
|
-
if #tags > 0 then
|
|
3807
|
-
entry.tags = addTags(entry.tags, tags)
|
|
3808
|
-
redis.call('HSET', KEYS[1], 'e:' .. key, cjson.encode(entry))
|
|
3809
|
-
end
|
|
3810
|
-
touch(key)
|
|
3811
|
-
return entry.payload
|
|
3812
|
-
end
|
|
3813
|
-
local function write(key, payload, tags)
|
|
3814
|
-
local entry = {payload=payload, tags=tags}
|
|
3815
|
-
redis.call('HSET', KEYS[1], 'e:' .. key, cjson.encode(entry))
|
|
3816
|
-
redis.call('ZADD', KEYS[2], r.ttlMs == 0 and '+inf' or now + r.ttlMs, key)
|
|
3817
|
-
touch(key)
|
|
3818
|
-
local overflow = redis.call('ZCARD', KEYS[3]) - r.capacity
|
|
3819
|
-
if overflow > 0 then
|
|
3820
|
-
for _, victim in ipairs(redis.call('ZRANGE', KEYS[3], 0, overflow - 1)) do removeEntry(victim) end
|
|
3821
|
-
end
|
|
3822
|
-
end
|
|
3823
|
-
local leaseField = 'l:' .. r.key
|
|
3824
|
-
local function lease()
|
|
3825
|
-
local raw = redis.call('HGET', KEYS[4], leaseField)
|
|
3826
|
-
if not raw then return nil end
|
|
3827
|
-
local value = cjson.decode(raw)
|
|
3828
|
-
if value.expires <= now or value.deadline <= now then removeMeta(leaseField); return nil end
|
|
3829
|
-
return value
|
|
3830
|
-
end
|
|
3831
|
-
local function metadataSpace(count)
|
|
3832
|
-
if redis.call('HLEN', KEYS[4]) + count > r.metadataLimit then
|
|
3833
|
-
-- Pressure sheds generation/lease state. Every old owner is fenced; cached
|
|
3834
|
-
-- entries remain valid because invalidation removes matching entries atomically.
|
|
3835
|
-
redis.call('DEL', KEYS[4], KEYS[5])
|
|
3836
|
-
end
|
|
3837
|
-
end
|
|
3838
|
-
local function putMeta(field, value, expires)
|
|
3839
|
-
redis.call('HSET', KEYS[4], field, value)
|
|
3840
|
-
redis.call('ZADD', KEYS[5], expires, field)
|
|
3841
|
-
end
|
|
3842
|
-
local function scopes()
|
|
3843
|
-
local fields = {}
|
|
3844
|
-
for _, tag in ipairs(r.tags) do table.insert(fields, 'g:tag:' .. tag) end
|
|
3845
|
-
for _, prefix in ipairs(r.prefixes) do table.insert(fields, 'g:prefix:' .. prefix) end
|
|
3846
|
-
return fields
|
|
3847
|
-
end
|
|
3848
|
-
local function matches(key, tags)
|
|
3849
|
-
if r.op == 'invalidate' then return key == r.key end
|
|
3850
|
-
if r.op == 'prefix' then return key == r.key or string.sub(key, 1, #r.key + 1) == r.key .. '?' end
|
|
3851
|
-
if r.op == 'tags' then
|
|
3852
|
-
for _, existing in ipairs(tags) do
|
|
3853
|
-
for _, wanted in ipairs(r.tags) do if wanted == existing then return true end end
|
|
3854
|
-
end
|
|
3855
|
-
end
|
|
3856
|
-
return false
|
|
3857
|
-
end
|
|
3858
|
-
if r.op == 'clear' then
|
|
3859
|
-
redis.call('DEL', unpack(KEYS))
|
|
3860
|
-
return cjson.encode({removed=0})
|
|
3861
|
-
elseif r.op == 'get' then
|
|
3862
|
-
local payload = read(r.key, r.tags)
|
|
3863
|
-
return response(payload and {kind='value', payload=payload} or {kind='missing'})
|
|
3864
|
-
elseif r.op == 'set' then
|
|
3865
|
-
local old = redis.call('HGET', KEYS[1], 'e:' .. r.key)
|
|
3866
|
-
local tags = old and cjson.decode(old).tags or {}
|
|
3867
|
-
write(r.key, r.payload, tags)
|
|
3868
|
-
removeMeta(leaseField)
|
|
3869
|
-
return response({written=true})
|
|
3870
|
-
elseif r.op == 'attach' then
|
|
3871
|
-
read(r.key, r.tags)
|
|
3872
|
-
return response({written=true})
|
|
3873
|
-
elseif r.op == 'size' then
|
|
3874
|
-
return response({size=redis.call('ZCARD', KEYS[3])})
|
|
3875
|
-
elseif r.op == 'tags' or r.op == 'prefix' or r.op == 'invalidate' then
|
|
3876
|
-
local generationFields = {}
|
|
3877
|
-
if r.op == 'tags' then
|
|
3878
|
-
for _, tag in ipairs(r.tags) do table.insert(generationFields, 'g:tag:' .. tag) end
|
|
3879
|
-
elseif r.op == 'prefix' then table.insert(generationFields, 'g:prefix:' .. r.key) end
|
|
3880
|
-
metadataSpace(#generationFields)
|
|
3881
|
-
for _, field in ipairs(generationFields) do putMeta(field, r.owner, now + r.metadataTtlMs) end
|
|
3882
|
-
local removed = 0
|
|
3883
|
-
for _, key in ipairs(redis.call('ZRANGE', KEYS[3], 0, -1)) do
|
|
3884
|
-
local raw = redis.call('HGET', KEYS[1], 'e:' .. key)
|
|
3885
|
-
if raw and matches(key, cjson.decode(raw).tags) then removed = removed + removeEntry(key) end
|
|
3886
|
-
end
|
|
3887
|
-
for _, field in ipairs(redis.call('HKEYS', KEYS[4])) do
|
|
3888
|
-
if string.sub(field, 1, 2) == 'l:' then
|
|
3889
|
-
local value = cjson.decode(redis.call('HGET', KEYS[4], field))
|
|
3890
|
-
if matches(string.sub(field, 3), value.tags) then removeMeta(field) end
|
|
3891
|
-
end
|
|
3892
|
-
end
|
|
3893
|
-
return response({removed=removed})
|
|
3894
|
-
elseif r.op == 'claim' then
|
|
3895
|
-
local payload = read(r.key, r.tags)
|
|
3896
|
-
if payload then return response({kind='value', payload=payload}) end
|
|
3897
|
-
local current = lease()
|
|
3898
|
-
if current then
|
|
3899
|
-
local shared = addTags(current.tags, r.tags)
|
|
3900
|
-
if #shared == #current.tags then return response({kind='wait'}) end
|
|
3901
|
-
-- A newly requested tag must not inherit a fill started before that tag's
|
|
3902
|
-
-- invalidation. Restart with the union of tags and a new owner.
|
|
3903
|
-
r.tags = shared
|
|
3904
|
-
removeMeta(leaseField)
|
|
3905
|
-
end
|
|
3906
|
-
local active = 0
|
|
3907
|
-
for _, field in ipairs(redis.call('HKEYS', KEYS[4])) do if string.sub(field, 1, 2) == 'l:' then active = active + 1 end end
|
|
3908
|
-
if active >= r.capacity then return response({kind='wait'}) end
|
|
3909
|
-
local fields = scopes()
|
|
3910
|
-
metadataSpace(#fields + 1)
|
|
3911
|
-
local generations = {}
|
|
3912
|
-
for _, field in ipairs(fields) do
|
|
3913
|
-
local token = redis.call('HGET', KEYS[4], field) or r.owner
|
|
3914
|
-
putMeta(field, token, now + r.metadataTtlMs)
|
|
3915
|
-
generations[field] = token
|
|
3916
|
-
end
|
|
3917
|
-
local value = {owner=r.owner, tags=r.tags, generations=generations, deadline=now + r.fillTimeoutMs, expires=now + r.leaseMs}
|
|
3918
|
-
putMeta(leaseField, cjson.encode(value), value.expires)
|
|
3919
|
-
return response({kind='owner'})
|
|
3920
|
-
elseif r.op == 'renew' or r.op == 'publish' or r.op == 'release' then
|
|
3921
|
-
local current = lease()
|
|
3922
|
-
if not current or current.owner ~= r.owner then return response({owned=false}) end
|
|
3923
|
-
if r.op == 'release' then removeMeta(leaseField); return response({owned=true}) end
|
|
3924
|
-
for field, token in pairs(current.generations) do
|
|
3925
|
-
if redis.call('HGET', KEYS[4], field) ~= token then removeMeta(leaseField); return response({owned=false}) end
|
|
3926
|
-
end
|
|
3927
|
-
if r.op == 'renew' then
|
|
3928
|
-
current.expires = math.min(current.deadline, now + r.leaseMs)
|
|
3929
|
-
putMeta(leaseField, cjson.encode(current), current.expires)
|
|
3930
|
-
else
|
|
3931
|
-
write(r.key, r.payload, current.tags)
|
|
3932
|
-
removeMeta(leaseField)
|
|
3933
|
-
end
|
|
3934
|
-
return response({owned=true})
|
|
3935
|
-
end
|
|
3936
|
-
return redis.error_reply('Unknown cache operation')
|
|
3937
|
-
`;
|
|
3938
|
-
|
|
3939
|
-
class RedisCacheStore {
|
|
3940
|
-
redisUrl;
|
|
3941
|
-
ttlMs;
|
|
3942
|
-
maxEntries;
|
|
3943
|
-
client;
|
|
3944
|
-
keys;
|
|
3945
|
-
options;
|
|
3946
|
-
fills = new Set;
|
|
3947
|
-
closed = false;
|
|
3948
|
-
constructor(redisUrl, ttlMs, maxEntries, options = {}) {
|
|
3949
|
-
this.redisUrl = redisUrl;
|
|
3950
|
-
this.ttlMs = ttlMs;
|
|
3951
|
-
this.maxEntries = maxEntries;
|
|
3952
|
-
if (!Number.isSafeInteger(ttlMs) || ttlMs < 0)
|
|
3953
|
-
throw new RangeError("ttlMs must be a non-negative safe integer.");
|
|
3954
|
-
if (!Number.isInteger(maxEntries) || maxEntries < 1 || maxEntries > 1e4)
|
|
3955
|
-
throw new RangeError("maxEntries must be between 1 and 10000.");
|
|
3956
|
-
this.options = {
|
|
3957
|
-
commandTimeoutMs: 5000,
|
|
3958
|
-
leaseMs: 1e4,
|
|
3959
|
-
fillTimeoutMs: 30000,
|
|
3960
|
-
waitTimeoutMs: 35000,
|
|
3961
|
-
pollMs: 25,
|
|
3962
|
-
metadataTtlMs: 60000,
|
|
3963
|
-
metadataLimit: 4096,
|
|
3964
|
-
...options
|
|
3965
|
-
};
|
|
3966
|
-
for (const [key, value] of Object.entries(this.options))
|
|
3967
|
-
if (!Number.isSafeInteger(value) || value < 1)
|
|
3968
|
-
throw new RangeError(`${key} must be a positive safe integer.`);
|
|
3969
|
-
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)
|
|
3970
|
-
throw new RangeError("Invalid Redis cache lease, deadline or metadata bounds.");
|
|
3971
|
-
const namespace = namespacedRedisKey("cache:v3:");
|
|
3972
|
-
const slot = new Bun.CryptoHasher("sha256").update(namespace).digest("hex");
|
|
3973
|
-
this.keys = ["entries", "expiry", "order", "metadata", "metadata-expiry"].map((name) => `${namespace}{${slot}}:${name}`);
|
|
3974
|
-
this.client = this.createClient();
|
|
3761
|
+
function parseQualifiedColumn(reference) {
|
|
3762
|
+
const [table, column] = reference.split(".", 2);
|
|
3763
|
+
if (!table || !column) {
|
|
3764
|
+
throw new Error(`Join columns must be qualified as table.column: ${reference}`);
|
|
3975
3765
|
}
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3766
|
+
return { table, column };
|
|
3767
|
+
}
|
|
3768
|
+
function normalizeDirection(direction = "ASC") {
|
|
3769
|
+
return direction.toUpperCase() === "DESC" ? "DESC" : "ASC";
|
|
3770
|
+
}
|
|
3771
|
+
function isQueryOperator(value) {
|
|
3772
|
+
return value !== null && !Array.isArray(value) && !(value instanceof Date) && typeof value === "object";
|
|
3773
|
+
}
|
|
3774
|
+
function pushParam(values, value) {
|
|
3775
|
+
values.push(value);
|
|
3776
|
+
return currentSqlDialect().placeholder(values.length);
|
|
3777
|
+
}
|
|
3778
|
+
var SUPPORTED_OPERATORS = new Set([
|
|
3779
|
+
"eq",
|
|
3780
|
+
"ne",
|
|
3781
|
+
"in",
|
|
3782
|
+
"notIn",
|
|
3783
|
+
"gt",
|
|
3784
|
+
"gte",
|
|
3785
|
+
"lt",
|
|
3786
|
+
"lte",
|
|
3787
|
+
"isNull",
|
|
3788
|
+
"ilike",
|
|
3789
|
+
"tsMatch"
|
|
3790
|
+
]);
|
|
3791
|
+
function buildInClause(column, values, params, negated = false) {
|
|
3792
|
+
if (values.length === 0) {
|
|
3793
|
+
return negated ? "1 = 1" : "1 = 0";
|
|
3981
3794
|
}
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3795
|
+
const placeholders = values.map((value) => pushParam(params, value)).join(", ");
|
|
3796
|
+
return `${column}${negated ? " NOT" : ""} IN (${placeholders})`;
|
|
3797
|
+
}
|
|
3798
|
+
function buildOperatorClauses(column, operator, params) {
|
|
3799
|
+
const clauses = [];
|
|
3800
|
+
const unsupported = Object.keys(operator).filter((key) => !SUPPORTED_OPERATORS.has(key));
|
|
3801
|
+
if (unsupported.length > 0) {
|
|
3802
|
+
throw new Error(`Unsupported query operator(s) for ${column}: ${unsupported.join(", ")}. Supported: ${[...SUPPORTED_OPERATORS].join(", ")}.`);
|
|
3988
3803
|
}
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
return result.kind === "value" ? JSON.parse(result.payload ?? "null") : undefined;
|
|
3804
|
+
if (operator.isNull === true) {
|
|
3805
|
+
clauses.push(`${column} IS NULL`);
|
|
3992
3806
|
}
|
|
3993
|
-
|
|
3994
|
-
|
|
3807
|
+
if (operator.isNull === false) {
|
|
3808
|
+
clauses.push(`${column} IS NOT NULL`);
|
|
3995
3809
|
}
|
|
3996
|
-
|
|
3997
|
-
|
|
3810
|
+
if (operator.eq !== undefined) {
|
|
3811
|
+
if (operator.eq === null) {
|
|
3812
|
+
clauses.push(`${column} IS NULL`);
|
|
3813
|
+
} else {
|
|
3814
|
+
clauses.push(`${column} = ${pushParam(params, operator.eq)}`);
|
|
3815
|
+
}
|
|
3998
3816
|
}
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
if (claim.kind === "value")
|
|
4005
|
-
return JSON.parse(claim.payload ?? "null");
|
|
4006
|
-
if (claim.kind === "owner")
|
|
4007
|
-
return this.fill(key, owner, loader, ttlMs);
|
|
4008
|
-
if (performance.now() >= end)
|
|
4009
|
-
throw new Error("Redis cache fill wait deadline exceeded.");
|
|
4010
|
-
await Bun.sleep(this.options.pollMs);
|
|
3817
|
+
if (operator.ne !== undefined) {
|
|
3818
|
+
if (operator.ne === null) {
|
|
3819
|
+
clauses.push(`${column} IS NOT NULL`);
|
|
3820
|
+
} else {
|
|
3821
|
+
clauses.push(`${column} <> ${pushParam(params, operator.ne)}`);
|
|
4011
3822
|
}
|
|
4012
|
-
throw new Error("Redis cache store is closed.");
|
|
4013
3823
|
}
|
|
4014
|
-
|
|
4015
|
-
|
|
3824
|
+
if (operator.in !== undefined) {
|
|
3825
|
+
clauses.push(buildInClause(column, operator.in, params));
|
|
4016
3826
|
}
|
|
4017
|
-
|
|
4018
|
-
|
|
3827
|
+
if (operator.notIn !== undefined) {
|
|
3828
|
+
clauses.push(buildInClause(column, operator.notIn, params, true));
|
|
4019
3829
|
}
|
|
4020
|
-
|
|
4021
|
-
|
|
3830
|
+
if (operator.gt !== undefined) {
|
|
3831
|
+
clauses.push(`${column} > ${pushParam(params, operator.gt)}`);
|
|
4022
3832
|
}
|
|
4023
|
-
|
|
4024
|
-
|
|
3833
|
+
if (operator.gte !== undefined) {
|
|
3834
|
+
clauses.push(`${column} >= ${pushParam(params, operator.gte)}`);
|
|
4025
3835
|
}
|
|
4026
|
-
|
|
4027
|
-
|
|
3836
|
+
if (operator.lt !== undefined) {
|
|
3837
|
+
clauses.push(`${column} < ${pushParam(params, operator.lt)}`);
|
|
4028
3838
|
}
|
|
4029
|
-
|
|
4030
|
-
|
|
3839
|
+
if (operator.lte !== undefined) {
|
|
3840
|
+
clauses.push(`${column} <= ${pushParam(params, operator.lte)}`);
|
|
4031
3841
|
}
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
this.fills.add(controller);
|
|
4039
|
-
let done = false;
|
|
4040
|
-
let renewing = false;
|
|
4041
|
-
const renew = setInterval(() => {
|
|
4042
|
-
if (renewing || done)
|
|
4043
|
-
return;
|
|
4044
|
-
renewing = true;
|
|
4045
|
-
this.execute("renew", key, { owner }).then((result) => {
|
|
4046
|
-
if (!result.owned)
|
|
4047
|
-
clearInterval(renew);
|
|
4048
|
-
}).catch((error) => {
|
|
4049
|
-
if (!done)
|
|
4050
|
-
controller.abort(error);
|
|
4051
|
-
}).finally(() => {
|
|
4052
|
-
renewing = false;
|
|
4053
|
-
});
|
|
4054
|
-
}, Math.max(10, Math.floor(this.options.leaseMs / 3)));
|
|
4055
|
-
const timeout = setTimeout(() => controller.abort(new Error("Redis cache fill deadline exceeded.")), this.options.fillTimeoutMs);
|
|
4056
|
-
let rejectAbort;
|
|
4057
|
-
const aborted = new Promise((_, reject) => {
|
|
4058
|
-
rejectAbort = () => reject(controller.signal.reason);
|
|
4059
|
-
controller.signal.addEventListener("abort", rejectAbort, { once: true });
|
|
4060
|
-
});
|
|
4061
|
-
try {
|
|
4062
|
-
const value = await Promise.race([
|
|
4063
|
-
Promise.resolve().then(() => loader(controller.signal)),
|
|
4064
|
-
aborted
|
|
4065
|
-
]);
|
|
4066
|
-
controller.signal.throwIfAborted();
|
|
4067
|
-
done = true;
|
|
4068
|
-
clearInterval(renew);
|
|
4069
|
-
await this.execute("publish", key, {
|
|
4070
|
-
owner,
|
|
4071
|
-
payload: this.serialize(value),
|
|
4072
|
-
ttlMs: ttlMs ?? this.ttlMs
|
|
4073
|
-
});
|
|
4074
|
-
return value;
|
|
4075
|
-
} finally {
|
|
4076
|
-
done = true;
|
|
4077
|
-
clearInterval(renew);
|
|
4078
|
-
clearTimeout(timeout);
|
|
4079
|
-
controller.signal.removeEventListener("abort", rejectAbort);
|
|
4080
|
-
this.fills.delete(controller);
|
|
4081
|
-
if (!this.closed)
|
|
4082
|
-
await this.execute("release", key, { owner }).catch(() => {});
|
|
4083
|
-
}
|
|
4084
|
-
}
|
|
4085
|
-
serialize(value) {
|
|
4086
|
-
const payload = JSON.stringify(value);
|
|
4087
|
-
if (typeof payload !== "string")
|
|
4088
|
-
throw new TypeError("Redis cache values must be JSON serializable.");
|
|
4089
|
-
return payload;
|
|
4090
|
-
}
|
|
4091
|
-
async execute(op, key, extra = {}) {
|
|
4092
|
-
if (this.closed)
|
|
4093
|
-
throw new Error("Redis cache store is closed.");
|
|
4094
|
-
if (Buffer.byteLength(key) > 4096)
|
|
4095
|
-
throw new RangeError("Invalid Redis cache identity.");
|
|
4096
|
-
const tags = Array.from(new Set(extra.tags ?? []));
|
|
4097
|
-
if (tags.length > 32 || tags.some((tag) => typeof tag !== "string" || Buffer.byteLength(tag) > 1024))
|
|
4098
|
-
throw new RangeError("Redis cache supports at most 32 tags of 1024 bytes each.");
|
|
4099
|
-
const prefixes = [key];
|
|
4100
|
-
for (let i = 0;i < key.length; i++)
|
|
4101
|
-
if (key[i] === "?")
|
|
4102
|
-
prefixes.push(key.slice(0, i));
|
|
4103
|
-
if (prefixes.length > 33)
|
|
4104
|
-
throw new RangeError("Redis cache identities support at most 32 query separators.");
|
|
4105
|
-
const ttlMs = extra.ttlMs ?? this.ttlMs;
|
|
4106
|
-
if (typeof ttlMs !== "number" || !Number.isSafeInteger(ttlMs) || ttlMs < 0)
|
|
4107
|
-
throw new RangeError("ttlMs must be a non-negative safe integer.");
|
|
4108
|
-
if (!this.client)
|
|
4109
|
-
this.client = this.createClient();
|
|
4110
|
-
const client = this.client;
|
|
4111
|
-
const retire = (reason) => {
|
|
4112
|
-
if (this.client !== client)
|
|
4113
|
-
return;
|
|
4114
|
-
this.client = undefined;
|
|
4115
|
-
for (const fill of this.fills)
|
|
4116
|
-
fill.abort(reason);
|
|
4117
|
-
client.close();
|
|
4118
|
-
};
|
|
4119
|
-
let timer;
|
|
4120
|
-
const timeout = new Promise((_, reject) => {
|
|
4121
|
-
timer = setTimeout(() => {
|
|
4122
|
-
const error = new Error("Redis cache command deadline exceeded.");
|
|
4123
|
-
reject(error);
|
|
4124
|
-
retire(error);
|
|
4125
|
-
}, this.options.commandTimeoutMs);
|
|
4126
|
-
});
|
|
4127
|
-
try {
|
|
4128
|
-
const raw = await Promise.race([
|
|
4129
|
-
client.send("EVAL", [
|
|
4130
|
-
CACHE_SCRIPT,
|
|
4131
|
-
String(this.keys.length),
|
|
4132
|
-
...this.keys,
|
|
4133
|
-
JSON.stringify({
|
|
4134
|
-
op,
|
|
4135
|
-
key,
|
|
4136
|
-
capacity: this.maxEntries,
|
|
4137
|
-
owner: crypto.randomUUID(),
|
|
4138
|
-
...this.options,
|
|
4139
|
-
...extra,
|
|
4140
|
-
tags,
|
|
4141
|
-
prefixes,
|
|
4142
|
-
ttlMs
|
|
4143
|
-
})
|
|
4144
|
-
]),
|
|
4145
|
-
timeout
|
|
4146
|
-
]);
|
|
4147
|
-
if (this.closed || this.client !== client)
|
|
4148
|
-
throw new Error("Redis cache connection was retired.");
|
|
4149
|
-
return JSON.parse(String(raw));
|
|
4150
|
-
} catch (error) {
|
|
4151
|
-
if (error && typeof error === "object" && "code" in error && typeof error.code === "string" && error.code.startsWith("ERR_REDIS_CONNECTION"))
|
|
4152
|
-
retire(error);
|
|
4153
|
-
throw error;
|
|
4154
|
-
} finally {
|
|
4155
|
-
clearTimeout(timer);
|
|
3842
|
+
if (operator.ilike !== undefined) {
|
|
3843
|
+
clauses.push(`${column} ${currentSqlDialect().ilikeOperator()} ${pushParam(params, operator.ilike)}`);
|
|
3844
|
+
}
|
|
3845
|
+
if (operator.tsMatch !== undefined) {
|
|
3846
|
+
if (currentSqlDialect().driver !== "pgsql") {
|
|
3847
|
+
throw new Error("Full-text search (tsMatch) is only available on PostgreSQL.");
|
|
4156
3848
|
}
|
|
3849
|
+
clauses.push(`${column} @@ plainto_tsquery('english', ${pushParam(params, operator.tsMatch)})`);
|
|
4157
3850
|
}
|
|
3851
|
+
return clauses;
|
|
4158
3852
|
}
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
maxEntries;
|
|
4165
|
-
cache = new Map;
|
|
4166
|
-
inflight = new Map;
|
|
4167
|
-
tagIndex = new Map;
|
|
4168
|
-
keyTags = new Map;
|
|
4169
|
-
constructor(ttlMs = 3600000, maxEntries = 100) {
|
|
4170
|
-
this.ttlMs = ttlMs;
|
|
4171
|
-
this.maxEntries = maxEntries;
|
|
4172
|
-
if (!Number.isFinite(ttlMs) || ttlMs < 0) {
|
|
4173
|
-
throw new RangeError("ttlMs must be a non-negative number.");
|
|
3853
|
+
function appendWhereParts(tableName, where, params) {
|
|
3854
|
+
const clauses = [];
|
|
3855
|
+
for (const [columnName, filterValue] of Object.entries(where)) {
|
|
3856
|
+
if (filterValue === undefined) {
|
|
3857
|
+
continue;
|
|
4174
3858
|
}
|
|
4175
|
-
|
|
4176
|
-
|
|
3859
|
+
const column = resolveQualifiedColumn(tableName, columnName);
|
|
3860
|
+
if (Array.isArray(filterValue)) {
|
|
3861
|
+
clauses.push(buildInClause(column, filterValue, params));
|
|
3862
|
+
continue;
|
|
3863
|
+
}
|
|
3864
|
+
if (isQueryOperator(filterValue)) {
|
|
3865
|
+
clauses.push(...buildOperatorClauses(column, filterValue, params));
|
|
3866
|
+
continue;
|
|
3867
|
+
}
|
|
3868
|
+
if (filterValue === null) {
|
|
3869
|
+
clauses.push(`${column} IS NULL`);
|
|
3870
|
+
continue;
|
|
4177
3871
|
}
|
|
3872
|
+
clauses.push(`${column} = ${pushParam(params, filterValue)}`);
|
|
4178
3873
|
}
|
|
4179
|
-
|
|
4180
|
-
|
|
3874
|
+
return clauses.join(" AND ");
|
|
3875
|
+
}
|
|
3876
|
+
function buildWhereClause(tableName, where = {}) {
|
|
3877
|
+
const params = [];
|
|
3878
|
+
const body = appendWhereParts(tableName, where, params);
|
|
3879
|
+
return {
|
|
3880
|
+
clause: body.length > 0 ? ` WHERE ${body}` : "",
|
|
3881
|
+
params
|
|
3882
|
+
};
|
|
3883
|
+
}
|
|
3884
|
+
function remapExistsSql(sql, existsParams, params) {
|
|
3885
|
+
const offset = params.length;
|
|
3886
|
+
params.push(...existsParams);
|
|
3887
|
+
return sql.replace(/\$(\d+)/g, (_match, index) => `$${offset + Number(index)}`);
|
|
3888
|
+
}
|
|
3889
|
+
function buildWhereNodeClause(tableName, node, params) {
|
|
3890
|
+
if ("compareRow" in node) {
|
|
3891
|
+
const { columns, operator, values } = node.compareRow;
|
|
3892
|
+
if (!Array.isArray(columns) || !columns.length || !Array.isArray(values) || columns.length !== values.length || !["lt", "gt"].includes(operator) || Array.from(columns).some((column) => typeof column !== "string" || !column.length))
|
|
3893
|
+
throw new TypeError("Invalid row comparison columns, values or operator.");
|
|
3894
|
+
return `(${columns.map((column) => qualifyColumn(tableName, column)).join(", ")}) ${operator === "lt" ? "<" : ">"} (${values.map((value) => pushParam(params, value)).join(", ")})`;
|
|
4181
3895
|
}
|
|
4182
|
-
|
|
4183
|
-
const
|
|
4184
|
-
|
|
4185
|
-
this.cache.set(key, {
|
|
4186
|
-
value,
|
|
4187
|
-
expiresAt: now + resolvedTtlMs,
|
|
4188
|
-
lastAccessedAt: now
|
|
4189
|
-
});
|
|
4190
|
-
this.evictOverflow();
|
|
3896
|
+
if ("exists" in node) {
|
|
3897
|
+
const body = remapExistsSql(node.exists.sql, node.exists.params, params);
|
|
3898
|
+
return `${node.exists.not ? "NOT " : ""}EXISTS (${body})`;
|
|
4191
3899
|
}
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
const cachedEntry = this.getFreshEntry(key);
|
|
4195
|
-
if (cachedEntry) {
|
|
4196
|
-
return cachedEntry.value;
|
|
4197
|
-
}
|
|
4198
|
-
const inflightRequest = this.inflight.get(key);
|
|
4199
|
-
if (inflightRequest) {
|
|
4200
|
-
return inflightRequest;
|
|
4201
|
-
}
|
|
4202
|
-
const pendingRequest = loader().then((value) => {
|
|
4203
|
-
this.set(key, value, ttlMs);
|
|
4204
|
-
return value;
|
|
4205
|
-
}).finally(() => {
|
|
4206
|
-
this.inflight.delete(key);
|
|
4207
|
-
});
|
|
4208
|
-
this.inflight.set(key, pendingRequest);
|
|
4209
|
-
return pendingRequest;
|
|
3900
|
+
if ("where" in node) {
|
|
3901
|
+
return appendWhereParts(tableName, node.where, params);
|
|
4210
3902
|
}
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
}
|
|
4215
|
-
let tagsForKey = this.keyTags.get(key);
|
|
4216
|
-
if (!tagsForKey) {
|
|
4217
|
-
tagsForKey = new Set;
|
|
4218
|
-
this.keyTags.set(key, tagsForKey);
|
|
4219
|
-
}
|
|
4220
|
-
for (const tag of tags) {
|
|
4221
|
-
tagsForKey.add(tag);
|
|
4222
|
-
let keysForTag = this.tagIndex.get(tag);
|
|
4223
|
-
if (!keysForTag) {
|
|
4224
|
-
keysForTag = new Set;
|
|
4225
|
-
this.tagIndex.set(tag, keysForTag);
|
|
4226
|
-
}
|
|
4227
|
-
keysForTag.add(key);
|
|
4228
|
-
}
|
|
3903
|
+
const grouped = buildWhereGroupClause(tableName, node.group, params);
|
|
3904
|
+
if (!grouped) {
|
|
3905
|
+
return "";
|
|
4229
3906
|
}
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
keysToRemove.add(key);
|
|
4239
|
-
}
|
|
4240
|
-
}
|
|
4241
|
-
let removed = 0;
|
|
4242
|
-
for (const key of keysToRemove) {
|
|
4243
|
-
if (this.invalidate(key)) {
|
|
4244
|
-
removed += 1;
|
|
4245
|
-
}
|
|
3907
|
+
return grouped.includes(" OR ") ? `(${grouped})` : grouped;
|
|
3908
|
+
}
|
|
3909
|
+
function buildWhereGroupClause(tableName, nodes, params) {
|
|
3910
|
+
let result = "";
|
|
3911
|
+
for (const node of nodes) {
|
|
3912
|
+
const part = buildWhereNodeClause(tableName, node, params);
|
|
3913
|
+
if (!part) {
|
|
3914
|
+
continue;
|
|
4246
3915
|
}
|
|
4247
|
-
|
|
4248
|
-
|
|
3916
|
+
if (!result) {
|
|
3917
|
+
result = part;
|
|
3918
|
+
continue;
|
|
4249
3919
|
}
|
|
4250
|
-
|
|
3920
|
+
result = node.kind === "or" ? `${result} OR ${part}` : `${result} AND ${part}`;
|
|
4251
3921
|
}
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
if (removed) {
|
|
4255
|
-
this.detachKeyFromTags(key);
|
|
4256
|
-
}
|
|
4257
|
-
return removed;
|
|
4258
|
-
}
|
|
4259
|
-
invalidateByPrefix(prefix) {
|
|
4260
|
-
let removed = 0;
|
|
4261
|
-
for (const key of [...this.cache.keys()]) {
|
|
4262
|
-
if (key === prefix || key.startsWith(`${prefix}?`)) {
|
|
4263
|
-
if (this.invalidate(key)) {
|
|
4264
|
-
removed += 1;
|
|
4265
|
-
}
|
|
4266
|
-
}
|
|
4267
|
-
}
|
|
4268
|
-
return removed;
|
|
4269
|
-
}
|
|
4270
|
-
clear() {
|
|
4271
|
-
this.cache.clear();
|
|
4272
|
-
this.inflight.clear();
|
|
4273
|
-
this.tagIndex.clear();
|
|
4274
|
-
this.keyTags.clear();
|
|
4275
|
-
}
|
|
4276
|
-
size() {
|
|
4277
|
-
this.pruneExpired();
|
|
4278
|
-
return this.cache.size;
|
|
4279
|
-
}
|
|
4280
|
-
detachKeyFromTags(key) {
|
|
4281
|
-
const tags = this.keyTags.get(key);
|
|
4282
|
-
if (!tags) {
|
|
4283
|
-
return;
|
|
4284
|
-
}
|
|
4285
|
-
for (const tag of tags) {
|
|
4286
|
-
const keys = this.tagIndex.get(tag);
|
|
4287
|
-
if (!keys) {
|
|
4288
|
-
continue;
|
|
4289
|
-
}
|
|
4290
|
-
keys.delete(key);
|
|
4291
|
-
if (keys.size === 0) {
|
|
4292
|
-
this.tagIndex.delete(tag);
|
|
4293
|
-
}
|
|
4294
|
-
}
|
|
4295
|
-
this.keyTags.delete(key);
|
|
4296
|
-
}
|
|
4297
|
-
getFreshEntry(key) {
|
|
4298
|
-
const entry = this.cache.get(key);
|
|
4299
|
-
if (!entry) {
|
|
4300
|
-
return;
|
|
4301
|
-
}
|
|
4302
|
-
if (entry.expiresAt <= Date.now()) {
|
|
4303
|
-
this.invalidate(key);
|
|
4304
|
-
return;
|
|
4305
|
-
}
|
|
4306
|
-
entry.lastAccessedAt = Date.now();
|
|
4307
|
-
return entry;
|
|
4308
|
-
}
|
|
4309
|
-
pruneExpired() {
|
|
4310
|
-
const now = Date.now();
|
|
4311
|
-
for (const [key, entry] of this.cache.entries()) {
|
|
4312
|
-
if (entry.expiresAt <= now) {
|
|
4313
|
-
this.invalidate(key);
|
|
4314
|
-
}
|
|
4315
|
-
}
|
|
4316
|
-
}
|
|
4317
|
-
evictOverflow() {
|
|
4318
|
-
while (this.cache.size > this.maxEntries) {
|
|
4319
|
-
let oldestKey;
|
|
4320
|
-
let oldestAccessTime = Number.POSITIVE_INFINITY;
|
|
4321
|
-
for (const [key, entry] of this.cache.entries()) {
|
|
4322
|
-
if (entry.lastAccessedAt < oldestAccessTime) {
|
|
4323
|
-
oldestAccessTime = entry.lastAccessedAt;
|
|
4324
|
-
oldestKey = key;
|
|
4325
|
-
}
|
|
4326
|
-
}
|
|
4327
|
-
if (!oldestKey) {
|
|
4328
|
-
return;
|
|
4329
|
-
}
|
|
4330
|
-
this.invalidate(oldestKey);
|
|
4331
|
-
}
|
|
3922
|
+
if (!result) {
|
|
3923
|
+
return "";
|
|
4332
3924
|
}
|
|
3925
|
+
return result;
|
|
4333
3926
|
}
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
cache;
|
|
4339
|
-
constructor(cache) {
|
|
4340
|
-
this.cache = cache;
|
|
3927
|
+
function buildAdvancedWhereClause(tableName, where = {}, whereNodes = [], params = []) {
|
|
3928
|
+
const nodes = [];
|
|
3929
|
+
if (Object.keys(where).length > 0) {
|
|
3930
|
+
nodes.push({ kind: "and", where });
|
|
4341
3931
|
}
|
|
4342
|
-
|
|
4343
|
-
|
|
3932
|
+
nodes.push(...whereNodes);
|
|
3933
|
+
const combined = buildWhereGroupClause(tableName, nodes, params);
|
|
3934
|
+
return {
|
|
3935
|
+
clause: combined ? ` WHERE ${combined}` : "",
|
|
3936
|
+
params
|
|
3937
|
+
};
|
|
3938
|
+
}
|
|
3939
|
+
function resolveSoftDeleteColumn(table) {
|
|
3940
|
+
if (!table.softDeletes) {
|
|
3941
|
+
return null;
|
|
4344
3942
|
}
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
return Promise.resolve();
|
|
3943
|
+
if (table.softDeletes === true) {
|
|
3944
|
+
return "deleted_at";
|
|
4348
3945
|
}
|
|
4349
|
-
|
|
4350
|
-
|
|
3946
|
+
return table.softDeletes.column ?? "deleted_at";
|
|
3947
|
+
}
|
|
3948
|
+
function appendSoftDeleteScope(table, options, clauses) {
|
|
3949
|
+
const column = resolveSoftDeleteColumn(table);
|
|
3950
|
+
if (!column) {
|
|
3951
|
+
return;
|
|
4351
3952
|
}
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
3953
|
+
const qualifiedColumn = qualifyColumn(table.name, column);
|
|
3954
|
+
if (options.onlyTrashed) {
|
|
3955
|
+
clauses.push(`${qualifiedColumn} IS NOT NULL`);
|
|
3956
|
+
return;
|
|
4355
3957
|
}
|
|
4356
|
-
|
|
4357
|
-
|
|
3958
|
+
if (!options.withTrashed) {
|
|
3959
|
+
clauses.push(`${qualifiedColumn} IS NULL`);
|
|
4358
3960
|
}
|
|
4359
|
-
|
|
4360
|
-
|
|
3961
|
+
}
|
|
3962
|
+
function buildQueryWhereClause(table, options = {}, whereNodes = [], params = []) {
|
|
3963
|
+
const { clause, params: whereParams } = buildAdvancedWhereClause(table.name, options.where ?? {}, whereNodes, params);
|
|
3964
|
+
const softDeleteClauses = [];
|
|
3965
|
+
appendSoftDeleteScope(table, options, softDeleteClauses);
|
|
3966
|
+
if (softDeleteClauses.length === 0) {
|
|
3967
|
+
return { clause, params: whereParams };
|
|
4361
3968
|
}
|
|
4362
|
-
|
|
4363
|
-
|
|
3969
|
+
const base = clause.replace(/^ WHERE /, "");
|
|
3970
|
+
const scope = softDeleteClauses.join(" AND ");
|
|
3971
|
+
return {
|
|
3972
|
+
clause: base ? ` WHERE (${base}) AND ${scope}` : ` WHERE ${scope}`,
|
|
3973
|
+
params: whereParams
|
|
3974
|
+
};
|
|
3975
|
+
}
|
|
3976
|
+
function isQueryOrder(value) {
|
|
3977
|
+
return "column" in value;
|
|
3978
|
+
}
|
|
3979
|
+
function normalizeOrderBy(orderBy) {
|
|
3980
|
+
if (!orderBy) {
|
|
3981
|
+
return [];
|
|
4364
3982
|
}
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
return Promise.resolve();
|
|
3983
|
+
if (Array.isArray(orderBy)) {
|
|
3984
|
+
return orderBy;
|
|
4368
3985
|
}
|
|
4369
|
-
|
|
4370
|
-
return
|
|
3986
|
+
if (isQueryOrder(orderBy)) {
|
|
3987
|
+
return [orderBy];
|
|
4371
3988
|
}
|
|
3989
|
+
return Object.entries(orderBy).map(([column, direction]) => ({
|
|
3990
|
+
column,
|
|
3991
|
+
direction
|
|
3992
|
+
}));
|
|
4372
3993
|
}
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
if (options.driver === "redis") {
|
|
4378
|
-
if (!options.redisUrl) {
|
|
4379
|
-
throw new Error('CACHE_DRIVER="redis" requires REDIS_URL to be set.');
|
|
3994
|
+
function buildOrderByClause(tableName, orderBy) {
|
|
3995
|
+
const parts = normalizeOrderBy(orderBy).map(({ column, direction, nulls }) => {
|
|
3996
|
+
if (nulls !== undefined && nulls !== "first" && nulls !== "last") {
|
|
3997
|
+
throw new TypeError("Invalid null ordering: expected first or last.");
|
|
4380
3998
|
}
|
|
4381
|
-
|
|
3999
|
+
const reference = resolveQualifiedColumn(tableName, column);
|
|
4000
|
+
const ordered = `${reference} ${normalizeDirection(direction)}`;
|
|
4001
|
+
if (nulls === undefined)
|
|
4002
|
+
return ordered;
|
|
4003
|
+
if (currentSqlDialect().driver === "mysql") {
|
|
4004
|
+
return `(${reference} IS NULL) ${nulls === "first" ? "DESC" : "ASC"}, ${ordered}`;
|
|
4005
|
+
}
|
|
4006
|
+
return `${ordered} NULLS ${nulls.toUpperCase()}`;
|
|
4007
|
+
});
|
|
4008
|
+
return parts.length > 0 ? ` ORDER BY ${parts.join(", ")}` : "";
|
|
4009
|
+
}
|
|
4010
|
+
function buildGroupByClause(tableName, groupBy) {
|
|
4011
|
+
if (!groupBy) {
|
|
4012
|
+
return "";
|
|
4382
4013
|
}
|
|
4383
|
-
|
|
4014
|
+
const columns = (Array.isArray(groupBy) ? groupBy : [groupBy]).map((column) => String(column));
|
|
4015
|
+
const parts = columns.map((column) => resolveQualifiedColumn(tableName, column));
|
|
4016
|
+
return parts.length > 0 ? ` GROUP BY ${parts.join(", ")}` : "";
|
|
4384
4017
|
}
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
tags;
|
|
4389
|
-
constructor(store, tags) {
|
|
4390
|
-
this.store = store;
|
|
4391
|
-
this.tags = tags;
|
|
4018
|
+
function buildHavingClause(tableName, having, params) {
|
|
4019
|
+
if (!having) {
|
|
4020
|
+
return "";
|
|
4392
4021
|
}
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
|
|
4398
|
-
|
|
4022
|
+
const body = appendWhereParts(tableName, having, params);
|
|
4023
|
+
return body.length > 0 ? ` HAVING ${body}` : "";
|
|
4024
|
+
}
|
|
4025
|
+
function buildJoinClause(joins = []) {
|
|
4026
|
+
return joins.map((join) => {
|
|
4027
|
+
const joinType = join.type === "left" ? "LEFT JOIN" : "INNER JOIN";
|
|
4028
|
+
const onClause = join.on.map(({ left, right }) => `${qualifyColumn(left.table, left.column)} = ${qualifyColumn(right.table, right.column)}`).join(" AND ");
|
|
4029
|
+
return ` ${joinType} ${quoteIdentifier(join.table)} ON ${onClause}`;
|
|
4030
|
+
}).join("");
|
|
4031
|
+
}
|
|
4032
|
+
function buildLimitClause(limit) {
|
|
4033
|
+
if (limit === undefined) {
|
|
4034
|
+
return "";
|
|
4399
4035
|
}
|
|
4400
|
-
|
|
4401
|
-
|
|
4036
|
+
if (!Number.isInteger(limit) || limit <= 0) {
|
|
4037
|
+
throw new Error("Query limit must be a positive integer.");
|
|
4402
4038
|
}
|
|
4039
|
+
return ` LIMIT ${limit}`;
|
|
4403
4040
|
}
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
class CacheRepository {
|
|
4408
|
-
store;
|
|
4409
|
-
constructor(store) {
|
|
4410
|
-
this.store = store;
|
|
4041
|
+
function buildOffsetClause(offset) {
|
|
4042
|
+
if (offset === undefined) {
|
|
4043
|
+
return "";
|
|
4411
4044
|
}
|
|
4412
|
-
|
|
4413
|
-
|
|
4045
|
+
if (!Number.isInteger(offset) || offset < 0) {
|
|
4046
|
+
throw new Error("Query offset must be a non-negative integer.");
|
|
4414
4047
|
}
|
|
4415
|
-
|
|
4416
|
-
|
|
4048
|
+
return ` OFFSET ${offset}`;
|
|
4049
|
+
}
|
|
4050
|
+
function buildReturningColumns(table) {
|
|
4051
|
+
return table.columns.map((column) => qualifyColumn(table.name, column)).join(", ");
|
|
4052
|
+
}
|
|
4053
|
+
function buildSelectList(table, select, params = []) {
|
|
4054
|
+
if (!select || select.length === 0) {
|
|
4055
|
+
return buildReturningColumns(table);
|
|
4417
4056
|
}
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
4438
|
-
|
|
4439
|
-
|
|
4440
|
-
await this.flush();
|
|
4441
|
-
}
|
|
4442
|
-
async size() {
|
|
4443
|
-
return this.store.size();
|
|
4444
|
-
}
|
|
4445
|
-
}
|
|
4446
|
-
var repository_default = CacheRepository;
|
|
4447
|
-
// ../../src/core/cache/tags.ts
|
|
4448
|
-
var CACHE_TAGS = {
|
|
4449
|
-
organizations: "organizations",
|
|
4450
|
-
projects: "projects",
|
|
4451
|
-
tasks: "tasks",
|
|
4452
|
-
comments: "comments",
|
|
4453
|
-
attachments: "attachments",
|
|
4454
|
-
reports: "reports",
|
|
4455
|
-
users: "users",
|
|
4456
|
-
departments: "departments",
|
|
4457
|
-
positions: "positions",
|
|
4458
|
-
applications: "applications",
|
|
4459
|
-
careers: "careers",
|
|
4460
|
-
offers: "offers"
|
|
4461
|
-
};
|
|
4462
|
-
// ../../src/core/crypto/fieldEncryptionRotation.ts
|
|
4463
|
-
import { createHash as createHash3, randomUUID } from "crypto";
|
|
4464
|
-
|
|
4465
|
-
// ../../src/core/database/baseRepository.ts
|
|
4466
|
-
init_eventBus();
|
|
4467
|
-
|
|
4468
|
-
// ../../src/core/events/index.ts
|
|
4469
|
-
init_deferredModelEvents();
|
|
4470
|
-
init_eventBus();
|
|
4471
|
-
function modelEventName(tableName, action) {
|
|
4472
|
-
return `${tableName}.${action}`;
|
|
4473
|
-
}
|
|
4474
|
-
|
|
4475
|
-
// ../../src/core/pagination/index.ts
|
|
4476
|
-
function buildPaginationMeta(input) {
|
|
4477
|
-
const lastPage = Math.max(1, Math.ceil(input.total / input.perPage));
|
|
4478
|
-
return {
|
|
4479
|
-
page: input.page,
|
|
4480
|
-
per_page: input.perPage,
|
|
4481
|
-
total: input.total,
|
|
4482
|
-
last_page: lastPage
|
|
4483
|
-
};
|
|
4057
|
+
return select.map((item) => {
|
|
4058
|
+
if (item.kind === "column") {
|
|
4059
|
+
const column = qualifyColumn(item.table, item.column);
|
|
4060
|
+
return item.as ? `${column} AS ${quoteIdentifier(item.as)}` : column;
|
|
4061
|
+
}
|
|
4062
|
+
if (item.kind === "textColumn") {
|
|
4063
|
+
const dialect = currentSqlDialect();
|
|
4064
|
+
const column = qualifyColumn(item.table, item.column);
|
|
4065
|
+
const text = dialect.driver === "sqlite" ? `CASE WHEN typeof(${column}) = 'real' THEN printf('%!.26g', ${column}) ELSE ${dialect.castToText(column)} END` : dialect.castToText(column);
|
|
4066
|
+
return `${text} AS ${quoteIdentifier(item.as)}`;
|
|
4067
|
+
}
|
|
4068
|
+
if (item.kind === "literalText") {
|
|
4069
|
+
return `${currentSqlDialect().castToText(pushParam(params, item.value))} AS ${quoteIdentifier(item.as)}`;
|
|
4070
|
+
}
|
|
4071
|
+
if (item.kind === "subqueryCount") {
|
|
4072
|
+
const body = remapExistsSql(item.sql, item.params, params);
|
|
4073
|
+
return `(${body}) AS ${quoteIdentifier(item.as)}`;
|
|
4074
|
+
}
|
|
4075
|
+
const column = qualifyColumn(item.table, item.column);
|
|
4076
|
+
const placeholder = pushParam(params, item.query);
|
|
4077
|
+
return `ts_rank(${column}, plainto_tsquery('english', ${placeholder})) AS ${quoteIdentifier(item.as)}`;
|
|
4078
|
+
}).join(", ");
|
|
4484
4079
|
}
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
throw new TypeError("Keyset ordering requires between 1 and 8 columns.");
|
|
4498
|
-
if (table.columns.some((column) => column.toLowerCase().startsWith(KEYSET_ALIAS_PREFIX)))
|
|
4499
|
-
throw new TypeError("Table columns conflict with reserved keyset aliases.");
|
|
4500
|
-
const seen = new Set;
|
|
4501
|
-
const order = Array.from(options.orderBy).map(({ column, direction }) => {
|
|
4502
|
-
if (!table.columns.includes(column) || seen.has(column) || !["asc", "desc"].includes(direction))
|
|
4503
|
-
throw new TypeError("Invalid or duplicate keyset ordering column/direction.");
|
|
4504
|
-
seen.add(column);
|
|
4505
|
-
return { column, direction };
|
|
4080
|
+
function getDefinedColumnEntries(table, values, options = {}) {
|
|
4081
|
+
const record = values;
|
|
4082
|
+
const excluded = new Set(options.exclude ?? []);
|
|
4083
|
+
return table.columns.flatMap((column) => {
|
|
4084
|
+
if (excluded.has(column) || !Object.hasOwn(record, column)) {
|
|
4085
|
+
return [];
|
|
4086
|
+
}
|
|
4087
|
+
const value = record[column];
|
|
4088
|
+
if (value === undefined) {
|
|
4089
|
+
return [];
|
|
4090
|
+
}
|
|
4091
|
+
return [[column, value]];
|
|
4506
4092
|
});
|
|
4507
|
-
if (order.at(-1)?.column !== table.primaryKey)
|
|
4508
|
-
throw new TypeError("Keyset ordering must end with the table primary key.");
|
|
4509
|
-
if (options.cursor !== undefined) {
|
|
4510
|
-
const cursor = options.cursor;
|
|
4511
|
-
if (cursor?.version !== 1 || !Array.isArray(cursor.order) || cursor.order.length !== order.length || !Array.isArray(cursor.values) || cursor.values.length !== order.length || Array.from(cursor.order).some((item, i) => !item || item.column !== order[i]?.column || item.direction !== order[i]?.direction) || Array.from(cursor.values).some((value) => typeof value !== "string" || value.length > 4096))
|
|
4512
|
-
throw new TypeError("Invalid keyset cursor or ordering mismatch.");
|
|
4513
|
-
}
|
|
4514
|
-
return order;
|
|
4515
4093
|
}
|
|
4516
|
-
function
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
4527
|
-
|
|
4528
|
-
|
|
4529
|
-
|
|
4094
|
+
function buildSelectLock(table, options) {
|
|
4095
|
+
const lock = options.lock;
|
|
4096
|
+
if (!lock)
|
|
4097
|
+
return "";
|
|
4098
|
+
const driver = currentSqlDialect().driver;
|
|
4099
|
+
if (driver === "sqlite")
|
|
4100
|
+
throw new Error("Row locks are not supported on SQLite; use an explicit write transaction.");
|
|
4101
|
+
if (lock.mode !== "update" && lock.mode !== "share")
|
|
4102
|
+
throw new TypeError("Invalid row lock mode.");
|
|
4103
|
+
if (lock.wait !== undefined && !["wait", "nowait", "skipLocked"].includes(lock.wait))
|
|
4104
|
+
throw new TypeError("Invalid row lock wait policy.");
|
|
4105
|
+
if (options.groupBy || options.having)
|
|
4106
|
+
throw new Error("Row locks cannot be combined with grouped/aggregate queries.");
|
|
4107
|
+
let target = "";
|
|
4108
|
+
if (lock.of !== undefined) {
|
|
4109
|
+
if (driver !== "pgsql")
|
|
4110
|
+
throw new Error("Row lock targets are supported only on PostgreSQL.");
|
|
4111
|
+
const tables = [table.name, ...(options.joins ?? []).map((join) => join.table)];
|
|
4112
|
+
if (!lock.of.length || lock.of.some((name) => !tables.includes(name)))
|
|
4113
|
+
throw new TypeError("Row lock targets must name tables in this query.");
|
|
4114
|
+
target = ` OF ${lock.of.map(quoteIdentifier).join(", ")}`;
|
|
4530
4115
|
}
|
|
4531
|
-
const
|
|
4532
|
-
|
|
4533
|
-
order.slice(0, i).forEach((prefix, j) => {
|
|
4534
|
-
where[prefix.column] = cursor.values[j];
|
|
4535
|
-
});
|
|
4536
|
-
where[item.column] = { [item.direction === "asc" ? "gt" : "lt"]: cursor.values[i] };
|
|
4537
|
-
return { kind: i === 0 ? "and" : "or", where };
|
|
4538
|
-
});
|
|
4539
|
-
return [{ kind: "and", group: branches }];
|
|
4116
|
+
const wait = lock.wait === "nowait" ? " NOWAIT" : lock.wait === "skipLocked" ? " SKIP LOCKED" : "";
|
|
4117
|
+
return ` FOR ${lock.mode === "update" ? "UPDATE" : "SHARE"}${target}${wait}`;
|
|
4540
4118
|
}
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
4119
|
+
function buildSelectQuery(table, options = {}, whereNodes = []) {
|
|
4120
|
+
const params = [];
|
|
4121
|
+
const columns = buildSelectList(table, options.select, params);
|
|
4122
|
+
const { clause } = buildQueryWhereClause(table, options, whereNodes, params);
|
|
4123
|
+
const joins = buildJoinClause(options.joins);
|
|
4124
|
+
const groupBy = buildGroupByClause(table.name, options.groupBy);
|
|
4125
|
+
const havingClause = buildHavingClause(table.name, options.having, params);
|
|
4126
|
+
const orderBy = buildOrderByClause(table.name, options.orderBy ?? table.defaultOrderBy);
|
|
4127
|
+
const limit = buildLimitClause(options.limit);
|
|
4128
|
+
const offset = buildOffsetClause(options.offset);
|
|
4129
|
+
const lock = buildSelectLock(table, options);
|
|
4130
|
+
return {
|
|
4131
|
+
text: `SELECT ${columns} FROM ${quoteIdentifier(table.name)}${joins}${clause}${groupBy}${havingClause}${orderBy}${limit}${offset}${lock}`,
|
|
4132
|
+
params
|
|
4133
|
+
};
|
|
4546
4134
|
}
|
|
4547
|
-
function
|
|
4548
|
-
|
|
4135
|
+
function buildCountQuery(table, where = {}, options = {}, whereNodes = []) {
|
|
4136
|
+
const params = [];
|
|
4137
|
+
const { clause, params: whereParams } = buildQueryWhereClause(table, {
|
|
4138
|
+
where,
|
|
4139
|
+
withTrashed: options.withTrashed,
|
|
4140
|
+
onlyTrashed: options.onlyTrashed
|
|
4141
|
+
}, whereNodes);
|
|
4142
|
+
params.push(...whereParams);
|
|
4143
|
+
const joins = buildJoinClause(options.joins);
|
|
4144
|
+
const groupBy = buildGroupByClause(table.name, options.groupBy);
|
|
4145
|
+
return {
|
|
4146
|
+
text: `SELECT COUNT(*) AS count FROM ${quoteIdentifier(table.name)}${joins}${clause}${groupBy}`,
|
|
4147
|
+
params
|
|
4148
|
+
};
|
|
4549
4149
|
}
|
|
4550
|
-
function
|
|
4551
|
-
|
|
4150
|
+
function buildProjectionQuery(table, expression, alias, options = {}, whereNodes = []) {
|
|
4151
|
+
assertSafeProjectionExpression(expression);
|
|
4152
|
+
const params = [];
|
|
4153
|
+
const { clause, params: whereParams } = buildQueryWhereClause(table, options, whereNodes);
|
|
4154
|
+
params.push(...whereParams);
|
|
4155
|
+
const joins = buildJoinClause(options.joins);
|
|
4156
|
+
const groupBy = buildGroupByClause(table.name, options.groupBy);
|
|
4157
|
+
const orderBy = buildOrderByClause(table.name, options.orderBy);
|
|
4158
|
+
const limit = buildLimitClause(options.limit);
|
|
4159
|
+
return {
|
|
4160
|
+
text: `SELECT ${expression} AS ${quoteIdentifier(alias)} FROM ${quoteIdentifier(table.name)}${joins}${clause}${groupBy}${orderBy}${limit}`,
|
|
4161
|
+
params
|
|
4162
|
+
};
|
|
4552
4163
|
}
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
throw new Error(`Invalid qualified column: ${columnName}`);
|
|
4558
|
-
}
|
|
4559
|
-
return qualifyColumn(table, column);
|
|
4164
|
+
var SAFE_PROJECTION_EXPRESSION = /^(COUNT\(\*\)|(?:"[\w_]+"(?:\."[\w_]+")?)|(?:AVG|SUM|MIN|MAX)\((?:"[\w_]+"(?:\."[\w_]+")?)\))$/;
|
|
4165
|
+
function assertSafeProjectionExpression(expression) {
|
|
4166
|
+
if (!SAFE_PROJECTION_EXPRESSION.test(expression.trim())) {
|
|
4167
|
+
throw new Error(`Unsafe projection expression: ${expression}`);
|
|
4560
4168
|
}
|
|
4561
|
-
return qualifyColumn(defaultTable, columnName);
|
|
4562
4169
|
}
|
|
4563
|
-
function
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
|
|
4170
|
+
function buildGroupedSumQuery(table, groupColumn, column, options = {}, whereNodes = options.whereNodes ?? []) {
|
|
4171
|
+
if (!table.columns.includes(groupColumn) || !table.columns.includes(column))
|
|
4172
|
+
throw new TypeError("Grouped sums require declared columns.");
|
|
4173
|
+
for (const key of Object.keys(options))
|
|
4174
|
+
if (!["where", "whereNodes", "withTrashed", "onlyTrashed"].includes(key))
|
|
4175
|
+
throw new TypeError("Grouped sums do not support joins, selection, ordering, pagination, grouping or locks.");
|
|
4176
|
+
const group = qualifyColumn(table.name, groupColumn);
|
|
4177
|
+
const total = currentSqlDialect().castToText(`SUM(${qualifyColumn(table.name, column)})`);
|
|
4178
|
+
const { clause, params } = buildQueryWhereClause(table, options, whereNodes);
|
|
4179
|
+
return {
|
|
4180
|
+
text: `SELECT ${group} AS ${quoteIdentifier("value")}, ${total} AS ${quoteIdentifier("sum")} FROM ${quoteIdentifier(table.name)}${clause} GROUP BY ${group} ORDER BY ${group} ASC`,
|
|
4181
|
+
params
|
|
4182
|
+
};
|
|
4569
4183
|
}
|
|
4570
|
-
function
|
|
4571
|
-
|
|
4572
|
-
}
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
}
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4184
|
+
function buildGroupedCountQuery(table, column, where = {}, options = {}) {
|
|
4185
|
+
const qualifiedColumn = qualifyColumn(table.name, column);
|
|
4186
|
+
const { clause, params } = buildQueryWhereClause(table, {
|
|
4187
|
+
where,
|
|
4188
|
+
...options
|
|
4189
|
+
});
|
|
4190
|
+
return {
|
|
4191
|
+
text: `SELECT ${qualifiedColumn} AS ${quoteIdentifier("value")}, COUNT(*) AS ${quoteIdentifier("count")} FROM ${quoteIdentifier(table.name)}${clause} GROUP BY ${qualifiedColumn} ORDER BY ${qualifiedColumn} ASC`,
|
|
4192
|
+
params
|
|
4193
|
+
};
|
|
4579
4194
|
}
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
"notIn",
|
|
4585
|
-
"gt",
|
|
4586
|
-
"gte",
|
|
4587
|
-
"lt",
|
|
4588
|
-
"lte",
|
|
4589
|
-
"isNull",
|
|
4590
|
-
"ilike",
|
|
4591
|
-
"tsMatch"
|
|
4592
|
-
]);
|
|
4593
|
-
function buildInClause(column, values, params, negated = false) {
|
|
4594
|
-
if (values.length === 0) {
|
|
4595
|
-
return negated ? "1 = 1" : "1 = 0";
|
|
4195
|
+
function buildInsertQuery(table, values) {
|
|
4196
|
+
const entries = getDefinedColumnEntries(table, values);
|
|
4197
|
+
if (entries.length === 0) {
|
|
4198
|
+
throw new Error(`Cannot insert into ${table.name} without any column values.`);
|
|
4596
4199
|
}
|
|
4597
|
-
const
|
|
4598
|
-
|
|
4200
|
+
const params = [];
|
|
4201
|
+
const columns = entries.map(([column]) => quoteIdentifier(column)).join(", ");
|
|
4202
|
+
const placeholders = entries.map(([, value]) => pushParam(params, value)).join(", ");
|
|
4203
|
+
const returningColumns = buildReturningColumns(table);
|
|
4204
|
+
return {
|
|
4205
|
+
text: `INSERT INTO ${quoteIdentifier(table.name)} (${columns}) VALUES (${placeholders})${returningSuffix(returningColumns)}`,
|
|
4206
|
+
params
|
|
4207
|
+
};
|
|
4599
4208
|
}
|
|
4600
|
-
function
|
|
4601
|
-
const
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
throw new Error(`Unsupported query operator(s) for ${column}: ${unsupported.join(", ")}. Supported: ${[...SUPPORTED_OPERATORS].join(", ")}.`);
|
|
4605
|
-
}
|
|
4606
|
-
if (operator.isNull === true) {
|
|
4607
|
-
clauses.push(`${column} IS NULL`);
|
|
4608
|
-
}
|
|
4609
|
-
if (operator.isNull === false) {
|
|
4610
|
-
clauses.push(`${column} IS NOT NULL`);
|
|
4611
|
-
}
|
|
4612
|
-
if (operator.eq !== undefined) {
|
|
4613
|
-
if (operator.eq === null) {
|
|
4614
|
-
clauses.push(`${column} IS NULL`);
|
|
4615
|
-
} else {
|
|
4616
|
-
clauses.push(`${column} = ${pushParam(params, operator.eq)}`);
|
|
4617
|
-
}
|
|
4618
|
-
}
|
|
4619
|
-
if (operator.ne !== undefined) {
|
|
4620
|
-
if (operator.ne === null) {
|
|
4621
|
-
clauses.push(`${column} IS NOT NULL`);
|
|
4622
|
-
} else {
|
|
4623
|
-
clauses.push(`${column} <> ${pushParam(params, operator.ne)}`);
|
|
4624
|
-
}
|
|
4625
|
-
}
|
|
4626
|
-
if (operator.in !== undefined) {
|
|
4627
|
-
clauses.push(buildInClause(column, operator.in, params));
|
|
4628
|
-
}
|
|
4629
|
-
if (operator.notIn !== undefined) {
|
|
4630
|
-
clauses.push(buildInClause(column, operator.notIn, params, true));
|
|
4631
|
-
}
|
|
4632
|
-
if (operator.gt !== undefined) {
|
|
4633
|
-
clauses.push(`${column} > ${pushParam(params, operator.gt)}`);
|
|
4634
|
-
}
|
|
4635
|
-
if (operator.gte !== undefined) {
|
|
4636
|
-
clauses.push(`${column} >= ${pushParam(params, operator.gte)}`);
|
|
4637
|
-
}
|
|
4638
|
-
if (operator.lt !== undefined) {
|
|
4639
|
-
clauses.push(`${column} < ${pushParam(params, operator.lt)}`);
|
|
4640
|
-
}
|
|
4641
|
-
if (operator.lte !== undefined) {
|
|
4642
|
-
clauses.push(`${column} <= ${pushParam(params, operator.lte)}`);
|
|
4643
|
-
}
|
|
4644
|
-
if (operator.ilike !== undefined) {
|
|
4645
|
-
clauses.push(`${column} ${currentSqlDialect().ilikeOperator()} ${pushParam(params, operator.ilike)}`);
|
|
4646
|
-
}
|
|
4647
|
-
if (operator.tsMatch !== undefined) {
|
|
4648
|
-
if (currentSqlDialect().driver !== "pgsql") {
|
|
4649
|
-
throw new Error("Full-text search (tsMatch) is only available on PostgreSQL.");
|
|
4650
|
-
}
|
|
4651
|
-
clauses.push(`${column} @@ plainto_tsquery('english', ${pushParam(params, operator.tsMatch)})`);
|
|
4209
|
+
function buildUpsertQuery(table, values, conflictColumns, updateColumns) {
|
|
4210
|
+
const entries = getDefinedColumnEntries(table, values);
|
|
4211
|
+
if (entries.length === 0) {
|
|
4212
|
+
throw new Error(`Cannot upsert into ${table.name} without any column values.`);
|
|
4652
4213
|
}
|
|
4653
|
-
|
|
4654
|
-
}
|
|
4655
|
-
function appendWhereParts(tableName, where, params) {
|
|
4656
|
-
const clauses = [];
|
|
4657
|
-
for (const [columnName, filterValue] of Object.entries(where)) {
|
|
4658
|
-
if (filterValue === undefined) {
|
|
4659
|
-
continue;
|
|
4660
|
-
}
|
|
4661
|
-
const column = resolveQualifiedColumn(tableName, columnName);
|
|
4662
|
-
if (Array.isArray(filterValue)) {
|
|
4663
|
-
clauses.push(buildInClause(column, filterValue, params));
|
|
4664
|
-
continue;
|
|
4665
|
-
}
|
|
4666
|
-
if (isQueryOperator(filterValue)) {
|
|
4667
|
-
clauses.push(...buildOperatorClauses(column, filterValue, params));
|
|
4668
|
-
continue;
|
|
4669
|
-
}
|
|
4670
|
-
if (filterValue === null) {
|
|
4671
|
-
clauses.push(`${column} IS NULL`);
|
|
4672
|
-
continue;
|
|
4673
|
-
}
|
|
4674
|
-
clauses.push(`${column} = ${pushParam(params, filterValue)}`);
|
|
4214
|
+
if (conflictColumns.length === 0) {
|
|
4215
|
+
throw new Error(`Cannot upsert into ${table.name} without any conflict columns.`);
|
|
4675
4216
|
}
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
4217
|
+
const insertable = new Set(entries.map(([column]) => column));
|
|
4218
|
+
const conflict = new Set(conflictColumns);
|
|
4219
|
+
const updatable = (updateColumns ?? entries.map(([column]) => column).filter((c) => !conflict.has(c))).filter((column) => insertable.has(column));
|
|
4679
4220
|
const params = [];
|
|
4680
|
-
const
|
|
4221
|
+
const columns = entries.map(([column]) => quoteIdentifier(column)).join(", ");
|
|
4222
|
+
const placeholders = entries.map(([, value]) => pushParam(params, value)).join(", ");
|
|
4223
|
+
const returningColumns = buildReturningColumns(table);
|
|
4224
|
+
const suffix = currentSqlDialect().upsertSuffix(conflictColumns, updatable);
|
|
4681
4225
|
return {
|
|
4682
|
-
|
|
4226
|
+
text: `INSERT INTO ${quoteIdentifier(table.name)} (${columns}) VALUES (${placeholders})${suffix}${returningSuffix(returningColumns)}`,
|
|
4683
4227
|
params
|
|
4684
4228
|
};
|
|
4685
4229
|
}
|
|
4686
|
-
function
|
|
4687
|
-
|
|
4688
|
-
|
|
4689
|
-
return sql.replace(/\$(\d+)/g, (_match, index) => `$${offset + Number(index)}`);
|
|
4690
|
-
}
|
|
4691
|
-
function buildWhereNodeClause(tableName, node, params) {
|
|
4692
|
-
if ("compareRow" in node) {
|
|
4693
|
-
const { columns, operator, values } = node.compareRow;
|
|
4694
|
-
if (!Array.isArray(columns) || !columns.length || !Array.isArray(values) || columns.length !== values.length || !["lt", "gt"].includes(operator) || Array.from(columns).some((column) => typeof column !== "string" || !column.length))
|
|
4695
|
-
throw new TypeError("Invalid row comparison columns, values or operator.");
|
|
4696
|
-
return `(${columns.map((column) => qualifyColumn(tableName, column)).join(", ")}) ${operator === "lt" ? "<" : ">"} (${values.map((value) => pushParam(params, value)).join(", ")})`;
|
|
4697
|
-
}
|
|
4698
|
-
if ("exists" in node) {
|
|
4699
|
-
const body = remapExistsSql(node.exists.sql, node.exists.params, params);
|
|
4700
|
-
return `${node.exists.not ? "NOT " : ""}EXISTS (${body})`;
|
|
4230
|
+
function buildIncrementQuery(table, id, column, amount, extra = {}) {
|
|
4231
|
+
if (!Number.isFinite(amount)) {
|
|
4232
|
+
throw new Error("Increment amount must be a finite number.");
|
|
4701
4233
|
}
|
|
4702
|
-
if (
|
|
4703
|
-
|
|
4234
|
+
if (!table.columns.includes(column)) {
|
|
4235
|
+
throw new Error(`Unknown column ${String(column)} on ${table.name}.`);
|
|
4704
4236
|
}
|
|
4705
|
-
const
|
|
4706
|
-
|
|
4707
|
-
|
|
4237
|
+
const params = [];
|
|
4238
|
+
const target = quoteIdentifier(column);
|
|
4239
|
+
const assignments = [`${target} = ${target} + ${pushParam(params, amount)}`];
|
|
4240
|
+
for (const [name, value] of getDefinedColumnEntries(table, extra, {
|
|
4241
|
+
exclude: [table.primaryKey, column]
|
|
4242
|
+
})) {
|
|
4243
|
+
assignments.push(`${quoteIdentifier(name)} = ${pushParam(params, value)}`);
|
|
4708
4244
|
}
|
|
4709
|
-
|
|
4245
|
+
const primaryKeyPlaceholder = pushParam(params, id);
|
|
4246
|
+
const returningColumns = buildReturningColumns(table);
|
|
4247
|
+
const scopeClauses = [];
|
|
4248
|
+
appendSoftDeleteScope(table, {}, scopeClauses);
|
|
4249
|
+
const scopeSuffix = scopeClauses.length > 0 ? ` AND ${scopeClauses.join(" AND ")}` : "";
|
|
4250
|
+
return {
|
|
4251
|
+
text: `UPDATE ${quoteIdentifier(table.name)} SET ${assignments.join(", ")} WHERE ${quoteIdentifier(table.primaryKey)} = ${primaryKeyPlaceholder}${scopeSuffix}${returningSuffix(returningColumns)}`,
|
|
4252
|
+
params
|
|
4253
|
+
};
|
|
4710
4254
|
}
|
|
4711
|
-
function
|
|
4712
|
-
|
|
4713
|
-
|
|
4714
|
-
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
}
|
|
4718
|
-
if (!result) {
|
|
4719
|
-
result = part;
|
|
4720
|
-
continue;
|
|
4721
|
-
}
|
|
4722
|
-
result = node.kind === "or" ? `${result} OR ${part}` : `${result} AND ${part}`;
|
|
4723
|
-
}
|
|
4724
|
-
if (!result) {
|
|
4725
|
-
return "";
|
|
4255
|
+
function buildUpdateQuery(table, id, changes) {
|
|
4256
|
+
const entries = getDefinedColumnEntries(table, changes, {
|
|
4257
|
+
exclude: [table.primaryKey]
|
|
4258
|
+
});
|
|
4259
|
+
if (entries.length === 0) {
|
|
4260
|
+
throw new Error(`Cannot update ${table.name} without any changed column values.`);
|
|
4726
4261
|
}
|
|
4727
|
-
|
|
4262
|
+
const params = [];
|
|
4263
|
+
const setClause = entries.map(([column, value]) => `${quoteIdentifier(column)} = ${pushParam(params, value)}`).join(", ");
|
|
4264
|
+
const primaryKeyPlaceholder = pushParam(params, id);
|
|
4265
|
+
const returningColumns = buildReturningColumns(table);
|
|
4266
|
+
const scopeClauses = [];
|
|
4267
|
+
appendSoftDeleteScope(table, {}, scopeClauses);
|
|
4268
|
+
const scopeSuffix = scopeClauses.length > 0 ? ` AND ${scopeClauses.join(" AND ")}` : "";
|
|
4269
|
+
return {
|
|
4270
|
+
text: `UPDATE ${quoteIdentifier(table.name)} SET ${setClause} WHERE ${quoteIdentifier(table.primaryKey)} = ${primaryKeyPlaceholder}${scopeSuffix}${returningSuffix(returningColumns)}`,
|
|
4271
|
+
params
|
|
4272
|
+
};
|
|
4728
4273
|
}
|
|
4729
|
-
function
|
|
4730
|
-
const
|
|
4731
|
-
if (
|
|
4732
|
-
|
|
4274
|
+
function buildSoftDeleteByIdQuery(table, id, deletedAt) {
|
|
4275
|
+
const deletedAtColumn = resolveSoftDeleteColumn(table);
|
|
4276
|
+
if (!deletedAtColumn) {
|
|
4277
|
+
throw new Error(`Table ${table.name} does not support soft deletes.`);
|
|
4733
4278
|
}
|
|
4734
|
-
|
|
4735
|
-
const
|
|
4279
|
+
const returningColumns = buildReturningColumns(table);
|
|
4280
|
+
const scopeClauses = [];
|
|
4281
|
+
appendSoftDeleteScope(table, {}, scopeClauses);
|
|
4282
|
+
const scopeSuffix = scopeClauses.length > 0 ? ` AND ${scopeClauses.join(" AND ")}` : "";
|
|
4283
|
+
const params = [];
|
|
4284
|
+
const deletedAtPlaceholder = pushParam(params, deletedAt);
|
|
4285
|
+
const idPlaceholder = pushParam(params, id);
|
|
4736
4286
|
return {
|
|
4737
|
-
|
|
4287
|
+
text: `UPDATE ${quoteIdentifier(table.name)} SET ${quoteIdentifier(deletedAtColumn)} = ${deletedAtPlaceholder} WHERE ${quoteIdentifier(table.primaryKey)} = ${idPlaceholder}${scopeSuffix}${returningSuffix(returningColumns)}`,
|
|
4738
4288
|
params
|
|
4739
4289
|
};
|
|
4740
4290
|
}
|
|
4741
|
-
function
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
if (table.softDeletes === true) {
|
|
4746
|
-
return "deleted_at";
|
|
4291
|
+
function buildRestoreByIdQuery(table, id) {
|
|
4292
|
+
const deletedAtColumn = resolveSoftDeleteColumn(table);
|
|
4293
|
+
if (!deletedAtColumn) {
|
|
4294
|
+
throw new Error(`Table ${table.name} does not support soft deletes.`);
|
|
4747
4295
|
}
|
|
4748
|
-
|
|
4296
|
+
const returningColumns = buildReturningColumns(table);
|
|
4297
|
+
const params = [];
|
|
4298
|
+
const deletedAtPlaceholder = pushParam(params, null);
|
|
4299
|
+
const idPlaceholder = pushParam(params, id);
|
|
4300
|
+
return {
|
|
4301
|
+
text: `UPDATE ${quoteIdentifier(table.name)} SET ${quoteIdentifier(deletedAtColumn)} = ${deletedAtPlaceholder} WHERE ${quoteIdentifier(table.primaryKey)} = ${idPlaceholder} AND ${qualifyColumn(table.name, deletedAtColumn)} IS NOT NULL${returningSuffix(returningColumns)}`,
|
|
4302
|
+
params
|
|
4303
|
+
};
|
|
4749
4304
|
}
|
|
4750
|
-
function
|
|
4751
|
-
const
|
|
4752
|
-
|
|
4753
|
-
|
|
4754
|
-
|
|
4755
|
-
|
|
4756
|
-
|
|
4757
|
-
|
|
4758
|
-
return;
|
|
4759
|
-
}
|
|
4760
|
-
if (!options.withTrashed) {
|
|
4761
|
-
clauses.push(`${qualifiedColumn} IS NULL`);
|
|
4762
|
-
}
|
|
4305
|
+
function buildDeleteByIdQuery(table, id) {
|
|
4306
|
+
const params = [];
|
|
4307
|
+
const idPlaceholder = pushParam(params, id);
|
|
4308
|
+
const returning = returningSuffix(`${quoteIdentifier(table.primaryKey)} AS ${quoteIdentifier("deleted_id")}`);
|
|
4309
|
+
return {
|
|
4310
|
+
text: `DELETE FROM ${quoteIdentifier(table.name)} WHERE ${quoteIdentifier(table.primaryKey)} = ${idPlaceholder}${returning}`,
|
|
4311
|
+
params
|
|
4312
|
+
};
|
|
4763
4313
|
}
|
|
4764
|
-
function
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
|
|
4769
|
-
|
|
4314
|
+
function buildConditionalWriteQuery(table, operation, changes, options, returning) {
|
|
4315
|
+
for (const key of [
|
|
4316
|
+
"joins",
|
|
4317
|
+
"groupBy",
|
|
4318
|
+
"having",
|
|
4319
|
+
"select",
|
|
4320
|
+
"orderBy",
|
|
4321
|
+
"limit",
|
|
4322
|
+
"offset",
|
|
4323
|
+
"lock"
|
|
4324
|
+
]) {
|
|
4325
|
+
if (options[key] !== undefined)
|
|
4326
|
+
throw new Error(`Conditional writes do not support ${key}.`);
|
|
4770
4327
|
}
|
|
4771
|
-
const
|
|
4772
|
-
const
|
|
4328
|
+
const params = [];
|
|
4329
|
+
const predicate = buildAdvancedWhereClause(table.name, options.where ?? {}, options.whereNodes ?? []);
|
|
4330
|
+
if (!predicate.clause)
|
|
4331
|
+
throw new Error("Conditional writes require an explicit predicate.");
|
|
4332
|
+
if (returning !== undefined && currentSqlDialect().driver === "mysql")
|
|
4333
|
+
throw new Error("Mutation RETURNING is not supported on MySQL.");
|
|
4334
|
+
const columns = returning ?? [table.primaryKey];
|
|
4335
|
+
if (!columns.length || columns.some((column) => !table.columns.includes(column)))
|
|
4336
|
+
throw new Error("RETURNING columns must be nonempty declared table columns.");
|
|
4337
|
+
const deletedAt = resolveSoftDeleteColumn(table);
|
|
4338
|
+
const values = operation === "delete" && deletedAt ? { [deletedAt]: new Date } : changes;
|
|
4339
|
+
let prefix;
|
|
4340
|
+
if (operation === "update" || deletedAt) {
|
|
4341
|
+
const entries = Object.entries(values).filter(([, value]) => value !== undefined);
|
|
4342
|
+
if (!entries.length)
|
|
4343
|
+
throw new Error("Conditional updates require at least one value.");
|
|
4344
|
+
if (entries.some(([column]) => column === table.primaryKey || !table.columns.includes(column)))
|
|
4345
|
+
throw new Error("Conditional updates require declared non-primary-key columns.");
|
|
4346
|
+
const assignments = entries.map(([column, value]) => `${quoteIdentifier(column)} = ${pushParam(params, value)}`);
|
|
4347
|
+
prefix = `UPDATE ${quoteIdentifier(table.name)} SET ${assignments.join(", ")}`;
|
|
4348
|
+
} else
|
|
4349
|
+
prefix = `DELETE FROM ${quoteIdentifier(table.name)}`;
|
|
4350
|
+
const { clause } = buildQueryWhereClause(table, options, options.whereNodes ?? [], params);
|
|
4773
4351
|
return {
|
|
4774
|
-
|
|
4775
|
-
params
|
|
4352
|
+
text: prefix + clause + returningSuffix(columns.map(quoteIdentifier).join(", ")),
|
|
4353
|
+
params
|
|
4776
4354
|
};
|
|
4777
4355
|
}
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
}
|
|
4785
|
-
if (Array.isArray(orderBy)) {
|
|
4786
|
-
return orderBy;
|
|
4787
|
-
}
|
|
4788
|
-
if (isQueryOrder(orderBy)) {
|
|
4789
|
-
return [orderBy];
|
|
4790
|
-
}
|
|
4791
|
-
return Object.entries(orderBy).map(([column, direction]) => ({
|
|
4792
|
-
column,
|
|
4793
|
-
direction
|
|
4794
|
-
}));
|
|
4356
|
+
|
|
4357
|
+
// ../../src/core/database/relationships.ts
|
|
4358
|
+
function hasMany(definition) {
|
|
4359
|
+
return {
|
|
4360
|
+
type: "hasMany",
|
|
4361
|
+
...definition
|
|
4362
|
+
};
|
|
4795
4363
|
}
|
|
4796
|
-
function
|
|
4797
|
-
|
|
4798
|
-
|
|
4799
|
-
|
|
4800
|
-
|
|
4801
|
-
const reference = resolveQualifiedColumn(tableName, column);
|
|
4802
|
-
const ordered = `${reference} ${normalizeDirection(direction)}`;
|
|
4803
|
-
if (nulls === undefined)
|
|
4804
|
-
return ordered;
|
|
4805
|
-
if (currentSqlDialect().driver === "mysql") {
|
|
4806
|
-
return `(${reference} IS NULL) ${nulls === "first" ? "DESC" : "ASC"}, ${ordered}`;
|
|
4807
|
-
}
|
|
4808
|
-
return `${ordered} NULLS ${nulls.toUpperCase()}`;
|
|
4809
|
-
});
|
|
4810
|
-
return parts.length > 0 ? ` ORDER BY ${parts.join(", ")}` : "";
|
|
4364
|
+
function hasOne(definition) {
|
|
4365
|
+
return {
|
|
4366
|
+
type: "hasOne",
|
|
4367
|
+
...definition
|
|
4368
|
+
};
|
|
4811
4369
|
}
|
|
4812
|
-
function
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
|
|
4816
|
-
|
|
4817
|
-
const parts = columns.map((column) => resolveQualifiedColumn(tableName, column));
|
|
4818
|
-
return parts.length > 0 ? ` GROUP BY ${parts.join(", ")}` : "";
|
|
4370
|
+
function belongsTo(definition) {
|
|
4371
|
+
return {
|
|
4372
|
+
type: "belongsTo",
|
|
4373
|
+
...definition
|
|
4374
|
+
};
|
|
4819
4375
|
}
|
|
4820
|
-
function
|
|
4821
|
-
|
|
4822
|
-
|
|
4823
|
-
|
|
4824
|
-
|
|
4825
|
-
|
|
4376
|
+
function hasManyThrough(definition) {
|
|
4377
|
+
return {
|
|
4378
|
+
type: "hasManyThrough",
|
|
4379
|
+
throughParentKey: "__through_parent_id",
|
|
4380
|
+
...definition
|
|
4381
|
+
};
|
|
4826
4382
|
}
|
|
4827
|
-
function
|
|
4828
|
-
return
|
|
4829
|
-
|
|
4830
|
-
|
|
4831
|
-
|
|
4832
|
-
}).join("");
|
|
4383
|
+
function belongsToMany(definition) {
|
|
4384
|
+
return {
|
|
4385
|
+
type: "belongsToMany",
|
|
4386
|
+
...definition
|
|
4387
|
+
};
|
|
4833
4388
|
}
|
|
4834
|
-
function
|
|
4835
|
-
if (
|
|
4389
|
+
function relationMatchKey(value) {
|
|
4390
|
+
if (value === null || value === undefined) {
|
|
4836
4391
|
return "";
|
|
4837
4392
|
}
|
|
4838
|
-
if (
|
|
4839
|
-
|
|
4393
|
+
if (typeof value === "bigint") {
|
|
4394
|
+
return value.toString();
|
|
4840
4395
|
}
|
|
4841
|
-
|
|
4842
|
-
|
|
4843
|
-
function buildOffsetClause(offset) {
|
|
4844
|
-
if (offset === undefined) {
|
|
4845
|
-
return "";
|
|
4396
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
4397
|
+
return String(value);
|
|
4846
4398
|
}
|
|
4847
|
-
if (
|
|
4848
|
-
|
|
4399
|
+
if (typeof value === "string" && /^-?\d+$/.test(value)) {
|
|
4400
|
+
return BigInt(value).toString();
|
|
4849
4401
|
}
|
|
4850
|
-
return
|
|
4402
|
+
return String(value);
|
|
4851
4403
|
}
|
|
4852
|
-
|
|
4853
|
-
|
|
4404
|
+
var relationLookupCache = new WeakMap;
|
|
4405
|
+
function indexedRelationLookup(map) {
|
|
4406
|
+
const cached = relationLookupCache.get(map);
|
|
4407
|
+
if (cached) {
|
|
4408
|
+
return cached;
|
|
4409
|
+
}
|
|
4410
|
+
const indexed = new Map;
|
|
4411
|
+
for (const [existing, value] of map) {
|
|
4412
|
+
indexed.set(relationMatchKey(existing), value);
|
|
4413
|
+
}
|
|
4414
|
+
relationLookupCache.set(map, indexed);
|
|
4415
|
+
return indexed;
|
|
4854
4416
|
}
|
|
4855
|
-
function
|
|
4856
|
-
if (
|
|
4857
|
-
return
|
|
4417
|
+
function getByRelationKey(map, key) {
|
|
4418
|
+
if (map.has(key)) {
|
|
4419
|
+
return map.get(key);
|
|
4858
4420
|
}
|
|
4859
|
-
|
|
4860
|
-
|
|
4861
|
-
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
|
|
4866
|
-
|
|
4867
|
-
|
|
4868
|
-
|
|
4421
|
+
const want = relationMatchKey(key);
|
|
4422
|
+
if (want === "") {
|
|
4423
|
+
return;
|
|
4424
|
+
}
|
|
4425
|
+
return indexedRelationLookup(map).get(want);
|
|
4426
|
+
}
|
|
4427
|
+
function indexHasManyRelation(parents, children, relation) {
|
|
4428
|
+
const groups = new Map;
|
|
4429
|
+
const originalKeys = new Map;
|
|
4430
|
+
for (const parent of parents) {
|
|
4431
|
+
const key = relationMatchKey(parent[relation.localKey]);
|
|
4432
|
+
if (!groups.has(key)) {
|
|
4433
|
+
groups.set(key, []);
|
|
4434
|
+
originalKeys.set(key, parent[relation.localKey]);
|
|
4869
4435
|
}
|
|
4870
|
-
|
|
4871
|
-
|
|
4436
|
+
}
|
|
4437
|
+
for (const child of children) {
|
|
4438
|
+
const group = groups.get(relationMatchKey(child[relation.foreignKey]));
|
|
4439
|
+
if (!group) {
|
|
4440
|
+
continue;
|
|
4872
4441
|
}
|
|
4873
|
-
|
|
4874
|
-
|
|
4875
|
-
|
|
4442
|
+
group.push(child);
|
|
4443
|
+
}
|
|
4444
|
+
const result = new Map;
|
|
4445
|
+
for (const [key, group] of groups) {
|
|
4446
|
+
const original = originalKeys.get(key);
|
|
4447
|
+
if (original !== undefined) {
|
|
4448
|
+
result.set(original, group);
|
|
4876
4449
|
}
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
return `ts_rank(${column}, plainto_tsquery('english', ${placeholder})) AS ${quoteIdentifier(item.as)}`;
|
|
4880
|
-
}).join(", ");
|
|
4450
|
+
}
|
|
4451
|
+
return result;
|
|
4881
4452
|
}
|
|
4882
|
-
function
|
|
4883
|
-
const
|
|
4884
|
-
const
|
|
4885
|
-
|
|
4886
|
-
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
|
|
4890
|
-
if (value === undefined) {
|
|
4891
|
-
return [];
|
|
4892
|
-
}
|
|
4893
|
-
return [[column, value]];
|
|
4894
|
-
});
|
|
4453
|
+
function indexHasOneRelation(parents, children, relation) {
|
|
4454
|
+
const grouped = indexHasManyRelation(parents, children, relation);
|
|
4455
|
+
const result = new Map;
|
|
4456
|
+
for (const parent of parents) {
|
|
4457
|
+
const matches = getByRelationKey(grouped, parent[relation.localKey]) ?? [];
|
|
4458
|
+
result.set(parent[relation.localKey], matches[0]);
|
|
4459
|
+
}
|
|
4460
|
+
return result;
|
|
4895
4461
|
}
|
|
4896
|
-
function
|
|
4897
|
-
const
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
const driver = currentSqlDialect().driver;
|
|
4901
|
-
if (driver === "sqlite")
|
|
4902
|
-
throw new Error("Row locks are not supported on SQLite; use an explicit write transaction.");
|
|
4903
|
-
if (lock.mode !== "update" && lock.mode !== "share")
|
|
4904
|
-
throw new TypeError("Invalid row lock mode.");
|
|
4905
|
-
if (lock.wait !== undefined && !["wait", "nowait", "skipLocked"].includes(lock.wait))
|
|
4906
|
-
throw new TypeError("Invalid row lock wait policy.");
|
|
4907
|
-
if (options.groupBy || options.having)
|
|
4908
|
-
throw new Error("Row locks cannot be combined with grouped/aggregate queries.");
|
|
4909
|
-
let target = "";
|
|
4910
|
-
if (lock.of !== undefined) {
|
|
4911
|
-
if (driver !== "pgsql")
|
|
4912
|
-
throw new Error("Row lock targets are supported only on PostgreSQL.");
|
|
4913
|
-
const tables = [table.name, ...(options.joins ?? []).map((join) => join.table)];
|
|
4914
|
-
if (!lock.of.length || lock.of.some((name) => !tables.includes(name)))
|
|
4915
|
-
throw new TypeError("Row lock targets must name tables in this query.");
|
|
4916
|
-
target = ` OF ${lock.of.map(quoteIdentifier).join(", ")}`;
|
|
4462
|
+
function indexBelongsToRelation(children, parents, relation) {
|
|
4463
|
+
const parentsById = new Map;
|
|
4464
|
+
for (const parent of parents) {
|
|
4465
|
+
parentsById.set(relationMatchKey(parent[relation.ownerKey]), parent);
|
|
4917
4466
|
}
|
|
4918
|
-
const
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
const havingClause = buildHavingClause(table.name, options.having, params);
|
|
4928
|
-
const orderBy = buildOrderByClause(table.name, options.orderBy ?? table.defaultOrderBy);
|
|
4929
|
-
const limit = buildLimitClause(options.limit);
|
|
4930
|
-
const offset = buildOffsetClause(options.offset);
|
|
4931
|
-
const lock = buildSelectLock(table, options);
|
|
4932
|
-
return {
|
|
4933
|
-
text: `SELECT ${columns} FROM ${quoteIdentifier(table.name)}${joins}${clause}${groupBy}${havingClause}${orderBy}${limit}${offset}${lock}`,
|
|
4934
|
-
params
|
|
4935
|
-
};
|
|
4936
|
-
}
|
|
4937
|
-
function buildCountQuery(table, where = {}, options = {}, whereNodes = []) {
|
|
4938
|
-
const params = [];
|
|
4939
|
-
const { clause, params: whereParams } = buildQueryWhereClause(table, {
|
|
4940
|
-
where,
|
|
4941
|
-
withTrashed: options.withTrashed,
|
|
4942
|
-
onlyTrashed: options.onlyTrashed
|
|
4943
|
-
}, whereNodes);
|
|
4944
|
-
params.push(...whereParams);
|
|
4945
|
-
const joins = buildJoinClause(options.joins);
|
|
4946
|
-
const groupBy = buildGroupByClause(table.name, options.groupBy);
|
|
4947
|
-
return {
|
|
4948
|
-
text: `SELECT COUNT(*) AS count FROM ${quoteIdentifier(table.name)}${joins}${clause}${groupBy}`,
|
|
4949
|
-
params
|
|
4950
|
-
};
|
|
4951
|
-
}
|
|
4952
|
-
function buildProjectionQuery(table, expression, alias, options = {}, whereNodes = []) {
|
|
4953
|
-
assertSafeProjectionExpression(expression);
|
|
4954
|
-
const params = [];
|
|
4955
|
-
const { clause, params: whereParams } = buildQueryWhereClause(table, options, whereNodes);
|
|
4956
|
-
params.push(...whereParams);
|
|
4957
|
-
const joins = buildJoinClause(options.joins);
|
|
4958
|
-
const groupBy = buildGroupByClause(table.name, options.groupBy);
|
|
4959
|
-
const orderBy = buildOrderByClause(table.name, options.orderBy);
|
|
4960
|
-
const limit = buildLimitClause(options.limit);
|
|
4961
|
-
return {
|
|
4962
|
-
text: `SELECT ${expression} AS ${quoteIdentifier(alias)} FROM ${quoteIdentifier(table.name)}${joins}${clause}${groupBy}${orderBy}${limit}`,
|
|
4963
|
-
params
|
|
4964
|
-
};
|
|
4467
|
+
const result = new Map;
|
|
4468
|
+
for (const child of children) {
|
|
4469
|
+
const foreignKey = child[relation.foreignKey];
|
|
4470
|
+
const parent = parentsById.get(relationMatchKey(foreignKey));
|
|
4471
|
+
if (parent) {
|
|
4472
|
+
result.set(foreignKey, parent);
|
|
4473
|
+
}
|
|
4474
|
+
}
|
|
4475
|
+
return result;
|
|
4965
4476
|
}
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
4477
|
+
function indexBelongsToManyRelation(parents, pivotRows, relatedRows, relation) {
|
|
4478
|
+
const relatedById = new Map;
|
|
4479
|
+
for (const related of relatedRows) {
|
|
4480
|
+
relatedById.set(relationMatchKey(related[relation.relatedKey]), related);
|
|
4481
|
+
}
|
|
4482
|
+
const groups = new Map;
|
|
4483
|
+
const originalKeys = new Map;
|
|
4484
|
+
for (const parent of parents) {
|
|
4485
|
+
const key = relationMatchKey(parent[relation.parentKey]);
|
|
4486
|
+
if (!groups.has(key)) {
|
|
4487
|
+
groups.set(key, []);
|
|
4488
|
+
originalKeys.set(key, parent[relation.parentKey]);
|
|
4489
|
+
}
|
|
4490
|
+
}
|
|
4491
|
+
for (const pivot of pivotRows) {
|
|
4492
|
+
const group = groups.get(relationMatchKey(pivot[relation.foreignPivotKey]));
|
|
4493
|
+
const related = relatedById.get(relationMatchKey(pivot[relation.relatedPivotKey]));
|
|
4494
|
+
if (!group || !related) {
|
|
4495
|
+
continue;
|
|
4496
|
+
}
|
|
4497
|
+
group.push(related);
|
|
4498
|
+
}
|
|
4499
|
+
const result = new Map;
|
|
4500
|
+
for (const [key, group] of groups) {
|
|
4501
|
+
const original = originalKeys.get(key);
|
|
4502
|
+
if (original !== undefined) {
|
|
4503
|
+
result.set(original, group);
|
|
4504
|
+
}
|
|
4970
4505
|
}
|
|
4506
|
+
return result;
|
|
4971
4507
|
}
|
|
4972
|
-
function
|
|
4973
|
-
if (!table.columns.includes(groupColumn) || !table.columns.includes(column))
|
|
4974
|
-
throw new TypeError("Grouped sums require declared columns.");
|
|
4975
|
-
for (const key of Object.keys(options))
|
|
4976
|
-
if (!["where", "whereNodes", "withTrashed", "onlyTrashed"].includes(key))
|
|
4977
|
-
throw new TypeError("Grouped sums do not support joins, selection, ordering, pagination, grouping or locks.");
|
|
4978
|
-
const group = qualifyColumn(table.name, groupColumn);
|
|
4979
|
-
const total = currentSqlDialect().castToText(`SUM(${qualifyColumn(table.name, column)})`);
|
|
4980
|
-
const { clause, params } = buildQueryWhereClause(table, options, whereNodes);
|
|
4508
|
+
function morphMany(definition) {
|
|
4981
4509
|
return {
|
|
4982
|
-
|
|
4983
|
-
|
|
4510
|
+
type: "morphMany",
|
|
4511
|
+
...definition
|
|
4984
4512
|
};
|
|
4985
4513
|
}
|
|
4986
|
-
function
|
|
4987
|
-
const qualifiedColumn = qualifyColumn(table.name, column);
|
|
4988
|
-
const { clause, params } = buildQueryWhereClause(table, {
|
|
4989
|
-
where,
|
|
4990
|
-
...options
|
|
4991
|
-
});
|
|
4514
|
+
function morphOne(definition) {
|
|
4992
4515
|
return {
|
|
4993
|
-
|
|
4994
|
-
|
|
4516
|
+
type: "morphOne",
|
|
4517
|
+
...definition
|
|
4995
4518
|
};
|
|
4996
4519
|
}
|
|
4997
|
-
function
|
|
4998
|
-
const entries = getDefinedColumnEntries(table, values);
|
|
4999
|
-
if (entries.length === 0) {
|
|
5000
|
-
throw new Error(`Cannot insert into ${table.name} without any column values.`);
|
|
5001
|
-
}
|
|
5002
|
-
const params = [];
|
|
5003
|
-
const columns = entries.map(([column]) => quoteIdentifier(column)).join(", ");
|
|
5004
|
-
const placeholders = entries.map(([, value]) => pushParam(params, value)).join(", ");
|
|
5005
|
-
const returningColumns = buildReturningColumns(table);
|
|
4520
|
+
function morphTo(definition) {
|
|
5006
4521
|
return {
|
|
5007
|
-
|
|
5008
|
-
|
|
4522
|
+
type: "morphTo",
|
|
4523
|
+
...definition
|
|
5009
4524
|
};
|
|
5010
4525
|
}
|
|
5011
|
-
function
|
|
5012
|
-
const
|
|
5013
|
-
|
|
5014
|
-
|
|
4526
|
+
function indexMorphManyRelation(parents, children, relation) {
|
|
4527
|
+
const groups = new Map;
|
|
4528
|
+
const originalKeys = new Map;
|
|
4529
|
+
for (const parent of parents) {
|
|
4530
|
+
const key = relationMatchKey(parent[relation.localKey]);
|
|
4531
|
+
if (!groups.has(key)) {
|
|
4532
|
+
groups.set(key, []);
|
|
4533
|
+
originalKeys.set(key, parent[relation.localKey]);
|
|
4534
|
+
}
|
|
5015
4535
|
}
|
|
5016
|
-
|
|
5017
|
-
|
|
4536
|
+
for (const child of children) {
|
|
4537
|
+
if (child[relation.morphTypeKey] !== relation.morphType) {
|
|
4538
|
+
continue;
|
|
4539
|
+
}
|
|
4540
|
+
const group = groups.get(relationMatchKey(child[relation.morphIdKey]));
|
|
4541
|
+
if (!group) {
|
|
4542
|
+
continue;
|
|
4543
|
+
}
|
|
4544
|
+
group.push(child);
|
|
5018
4545
|
}
|
|
5019
|
-
const
|
|
5020
|
-
const
|
|
5021
|
-
|
|
5022
|
-
|
|
5023
|
-
|
|
5024
|
-
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
return {
|
|
5028
|
-
text: `INSERT INTO ${quoteIdentifier(table.name)} (${columns}) VALUES (${placeholders})${suffix}${returningSuffix(returningColumns)}`,
|
|
5029
|
-
params
|
|
5030
|
-
};
|
|
4546
|
+
const result = new Map;
|
|
4547
|
+
for (const [key, group] of groups) {
|
|
4548
|
+
const original = originalKeys.get(key);
|
|
4549
|
+
if (original !== undefined) {
|
|
4550
|
+
result.set(original, group);
|
|
4551
|
+
}
|
|
4552
|
+
}
|
|
4553
|
+
return result;
|
|
5031
4554
|
}
|
|
5032
|
-
function
|
|
5033
|
-
|
|
5034
|
-
|
|
4555
|
+
function indexMorphOneRelation(parents, children, relation) {
|
|
4556
|
+
const grouped = indexMorphManyRelation(parents, children, relation);
|
|
4557
|
+
const result = new Map;
|
|
4558
|
+
for (const parent of parents) {
|
|
4559
|
+
const matches = getByRelationKey(grouped, parent[relation.localKey]) ?? [];
|
|
4560
|
+
result.set(parent[relation.localKey], matches[0]);
|
|
5035
4561
|
}
|
|
5036
|
-
|
|
5037
|
-
|
|
4562
|
+
return result;
|
|
4563
|
+
}
|
|
4564
|
+
function indexHasManyThroughRelation(parents, children, relation) {
|
|
4565
|
+
const throughKey = relation.throughParentKey ?? "__through_parent_id";
|
|
4566
|
+
const grouped = new Map;
|
|
4567
|
+
for (const child of children) {
|
|
4568
|
+
const key = relationMatchKey(child[throughKey]);
|
|
4569
|
+
if (key === "") {
|
|
4570
|
+
continue;
|
|
4571
|
+
}
|
|
4572
|
+
const existing = grouped.get(key) ?? [];
|
|
4573
|
+
const { [throughKey]: _through, ...far } = child;
|
|
4574
|
+
existing.push(far);
|
|
4575
|
+
grouped.set(key, existing);
|
|
5038
4576
|
}
|
|
5039
|
-
const
|
|
5040
|
-
const
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
exclude: [table.primaryKey, column]
|
|
5044
|
-
})) {
|
|
5045
|
-
assignments.push(`${quoteIdentifier(name)} = ${pushParam(params, value)}`);
|
|
4577
|
+
const result = new Map;
|
|
4578
|
+
for (const parent of parents) {
|
|
4579
|
+
const key = relationMatchKey(parent[relation.localKey]);
|
|
4580
|
+
result.set(parent[relation.localKey], grouped.get(key) ?? []);
|
|
5046
4581
|
}
|
|
5047
|
-
|
|
5048
|
-
const returningColumns = buildReturningColumns(table);
|
|
5049
|
-
const scopeClauses = [];
|
|
5050
|
-
appendSoftDeleteScope(table, {}, scopeClauses);
|
|
5051
|
-
const scopeSuffix = scopeClauses.length > 0 ? ` AND ${scopeClauses.join(" AND ")}` : "";
|
|
5052
|
-
return {
|
|
5053
|
-
text: `UPDATE ${quoteIdentifier(table.name)} SET ${assignments.join(", ")} WHERE ${quoteIdentifier(table.primaryKey)} = ${primaryKeyPlaceholder}${scopeSuffix}${returningSuffix(returningColumns)}`,
|
|
5054
|
-
params
|
|
5055
|
-
};
|
|
4582
|
+
return result;
|
|
5056
4583
|
}
|
|
5057
|
-
function
|
|
5058
|
-
const
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
appendSoftDeleteScope(table, {}, scopeClauses);
|
|
5070
|
-
const scopeSuffix = scopeClauses.length > 0 ? ` AND ${scopeClauses.join(" AND ")}` : "";
|
|
5071
|
-
return {
|
|
5072
|
-
text: `UPDATE ${quoteIdentifier(table.name)} SET ${setClause} WHERE ${quoteIdentifier(table.primaryKey)} = ${primaryKeyPlaceholder}${scopeSuffix}${returningSuffix(returningColumns)}`,
|
|
5073
|
-
params
|
|
5074
|
-
};
|
|
5075
|
-
}
|
|
5076
|
-
function buildSoftDeleteByIdQuery(table, id, deletedAt) {
|
|
5077
|
-
const deletedAtColumn = resolveSoftDeleteColumn(table);
|
|
5078
|
-
if (!deletedAtColumn) {
|
|
5079
|
-
throw new Error(`Table ${table.name} does not support soft deletes.`);
|
|
5080
|
-
}
|
|
5081
|
-
const returningColumns = buildReturningColumns(table);
|
|
5082
|
-
const scopeClauses = [];
|
|
5083
|
-
appendSoftDeleteScope(table, {}, scopeClauses);
|
|
5084
|
-
const scopeSuffix = scopeClauses.length > 0 ? ` AND ${scopeClauses.join(" AND ")}` : "";
|
|
5085
|
-
const params = [];
|
|
5086
|
-
const deletedAtPlaceholder = pushParam(params, deletedAt);
|
|
5087
|
-
const idPlaceholder = pushParam(params, id);
|
|
5088
|
-
return {
|
|
5089
|
-
text: `UPDATE ${quoteIdentifier(table.name)} SET ${quoteIdentifier(deletedAtColumn)} = ${deletedAtPlaceholder} WHERE ${quoteIdentifier(table.primaryKey)} = ${idPlaceholder}${scopeSuffix}${returningSuffix(returningColumns)}`,
|
|
5090
|
-
params
|
|
5091
|
-
};
|
|
5092
|
-
}
|
|
5093
|
-
function buildRestoreByIdQuery(table, id) {
|
|
5094
|
-
const deletedAtColumn = resolveSoftDeleteColumn(table);
|
|
5095
|
-
if (!deletedAtColumn) {
|
|
5096
|
-
throw new Error(`Table ${table.name} does not support soft deletes.`);
|
|
5097
|
-
}
|
|
5098
|
-
const returningColumns = buildReturningColumns(table);
|
|
5099
|
-
const params = [];
|
|
5100
|
-
const deletedAtPlaceholder = pushParam(params, null);
|
|
5101
|
-
const idPlaceholder = pushParam(params, id);
|
|
5102
|
-
return {
|
|
5103
|
-
text: `UPDATE ${quoteIdentifier(table.name)} SET ${quoteIdentifier(deletedAtColumn)} = ${deletedAtPlaceholder} WHERE ${quoteIdentifier(table.primaryKey)} = ${idPlaceholder} AND ${qualifyColumn(table.name, deletedAtColumn)} IS NOT NULL${returningSuffix(returningColumns)}`,
|
|
5104
|
-
params
|
|
5105
|
-
};
|
|
5106
|
-
}
|
|
5107
|
-
function buildDeleteByIdQuery(table, id) {
|
|
5108
|
-
const params = [];
|
|
5109
|
-
const idPlaceholder = pushParam(params, id);
|
|
5110
|
-
const returning = returningSuffix(`${quoteIdentifier(table.primaryKey)} AS ${quoteIdentifier("deleted_id")}`);
|
|
5111
|
-
return {
|
|
5112
|
-
text: `DELETE FROM ${quoteIdentifier(table.name)} WHERE ${quoteIdentifier(table.primaryKey)} = ${idPlaceholder}${returning}`,
|
|
5113
|
-
params
|
|
5114
|
-
};
|
|
5115
|
-
}
|
|
5116
|
-
function buildConditionalWriteQuery(table, operation, changes, options, returning) {
|
|
5117
|
-
for (const key of [
|
|
5118
|
-
"joins",
|
|
5119
|
-
"groupBy",
|
|
5120
|
-
"having",
|
|
5121
|
-
"select",
|
|
5122
|
-
"orderBy",
|
|
5123
|
-
"limit",
|
|
5124
|
-
"offset",
|
|
5125
|
-
"lock"
|
|
5126
|
-
]) {
|
|
5127
|
-
if (options[key] !== undefined)
|
|
5128
|
-
throw new Error(`Conditional writes do not support ${key}.`);
|
|
4584
|
+
function indexMorphToRelation(children, parentsByType, relation) {
|
|
4585
|
+
const result = new Map;
|
|
4586
|
+
for (const child of children) {
|
|
4587
|
+
const morphType = String(child[relation.morphTypeKey]);
|
|
4588
|
+
const parents = parentsByType.get(morphType);
|
|
4589
|
+
if (!parents) {
|
|
4590
|
+
continue;
|
|
4591
|
+
}
|
|
4592
|
+
const parent = getByRelationKey(parents, child[relation.morphIdKey]);
|
|
4593
|
+
if (parent) {
|
|
4594
|
+
result.set(child[relation.morphIdKey], parent);
|
|
4595
|
+
}
|
|
5129
4596
|
}
|
|
5130
|
-
|
|
5131
|
-
const predicate = buildAdvancedWhereClause(table.name, options.where ?? {}, options.whereNodes ?? []);
|
|
5132
|
-
if (!predicate.clause)
|
|
5133
|
-
throw new Error("Conditional writes require an explicit predicate.");
|
|
5134
|
-
if (returning !== undefined && currentSqlDialect().driver === "mysql")
|
|
5135
|
-
throw new Error("Mutation RETURNING is not supported on MySQL.");
|
|
5136
|
-
const columns = returning ?? [table.primaryKey];
|
|
5137
|
-
if (!columns.length || columns.some((column) => !table.columns.includes(column)))
|
|
5138
|
-
throw new Error("RETURNING columns must be nonempty declared table columns.");
|
|
5139
|
-
const deletedAt = resolveSoftDeleteColumn(table);
|
|
5140
|
-
const values = operation === "delete" && deletedAt ? { [deletedAt]: new Date } : changes;
|
|
5141
|
-
let prefix;
|
|
5142
|
-
if (operation === "update" || deletedAt) {
|
|
5143
|
-
const entries = Object.entries(values).filter(([, value]) => value !== undefined);
|
|
5144
|
-
if (!entries.length)
|
|
5145
|
-
throw new Error("Conditional updates require at least one value.");
|
|
5146
|
-
if (entries.some(([column]) => column === table.primaryKey || !table.columns.includes(column)))
|
|
5147
|
-
throw new Error("Conditional updates require declared non-primary-key columns.");
|
|
5148
|
-
const assignments = entries.map(([column, value]) => `${quoteIdentifier(column)} = ${pushParam(params, value)}`);
|
|
5149
|
-
prefix = `UPDATE ${quoteIdentifier(table.name)} SET ${assignments.join(", ")}`;
|
|
5150
|
-
} else
|
|
5151
|
-
prefix = `DELETE FROM ${quoteIdentifier(table.name)}`;
|
|
5152
|
-
const { clause } = buildQueryWhereClause(table, options, options.whereNodes ?? [], params);
|
|
5153
|
-
return {
|
|
5154
|
-
text: prefix + clause + returningSuffix(columns.map(quoteIdentifier).join(", ")),
|
|
5155
|
-
params
|
|
5156
|
-
};
|
|
4597
|
+
return result;
|
|
5157
4598
|
}
|
|
5158
4599
|
|
|
5159
|
-
// ../../src/core/database/
|
|
5160
|
-
|
|
5161
|
-
|
|
5162
|
-
|
|
5163
|
-
|
|
5164
|
-
|
|
5165
|
-
|
|
5166
|
-
|
|
5167
|
-
|
|
5168
|
-
|
|
5169
|
-
|
|
5170
|
-
|
|
5171
|
-
}
|
|
5172
|
-
function belongsTo(definition) {
|
|
5173
|
-
return {
|
|
5174
|
-
type: "belongsTo",
|
|
5175
|
-
...definition
|
|
5176
|
-
};
|
|
5177
|
-
}
|
|
5178
|
-
function hasManyThrough(definition) {
|
|
5179
|
-
return {
|
|
5180
|
-
type: "hasManyThrough",
|
|
5181
|
-
throughParentKey: "__through_parent_id",
|
|
5182
|
-
...definition
|
|
5183
|
-
};
|
|
5184
|
-
}
|
|
5185
|
-
function belongsToMany(definition) {
|
|
5186
|
-
return {
|
|
5187
|
-
type: "belongsToMany",
|
|
5188
|
-
...definition
|
|
5189
|
-
};
|
|
5190
|
-
}
|
|
5191
|
-
function relationMatchKey(value) {
|
|
5192
|
-
if (value === null || value === undefined) {
|
|
5193
|
-
return "";
|
|
5194
|
-
}
|
|
5195
|
-
if (typeof value === "bigint") {
|
|
5196
|
-
return value.toString();
|
|
4600
|
+
// ../../src/core/database/baseRepository.ts
|
|
4601
|
+
init_repositoryConnection();
|
|
4602
|
+
|
|
4603
|
+
// ../../src/core/database/pluck.ts
|
|
4604
|
+
function uniqueColumnSelect(table, columns) {
|
|
4605
|
+
const seen = new Set;
|
|
4606
|
+
const select = [];
|
|
4607
|
+
for (const column of columns) {
|
|
4608
|
+
if (seen.has(column)) {
|
|
4609
|
+
continue;
|
|
4610
|
+
}
|
|
4611
|
+
seen.add(column);
|
|
4612
|
+
select.push({ kind: "column", table, column, as: column });
|
|
5197
4613
|
}
|
|
5198
|
-
|
|
5199
|
-
|
|
4614
|
+
return select;
|
|
4615
|
+
}
|
|
4616
|
+
function projectPluck(rows, column, keyBy) {
|
|
4617
|
+
if (keyBy === undefined) {
|
|
4618
|
+
return rows.map((row) => row[column]);
|
|
5200
4619
|
}
|
|
5201
|
-
|
|
5202
|
-
|
|
4620
|
+
const keyed = new Map;
|
|
4621
|
+
for (const row of rows) {
|
|
4622
|
+
keyed.set(row[keyBy], row[column]);
|
|
5203
4623
|
}
|
|
5204
|
-
return
|
|
4624
|
+
return keyed;
|
|
5205
4625
|
}
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
|
|
4626
|
+
|
|
4627
|
+
// ../../src/core/database/whereBuilder.ts
|
|
4628
|
+
class WhereBuilder {
|
|
4629
|
+
nodes = [];
|
|
4630
|
+
where(where) {
|
|
4631
|
+
this.nodes.push({ kind: "and", where });
|
|
4632
|
+
return this;
|
|
5211
4633
|
}
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
|
|
4634
|
+
orWhere(where) {
|
|
4635
|
+
this.nodes.push({ kind: "or", where });
|
|
4636
|
+
return this;
|
|
5215
4637
|
}
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
5221
|
-
|
|
4638
|
+
whereGroup(fn) {
|
|
4639
|
+
const nested = new WhereBuilder;
|
|
4640
|
+
fn(nested);
|
|
4641
|
+
if (nested.nodes.length > 0) {
|
|
4642
|
+
this.nodes.push({ kind: "and", group: nested.nodes });
|
|
4643
|
+
}
|
|
4644
|
+
return this;
|
|
5222
4645
|
}
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
|
|
4646
|
+
orWhereGroup(fn) {
|
|
4647
|
+
const nested = new WhereBuilder;
|
|
4648
|
+
fn(nested);
|
|
4649
|
+
if (nested.nodes.length > 0) {
|
|
4650
|
+
this.nodes.push({ kind: "or", group: nested.nodes });
|
|
4651
|
+
}
|
|
4652
|
+
return this;
|
|
5226
4653
|
}
|
|
5227
|
-
return indexedRelationLookup(map).get(want);
|
|
5228
4654
|
}
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
|
|
5232
|
-
|
|
5233
|
-
|
|
5234
|
-
|
|
5235
|
-
|
|
5236
|
-
|
|
5237
|
-
|
|
4655
|
+
|
|
4656
|
+
// ../../src/core/database/repositoryQuery.ts
|
|
4657
|
+
class RepositoryQuery {
|
|
4658
|
+
repository;
|
|
4659
|
+
whereClause;
|
|
4660
|
+
queryOptions;
|
|
4661
|
+
eagerLoads = [];
|
|
4662
|
+
whereNodes = [];
|
|
4663
|
+
protectedWhere = [];
|
|
4664
|
+
constructor(repository, whereClause = {}, queryOptions = {}) {
|
|
4665
|
+
this.repository = repository;
|
|
4666
|
+
this.whereClause = whereClause;
|
|
4667
|
+
this.queryOptions = queryOptions;
|
|
5238
4668
|
}
|
|
5239
|
-
|
|
5240
|
-
|
|
5241
|
-
|
|
5242
|
-
|
|
4669
|
+
where(input) {
|
|
4670
|
+
if (typeof input === "function") {
|
|
4671
|
+
const builder = new WhereBuilder;
|
|
4672
|
+
input(builder);
|
|
4673
|
+
if (builder.nodes.length > 0)
|
|
4674
|
+
this.whereNodes.push({ kind: "and", group: builder.nodes });
|
|
4675
|
+
return this;
|
|
5243
4676
|
}
|
|
5244
|
-
|
|
4677
|
+
this.whereNodes.push({ kind: "and", where: { ...input } });
|
|
4678
|
+
return this;
|
|
5245
4679
|
}
|
|
5246
|
-
|
|
5247
|
-
|
|
5248
|
-
|
|
5249
|
-
|
|
5250
|
-
result.set(original, group);
|
|
4680
|
+
protectWhere() {
|
|
4681
|
+
const nodes = [];
|
|
4682
|
+
if (Object.keys(this.whereClause).length > 0) {
|
|
4683
|
+
nodes.push({ kind: "and", where: this.whereClause });
|
|
5251
4684
|
}
|
|
5252
|
-
|
|
5253
|
-
|
|
5254
|
-
}
|
|
5255
|
-
|
|
5256
|
-
|
|
5257
|
-
|
|
5258
|
-
for (const parent of parents) {
|
|
5259
|
-
const matches = getByRelationKey(grouped, parent[relation.localKey]) ?? [];
|
|
5260
|
-
result.set(parent[relation.localKey], matches[0]);
|
|
5261
|
-
}
|
|
5262
|
-
return result;
|
|
5263
|
-
}
|
|
5264
|
-
function indexBelongsToRelation(children, parents, relation) {
|
|
5265
|
-
const parentsById = new Map;
|
|
5266
|
-
for (const parent of parents) {
|
|
5267
|
-
parentsById.set(relationMatchKey(parent[relation.ownerKey]), parent);
|
|
5268
|
-
}
|
|
5269
|
-
const result = new Map;
|
|
5270
|
-
for (const child of children) {
|
|
5271
|
-
const foreignKey = child[relation.foreignKey];
|
|
5272
|
-
const parent = parentsById.get(relationMatchKey(foreignKey));
|
|
5273
|
-
if (parent) {
|
|
5274
|
-
result.set(foreignKey, parent);
|
|
5275
|
-
}
|
|
5276
|
-
}
|
|
5277
|
-
return result;
|
|
5278
|
-
}
|
|
5279
|
-
function indexBelongsToManyRelation(parents, pivotRows, relatedRows, relation) {
|
|
5280
|
-
const relatedById = new Map;
|
|
5281
|
-
for (const related of relatedRows) {
|
|
5282
|
-
relatedById.set(relationMatchKey(related[relation.relatedKey]), related);
|
|
5283
|
-
}
|
|
5284
|
-
const groups = new Map;
|
|
5285
|
-
const originalKeys = new Map;
|
|
5286
|
-
for (const parent of parents) {
|
|
5287
|
-
const key = relationMatchKey(parent[relation.parentKey]);
|
|
5288
|
-
if (!groups.has(key)) {
|
|
5289
|
-
groups.set(key, []);
|
|
5290
|
-
originalKeys.set(key, parent[relation.parentKey]);
|
|
5291
|
-
}
|
|
5292
|
-
}
|
|
5293
|
-
for (const pivot of pivotRows) {
|
|
5294
|
-
const group = groups.get(relationMatchKey(pivot[relation.foreignPivotKey]));
|
|
5295
|
-
const related = relatedById.get(relationMatchKey(pivot[relation.relatedPivotKey]));
|
|
5296
|
-
if (!group || !related) {
|
|
5297
|
-
continue;
|
|
5298
|
-
}
|
|
5299
|
-
group.push(related);
|
|
5300
|
-
}
|
|
5301
|
-
const result = new Map;
|
|
5302
|
-
for (const [key, group] of groups) {
|
|
5303
|
-
const original = originalKeys.get(key);
|
|
5304
|
-
if (original !== undefined) {
|
|
5305
|
-
result.set(original, group);
|
|
5306
|
-
}
|
|
5307
|
-
}
|
|
5308
|
-
return result;
|
|
5309
|
-
}
|
|
5310
|
-
function morphMany(definition) {
|
|
5311
|
-
return {
|
|
5312
|
-
type: "morphMany",
|
|
5313
|
-
...definition
|
|
5314
|
-
};
|
|
5315
|
-
}
|
|
5316
|
-
function morphOne(definition) {
|
|
5317
|
-
return {
|
|
5318
|
-
type: "morphOne",
|
|
5319
|
-
...definition
|
|
5320
|
-
};
|
|
5321
|
-
}
|
|
5322
|
-
function morphTo(definition) {
|
|
5323
|
-
return {
|
|
5324
|
-
type: "morphTo",
|
|
5325
|
-
...definition
|
|
5326
|
-
};
|
|
5327
|
-
}
|
|
5328
|
-
function indexMorphManyRelation(parents, children, relation) {
|
|
5329
|
-
const groups = new Map;
|
|
5330
|
-
const originalKeys = new Map;
|
|
5331
|
-
for (const parent of parents) {
|
|
5332
|
-
const key = relationMatchKey(parent[relation.localKey]);
|
|
5333
|
-
if (!groups.has(key)) {
|
|
5334
|
-
groups.set(key, []);
|
|
5335
|
-
originalKeys.set(key, parent[relation.localKey]);
|
|
5336
|
-
}
|
|
5337
|
-
}
|
|
5338
|
-
for (const child of children) {
|
|
5339
|
-
if (child[relation.morphTypeKey] !== relation.morphType) {
|
|
5340
|
-
continue;
|
|
5341
|
-
}
|
|
5342
|
-
const group = groups.get(relationMatchKey(child[relation.morphIdKey]));
|
|
5343
|
-
if (!group) {
|
|
5344
|
-
continue;
|
|
5345
|
-
}
|
|
5346
|
-
group.push(child);
|
|
5347
|
-
}
|
|
5348
|
-
const result = new Map;
|
|
5349
|
-
for (const [key, group] of groups) {
|
|
5350
|
-
const original = originalKeys.get(key);
|
|
5351
|
-
if (original !== undefined) {
|
|
5352
|
-
result.set(original, group);
|
|
5353
|
-
}
|
|
5354
|
-
}
|
|
5355
|
-
return result;
|
|
5356
|
-
}
|
|
5357
|
-
function indexMorphOneRelation(parents, children, relation) {
|
|
5358
|
-
const grouped = indexMorphManyRelation(parents, children, relation);
|
|
5359
|
-
const result = new Map;
|
|
5360
|
-
for (const parent of parents) {
|
|
5361
|
-
const matches = getByRelationKey(grouped, parent[relation.localKey]) ?? [];
|
|
5362
|
-
result.set(parent[relation.localKey], matches[0]);
|
|
5363
|
-
}
|
|
5364
|
-
return result;
|
|
5365
|
-
}
|
|
5366
|
-
function indexHasManyThroughRelation(parents, children, relation) {
|
|
5367
|
-
const throughKey = relation.throughParentKey ?? "__through_parent_id";
|
|
5368
|
-
const grouped = new Map;
|
|
5369
|
-
for (const child of children) {
|
|
5370
|
-
const key = relationMatchKey(child[throughKey]);
|
|
5371
|
-
if (key === "") {
|
|
5372
|
-
continue;
|
|
5373
|
-
}
|
|
5374
|
-
const existing = grouped.get(key) ?? [];
|
|
5375
|
-
const { [throughKey]: _through, ...far } = child;
|
|
5376
|
-
existing.push(far);
|
|
5377
|
-
grouped.set(key, existing);
|
|
5378
|
-
}
|
|
5379
|
-
const result = new Map;
|
|
5380
|
-
for (const parent of parents) {
|
|
5381
|
-
const key = relationMatchKey(parent[relation.localKey]);
|
|
5382
|
-
result.set(parent[relation.localKey], grouped.get(key) ?? []);
|
|
5383
|
-
}
|
|
5384
|
-
return result;
|
|
5385
|
-
}
|
|
5386
|
-
function indexMorphToRelation(children, parentsByType, relation) {
|
|
5387
|
-
const result = new Map;
|
|
5388
|
-
for (const child of children) {
|
|
5389
|
-
const morphType = String(child[relation.morphTypeKey]);
|
|
5390
|
-
const parents = parentsByType.get(morphType);
|
|
5391
|
-
if (!parents) {
|
|
5392
|
-
continue;
|
|
5393
|
-
}
|
|
5394
|
-
const parent = getByRelationKey(parents, child[relation.morphIdKey]);
|
|
5395
|
-
if (parent) {
|
|
5396
|
-
result.set(child[relation.morphIdKey], parent);
|
|
5397
|
-
}
|
|
5398
|
-
}
|
|
5399
|
-
return result;
|
|
5400
|
-
}
|
|
5401
|
-
|
|
5402
|
-
// ../../src/core/database/baseRepository.ts
|
|
5403
|
-
init_repositoryConnection();
|
|
5404
|
-
|
|
5405
|
-
// ../../src/core/database/pluck.ts
|
|
5406
|
-
function uniqueColumnSelect(table, columns) {
|
|
5407
|
-
const seen = new Set;
|
|
5408
|
-
const select = [];
|
|
5409
|
-
for (const column of columns) {
|
|
5410
|
-
if (seen.has(column)) {
|
|
5411
|
-
continue;
|
|
5412
|
-
}
|
|
5413
|
-
seen.add(column);
|
|
5414
|
-
select.push({ kind: "column", table, column, as: column });
|
|
5415
|
-
}
|
|
5416
|
-
return select;
|
|
5417
|
-
}
|
|
5418
|
-
function projectPluck(rows, column, keyBy) {
|
|
5419
|
-
if (keyBy === undefined) {
|
|
5420
|
-
return rows.map((row) => row[column]);
|
|
5421
|
-
}
|
|
5422
|
-
const keyed = new Map;
|
|
5423
|
-
for (const row of rows) {
|
|
5424
|
-
keyed.set(row[keyBy], row[column]);
|
|
5425
|
-
}
|
|
5426
|
-
return keyed;
|
|
5427
|
-
}
|
|
5428
|
-
|
|
5429
|
-
// ../../src/core/database/whereBuilder.ts
|
|
5430
|
-
class WhereBuilder {
|
|
5431
|
-
nodes = [];
|
|
5432
|
-
where(where) {
|
|
5433
|
-
this.nodes.push({ kind: "and", where });
|
|
5434
|
-
return this;
|
|
5435
|
-
}
|
|
5436
|
-
orWhere(where) {
|
|
5437
|
-
this.nodes.push({ kind: "or", where });
|
|
5438
|
-
return this;
|
|
5439
|
-
}
|
|
5440
|
-
whereGroup(fn) {
|
|
5441
|
-
const nested = new WhereBuilder;
|
|
5442
|
-
fn(nested);
|
|
5443
|
-
if (nested.nodes.length > 0) {
|
|
5444
|
-
this.nodes.push({ kind: "and", group: nested.nodes });
|
|
5445
|
-
}
|
|
5446
|
-
return this;
|
|
5447
|
-
}
|
|
5448
|
-
orWhereGroup(fn) {
|
|
5449
|
-
const nested = new WhereBuilder;
|
|
5450
|
-
fn(nested);
|
|
5451
|
-
if (nested.nodes.length > 0) {
|
|
5452
|
-
this.nodes.push({ kind: "or", group: nested.nodes });
|
|
5453
|
-
}
|
|
5454
|
-
return this;
|
|
5455
|
-
}
|
|
5456
|
-
}
|
|
5457
|
-
|
|
5458
|
-
// ../../src/core/database/repositoryQuery.ts
|
|
5459
|
-
class RepositoryQuery {
|
|
5460
|
-
repository;
|
|
5461
|
-
whereClause;
|
|
5462
|
-
queryOptions;
|
|
5463
|
-
eagerLoads = [];
|
|
5464
|
-
whereNodes = [];
|
|
5465
|
-
protectedWhere = [];
|
|
5466
|
-
constructor(repository, whereClause = {}, queryOptions = {}) {
|
|
5467
|
-
this.repository = repository;
|
|
5468
|
-
this.whereClause = whereClause;
|
|
5469
|
-
this.queryOptions = queryOptions;
|
|
5470
|
-
}
|
|
5471
|
-
where(input) {
|
|
5472
|
-
if (typeof input === "function") {
|
|
5473
|
-
const builder = new WhereBuilder;
|
|
5474
|
-
input(builder);
|
|
5475
|
-
if (builder.nodes.length > 0)
|
|
5476
|
-
this.whereNodes.push({ kind: "and", group: builder.nodes });
|
|
5477
|
-
return this;
|
|
5478
|
-
}
|
|
5479
|
-
this.whereNodes.push({ kind: "and", where: { ...input } });
|
|
5480
|
-
return this;
|
|
5481
|
-
}
|
|
5482
|
-
protectWhere() {
|
|
5483
|
-
const nodes = [];
|
|
5484
|
-
if (Object.keys(this.whereClause).length > 0) {
|
|
5485
|
-
nodes.push({ kind: "and", where: this.whereClause });
|
|
5486
|
-
}
|
|
5487
|
-
nodes.push(...this.whereNodes);
|
|
5488
|
-
if (nodes.length > 0)
|
|
5489
|
-
this.protectedWhere.push({ kind: "and", group: nodes });
|
|
5490
|
-
this.whereClause = {};
|
|
5491
|
-
this.whereNodes.length = 0;
|
|
5492
|
-
return this;
|
|
4685
|
+
nodes.push(...this.whereNodes);
|
|
4686
|
+
if (nodes.length > 0)
|
|
4687
|
+
this.protectedWhere.push({ kind: "and", group: nodes });
|
|
4688
|
+
this.whereClause = {};
|
|
4689
|
+
this.whereNodes.length = 0;
|
|
4690
|
+
return this;
|
|
5493
4691
|
}
|
|
5494
4692
|
orWhere(input) {
|
|
5495
4693
|
if (typeof input === "function") {
|
|
@@ -6114,356 +5312,1159 @@ class BaseRepository {
|
|
|
6114
5312
|
}
|
|
6115
5313
|
};
|
|
6116
5314
|
}
|
|
6117
|
-
async findById(id) {
|
|
6118
|
-
return await this.firstOrNull({
|
|
6119
|
-
[this.table.primaryKey]: id
|
|
5315
|
+
async findById(id) {
|
|
5316
|
+
return await this.firstOrNull({
|
|
5317
|
+
[this.table.primaryKey]: id
|
|
5318
|
+
});
|
|
5319
|
+
}
|
|
5320
|
+
async findByIdOrThrow(id, errorFactory) {
|
|
5321
|
+
const record = await this.findById(id);
|
|
5322
|
+
if (record) {
|
|
5323
|
+
return record;
|
|
5324
|
+
}
|
|
5325
|
+
throw errorFactory?.(id) ?? new Error(`${this.table.name} ${String(id)} was not found.`);
|
|
5326
|
+
}
|
|
5327
|
+
async findByIds(ids) {
|
|
5328
|
+
const uniqueIds = [...new Set(ids)];
|
|
5329
|
+
if (uniqueIds.length === 0) {
|
|
5330
|
+
return [];
|
|
5331
|
+
}
|
|
5332
|
+
return await this.findWhere({
|
|
5333
|
+
[this.table.primaryKey]: uniqueIds
|
|
5334
|
+
});
|
|
5335
|
+
}
|
|
5336
|
+
async firstOrNull(where, options = {}) {
|
|
5337
|
+
const [record] = await this.findAll({ ...options, where, limit: 1 });
|
|
5338
|
+
return record ?? null;
|
|
5339
|
+
}
|
|
5340
|
+
async upsert(values, conflictColumns, updateColumns) {
|
|
5341
|
+
return await withDatabaseErrorHandling(async () => {
|
|
5342
|
+
const { text, params } = buildUpsertQuery(this.table, values, conflictColumns, updateColumns);
|
|
5343
|
+
const [record] = await this.connection.unsafe(text, params);
|
|
5344
|
+
return record ?? null;
|
|
5345
|
+
});
|
|
5346
|
+
}
|
|
5347
|
+
async incrementById(id, column, amount = 1, extra = {}) {
|
|
5348
|
+
return await withDatabaseErrorHandling(async () => {
|
|
5349
|
+
const { text, params } = buildIncrementQuery(this.table, id, column, amount, extra);
|
|
5350
|
+
const [record] = await this.connection.unsafe(text, params);
|
|
5351
|
+
return record ?? null;
|
|
5352
|
+
});
|
|
5353
|
+
}
|
|
5354
|
+
async decrementById(id, column, amount = 1, extra = {}) {
|
|
5355
|
+
return await this.incrementById(id, column, -amount, extra);
|
|
5356
|
+
}
|
|
5357
|
+
async create(values) {
|
|
5358
|
+
this.assertTransactionalEvent("created");
|
|
5359
|
+
return await this.runModelMutation("created", () => withDatabaseErrorHandling(async () => {
|
|
5360
|
+
const { text, params } = buildInsertQuery(this.table, values);
|
|
5361
|
+
const [record] = await this.connection.unsafe(text, params);
|
|
5362
|
+
if (!record) {
|
|
5363
|
+
throw new Error(`Insert into ${this.table.name} did not return a record.`);
|
|
5364
|
+
}
|
|
5365
|
+
const entity = record;
|
|
5366
|
+
await dispatchModelEvent(modelEventName(this.table.name, "created"), entity);
|
|
5367
|
+
return entity;
|
|
5368
|
+
}));
|
|
5369
|
+
}
|
|
5370
|
+
async updateWhere(changes, options) {
|
|
5371
|
+
return this.executeConditionalWrite("update", changes, options);
|
|
5372
|
+
}
|
|
5373
|
+
async deleteWhere(options) {
|
|
5374
|
+
return this.executeConditionalWrite("delete", {}, options);
|
|
5375
|
+
}
|
|
5376
|
+
async updateWhereReturning(changes, columns, options) {
|
|
5377
|
+
const { text, params } = buildConditionalWriteQuery(this.table, "update", changes, options, columns);
|
|
5378
|
+
return withDatabaseErrorHandling(() => this.connection.unsafe(text, params));
|
|
5379
|
+
}
|
|
5380
|
+
async executeConditionalWrite(operation, changes, options) {
|
|
5381
|
+
const { text, params } = buildConditionalWriteQuery(this.table, operation, changes, options);
|
|
5382
|
+
return withDatabaseErrorHandling(async () => {
|
|
5383
|
+
const rows = await this.connection.unsafe(text, params);
|
|
5384
|
+
if (currentSqlDialect().driver !== "mysql")
|
|
5385
|
+
return rows.length;
|
|
5386
|
+
const affected = rows[0]?.affectedRows;
|
|
5387
|
+
if (!Number.isSafeInteger(affected) || affected === undefined || affected < 0)
|
|
5388
|
+
throw new Error("MySQL conditional writes require affectedRows metadata from the framework adapter.");
|
|
5389
|
+
return affected;
|
|
5390
|
+
});
|
|
5391
|
+
}
|
|
5392
|
+
async updateById(id, changes) {
|
|
5393
|
+
this.assertTransactionalEvent("updated");
|
|
5394
|
+
return await this.runModelMutation("updated", () => withDatabaseErrorHandling(async () => {
|
|
5395
|
+
const { text, params } = buildUpdateQuery(this.table, id, changes);
|
|
5396
|
+
const [record] = await this.connection.unsafe(text, params);
|
|
5397
|
+
const entity = record ?? null;
|
|
5398
|
+
if (entity) {
|
|
5399
|
+
await dispatchModelEvent(modelEventName(this.table.name, "updated"), entity);
|
|
5400
|
+
}
|
|
5401
|
+
return entity;
|
|
5402
|
+
}));
|
|
5403
|
+
}
|
|
5404
|
+
async updateByIdOrThrow(id, changes, errorFactory) {
|
|
5405
|
+
const record = await this.updateById(id, changes);
|
|
5406
|
+
if (record) {
|
|
5407
|
+
return record;
|
|
5408
|
+
}
|
|
5409
|
+
throw errorFactory?.(id) ?? new Error(`${this.table.name} ${String(id)} was not found.`);
|
|
5410
|
+
}
|
|
5411
|
+
async deleteById(id) {
|
|
5412
|
+
if (resolveSoftDeleteColumn(this.table)) {
|
|
5413
|
+
return await this.softDeleteById(id);
|
|
5414
|
+
}
|
|
5415
|
+
return await this.forceDeleteById(id);
|
|
5416
|
+
}
|
|
5417
|
+
async softDeleteById(id) {
|
|
5418
|
+
this.assertTransactionalEvent("deleted");
|
|
5419
|
+
return await this.runModelMutation("deleted", () => withDatabaseErrorHandling(async () => {
|
|
5420
|
+
const { text, params } = buildSoftDeleteByIdQuery(this.table, id, new Date);
|
|
5421
|
+
const [record] = await this.connection.unsafe(text, params);
|
|
5422
|
+
if (!record) {
|
|
5423
|
+
return false;
|
|
5424
|
+
}
|
|
5425
|
+
await dispatchModelEvent(modelEventName(this.table.name, "deleted"), record);
|
|
5426
|
+
return true;
|
|
5427
|
+
}));
|
|
5428
|
+
}
|
|
5429
|
+
async forceDeleteById(id) {
|
|
5430
|
+
this.assertTransactionalEvent("force-deleted");
|
|
5431
|
+
return await this.runModelMutation("force-deleted", () => withDatabaseErrorHandling(async () => {
|
|
5432
|
+
const { text, params } = buildDeleteByIdQuery(this.table, id);
|
|
5433
|
+
const [row] = await this.connection.unsafe(text, params);
|
|
5434
|
+
if (!row) {
|
|
5435
|
+
return false;
|
|
5436
|
+
}
|
|
5437
|
+
await dispatchModelEvent(modelEventName(this.table.name, "force-deleted"), {
|
|
5438
|
+
id
|
|
5439
|
+
});
|
|
5440
|
+
return true;
|
|
5441
|
+
}));
|
|
5442
|
+
}
|
|
5443
|
+
async restoreById(id) {
|
|
5444
|
+
this.assertTransactionalEvent("restored");
|
|
5445
|
+
return await this.runModelMutation("restored", () => withDatabaseErrorHandling(async () => {
|
|
5446
|
+
const { text, params } = buildRestoreByIdQuery(this.table, id);
|
|
5447
|
+
const [record] = await this.connection.unsafe(text, params);
|
|
5448
|
+
if (!record) {
|
|
5449
|
+
return null;
|
|
5450
|
+
}
|
|
5451
|
+
const entity = record;
|
|
5452
|
+
await dispatchModelEvent(modelEventName(this.table.name, "restored"), entity);
|
|
5453
|
+
return entity;
|
|
5454
|
+
}));
|
|
5455
|
+
}
|
|
5456
|
+
primaryKeySeekDirection(options) {
|
|
5457
|
+
if (options.joins?.length || options.groupBy || options.having || options.select?.length || options.offset !== undefined || options.limit !== undefined || options.lock) {
|
|
5458
|
+
return null;
|
|
5459
|
+
}
|
|
5460
|
+
const orders = listOrders(options.orderBy ?? this.table.defaultOrderBy);
|
|
5461
|
+
const order = orders[0];
|
|
5462
|
+
if (orders.length !== 1 || !order || order.nulls || order.column !== this.table.primaryKey) {
|
|
5463
|
+
return null;
|
|
5464
|
+
}
|
|
5465
|
+
return order.direction;
|
|
5466
|
+
}
|
|
5467
|
+
withConnection(connection) {
|
|
5468
|
+
const clone = Object.create(Object.getPrototypeOf(this));
|
|
5469
|
+
Object.assign(clone, this);
|
|
5470
|
+
clone.connection = connection;
|
|
5471
|
+
return clone;
|
|
5472
|
+
}
|
|
5473
|
+
getConnection() {
|
|
5474
|
+
return this.connection;
|
|
5475
|
+
}
|
|
5476
|
+
getTable() {
|
|
5477
|
+
return this.table;
|
|
5478
|
+
}
|
|
5479
|
+
query(where = {}) {
|
|
5480
|
+
return new RepositoryQuery(this, where);
|
|
5481
|
+
}
|
|
5482
|
+
async findWhere(where, options = {}) {
|
|
5483
|
+
return await this.findAll({ ...options, where });
|
|
5484
|
+
}
|
|
5485
|
+
async countWhere(where = {}, options = {}, whereNodes = []) {
|
|
5486
|
+
const { text, params } = buildCountQuery(this.table, where, options, whereNodes);
|
|
5487
|
+
const [row] = await this.connection.unsafe(text, params);
|
|
5488
|
+
return Number(row?.count ?? 0);
|
|
5489
|
+
}
|
|
5490
|
+
async averageColumn(column, where = {}) {
|
|
5491
|
+
const qualifiedColumn = qualifyColumn(this.table.name, column);
|
|
5492
|
+
return await this.averageExpression(`AVG(${qualifiedColumn})`, "value", where);
|
|
5493
|
+
}
|
|
5494
|
+
async averageExpression(expression, alias, where = {}) {
|
|
5495
|
+
const { text, params } = buildProjectionQuery(this.table, expression, alias, { where });
|
|
5496
|
+
const [row] = await this.connection.unsafe(text, params);
|
|
5497
|
+
return Number(row?.[alias] ?? 0);
|
|
5498
|
+
}
|
|
5499
|
+
async sumBy(groupColumn, column, options = {}) {
|
|
5500
|
+
const { whereNodes, ...queryOptions } = options;
|
|
5501
|
+
const { text, params } = buildGroupedSumQuery(this.table, groupColumn, column, queryOptions, whereNodes ?? []);
|
|
5502
|
+
return withDatabaseErrorHandling(() => this.connection.unsafe(text, params));
|
|
5503
|
+
}
|
|
5504
|
+
async sum(column, where = {}) {
|
|
5505
|
+
return await this.aggregateColumn("SUM", column, where);
|
|
5506
|
+
}
|
|
5507
|
+
async avg(column, where = {}) {
|
|
5508
|
+
return await this.aggregateColumn("AVG", column, where);
|
|
5509
|
+
}
|
|
5510
|
+
async min(column, where = {}) {
|
|
5511
|
+
return await this.aggregateColumn("MIN", column, where);
|
|
5512
|
+
}
|
|
5513
|
+
async max(column, where = {}) {
|
|
5514
|
+
return await this.aggregateColumn("MAX", column, where);
|
|
5515
|
+
}
|
|
5516
|
+
async aggregateColumn(fn, column, where) {
|
|
5517
|
+
const qualifiedColumn = qualifyColumn(this.table.name, column);
|
|
5518
|
+
return await this.averageExpression(`${fn}(${qualifiedColumn})`, "value", where);
|
|
5519
|
+
}
|
|
5520
|
+
async pluckNumberValues(expression, alias, options = {}) {
|
|
5521
|
+
const { text, params } = buildProjectionQuery(this.table, expression, alias, options);
|
|
5522
|
+
const rows = await this.connection.unsafe(text, params);
|
|
5523
|
+
return rows.flatMap((row) => {
|
|
5524
|
+
const value = row[alias];
|
|
5525
|
+
return value === null || value === undefined ? [] : [Number(value)];
|
|
5526
|
+
});
|
|
5527
|
+
}
|
|
5528
|
+
async countGroupedBy(column, where = {}) {
|
|
5529
|
+
const { text, params } = buildGroupedCountQuery(this.table, column, where);
|
|
5530
|
+
const rows = await this.connection.unsafe(text, params);
|
|
5531
|
+
return rows.map(({ value, count }) => ({
|
|
5532
|
+
value,
|
|
5533
|
+
count: Number(count)
|
|
5534
|
+
}));
|
|
5535
|
+
}
|
|
5536
|
+
async findByHasManyRelation(relation, parentId, options = {}) {
|
|
5537
|
+
return await this.findWhere({
|
|
5538
|
+
[relation.foreignKey]: parentId
|
|
5539
|
+
}, options);
|
|
5540
|
+
}
|
|
5541
|
+
async loadHasManyForParents(parents, relation, options = {}) {
|
|
5542
|
+
if (parents.length === 0) {
|
|
5543
|
+
return indexHasManyRelation(parents, [], relation);
|
|
5544
|
+
}
|
|
5545
|
+
const parentIds = [...new Set(parents.map((parent) => parent[relation.localKey]))];
|
|
5546
|
+
const children = await this.findWhere({
|
|
5547
|
+
[relation.foreignKey]: parentIds
|
|
5548
|
+
}, options);
|
|
5549
|
+
return indexHasManyRelation(parents, children, relation);
|
|
5550
|
+
}
|
|
5551
|
+
async findHasManyThrough(parentId, relation, options = {}) {
|
|
5552
|
+
const grouped = await this.loadHasManyThroughForParents([{ [relation.localKey]: parentId }], relation, options);
|
|
5553
|
+
return getByRelationKey(grouped, parentId) ?? [];
|
|
5554
|
+
}
|
|
5555
|
+
async loadHasManyThroughForParents(parents, relation, options = {}) {
|
|
5556
|
+
if (parents.length === 0) {
|
|
5557
|
+
return indexHasManyThroughRelation(parents, [], relation);
|
|
5558
|
+
}
|
|
5559
|
+
if (options.joins?.some((join) => join.table === relation.throughTable)) {
|
|
5560
|
+
throw new Error("A related model scope joins the through table twice; use an explicitly aliased correlated predicate instead.");
|
|
5561
|
+
}
|
|
5562
|
+
const parentIds = [...new Set(parents.map((parent) => parent[relation.localKey]))];
|
|
5563
|
+
const throughParentKey = relation.throughParentKey ?? "__through_parent_id";
|
|
5564
|
+
const farTable = this.table.name;
|
|
5565
|
+
const columns = this.table.columns.map((column) => `${qualifyColumn(farTable, column)}`).join(", ");
|
|
5566
|
+
const placeholders = parentIds.map((_, index) => currentSqlDialect().placeholder(index + 1)).join(", ");
|
|
5567
|
+
const { text: extraWhere, params: extraParams } = this.buildThroughWhere(options, parentIds);
|
|
5568
|
+
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}`;
|
|
5569
|
+
const children = await this.connection.unsafe(sql, [
|
|
5570
|
+
...parentIds,
|
|
5571
|
+
...extraParams
|
|
5572
|
+
]);
|
|
5573
|
+
return indexHasManyThroughRelation(parents, children, relation);
|
|
5574
|
+
}
|
|
5575
|
+
buildThroughWhere(options, parentIds) {
|
|
5576
|
+
const parameters = [...parentIds];
|
|
5577
|
+
const { clause } = buildQueryWhereClause(this.table, options, options.whereNodes, parameters);
|
|
5578
|
+
return {
|
|
5579
|
+
text: clause ? ` AND (${clause.replace(/^ WHERE /, "")})` : "",
|
|
5580
|
+
params: parameters.slice(parentIds.length)
|
|
5581
|
+
};
|
|
5582
|
+
}
|
|
5583
|
+
async loadBelongsToForParents(children, relation, parentRepository, options = {}) {
|
|
5584
|
+
if (children.length === 0) {
|
|
5585
|
+
return new Map;
|
|
5586
|
+
}
|
|
5587
|
+
const ownerIds = [...new Set(children.map((child) => child[relation.foreignKey]))];
|
|
5588
|
+
const parents = await parentRepository.withConnection(this.connection).findWhere({
|
|
5589
|
+
[relation.ownerKey]: ownerIds
|
|
5590
|
+
}, options);
|
|
5591
|
+
return indexBelongsToRelation(children, parents, relation);
|
|
5592
|
+
}
|
|
5593
|
+
async loadMorphManyForParents(parents, relation, options = {}) {
|
|
5594
|
+
if (parents.length === 0) {
|
|
5595
|
+
return indexMorphManyRelation(parents, [], relation);
|
|
5596
|
+
}
|
|
5597
|
+
const parentIds = [...new Set(parents.map((parent) => parent[relation.localKey]))];
|
|
5598
|
+
const children = await this.findWhere({
|
|
5599
|
+
[relation.morphTypeKey]: relation.morphType,
|
|
5600
|
+
[relation.morphIdKey]: parentIds
|
|
5601
|
+
}, options);
|
|
5602
|
+
return indexMorphManyRelation(parents, children, relation);
|
|
5603
|
+
}
|
|
5604
|
+
async loadMorphOneForParents(parents, relation, options = {}) {
|
|
5605
|
+
const grouped = await this.loadMorphManyForParents(parents, relation, options);
|
|
5606
|
+
const result = new Map;
|
|
5607
|
+
for (const parent of parents) {
|
|
5608
|
+
const matches = getByRelationKey(grouped, parent[relation.localKey]) ?? [];
|
|
5609
|
+
result.set(parent[relation.localKey], matches[0]);
|
|
5610
|
+
}
|
|
5611
|
+
return result;
|
|
5612
|
+
}
|
|
5613
|
+
async loadMorphToForChildren(children, relation, repositoriesByType, options = {}) {
|
|
5614
|
+
if (children.length === 0) {
|
|
5615
|
+
return new Map;
|
|
5616
|
+
}
|
|
5617
|
+
const idsByType = new Map;
|
|
5618
|
+
for (const child of children) {
|
|
5619
|
+
const morphType = String(child[relation.morphTypeKey]);
|
|
5620
|
+
const morphId = child[relation.morphIdKey];
|
|
5621
|
+
const ids = idsByType.get(morphType) ?? new Set;
|
|
5622
|
+
ids.add(morphId);
|
|
5623
|
+
idsByType.set(morphType, ids);
|
|
5624
|
+
}
|
|
5625
|
+
const parentsByType = new Map;
|
|
5626
|
+
await Promise.all([...idsByType.entries()].map(async ([morphType, ids]) => {
|
|
5627
|
+
const repository = repositoriesByType.get(morphType);
|
|
5628
|
+
if (!repository) {
|
|
5629
|
+
return;
|
|
5630
|
+
}
|
|
5631
|
+
const ownerKey = repository.getTable().primaryKey;
|
|
5632
|
+
const parents = await repository.withConnection(this.connection).findWhere({
|
|
5633
|
+
[ownerKey]: [...ids]
|
|
5634
|
+
}, options);
|
|
5635
|
+
const indexed = new Map;
|
|
5636
|
+
for (const parent of parents) {
|
|
5637
|
+
indexed.set(relationMatchKey(parent[ownerKey]), parent);
|
|
5638
|
+
}
|
|
5639
|
+
parentsByType.set(morphType, indexed);
|
|
5640
|
+
}));
|
|
5641
|
+
return indexMorphToRelation(children, parentsByType, relation);
|
|
5642
|
+
}
|
|
5643
|
+
async loadBelongsToManyForParents(parents, relation, relatedRepository, options = {}) {
|
|
5644
|
+
if (parents.length === 0) {
|
|
5645
|
+
return indexBelongsToManyRelation(parents, [], [], relation);
|
|
5646
|
+
}
|
|
5647
|
+
const parentIds = [...new Set(parents.map((parent) => parent[relation.parentKey]))];
|
|
5648
|
+
const placeholders = parentIds.map((_, index) => currentSqlDialect().placeholder(index + 1)).join(", ");
|
|
5649
|
+
const pivotRows = await this.connection.unsafe(`SELECT * FROM ${relation.pivotTable} WHERE ${String(relation.foreignPivotKey)} IN (${placeholders})`, parentIds);
|
|
5650
|
+
if (pivotRows.length === 0) {
|
|
5651
|
+
return indexBelongsToManyRelation(parents, [], [], relation);
|
|
5652
|
+
}
|
|
5653
|
+
const relatedIds = [
|
|
5654
|
+
...new Set(pivotRows.map((row) => row[relation.relatedPivotKey]))
|
|
5655
|
+
];
|
|
5656
|
+
const relatedRows = await relatedRepository.withConnection(this.connection).findWhere({
|
|
5657
|
+
[relation.relatedKey]: relatedIds
|
|
5658
|
+
}, options);
|
|
5659
|
+
return indexBelongsToManyRelation(parents, pivotRows, relatedRows, relation);
|
|
5660
|
+
}
|
|
5661
|
+
}
|
|
5662
|
+
var baseRepository_default = BaseRepository;
|
|
5663
|
+
|
|
5664
|
+
// ../../src/core/database/table.ts
|
|
5665
|
+
function defineTable(definition) {
|
|
5666
|
+
return definition;
|
|
5667
|
+
}
|
|
5668
|
+
|
|
5669
|
+
// ../../src/core/tenant/resolveTenant.ts
|
|
5670
|
+
init_tenancyConfig();
|
|
5671
|
+
var tenants = new BaseRepository(defineTable({
|
|
5672
|
+
name: "tenant",
|
|
5673
|
+
primaryKey: "id",
|
|
5674
|
+
columns: ["id", "slug"]
|
|
5675
|
+
}));
|
|
5676
|
+
async function resolveTenant(tenantId) {
|
|
5677
|
+
if (!isTenancyEnabled()) {
|
|
5678
|
+
return {
|
|
5679
|
+
id: tenantId,
|
|
5680
|
+
slug: "default"
|
|
5681
|
+
};
|
|
5682
|
+
}
|
|
5683
|
+
const row = await tenants.findById(tenantId);
|
|
5684
|
+
return row ? { id: row.id, slug: row.slug } : null;
|
|
5685
|
+
}
|
|
5686
|
+
|
|
5687
|
+
// ../../src/core/auth/scimAuthMiddleware.ts
|
|
5688
|
+
init_tenantDatabaseScope();
|
|
5689
|
+
function createScimAuthMiddleware() {
|
|
5690
|
+
return async (request, next) => {
|
|
5691
|
+
const authorization = request.headers.get("authorization");
|
|
5692
|
+
if (!authorization?.startsWith("Bearer ")) {
|
|
5693
|
+
return jsonScimError("SCIM bearer token required.", 401);
|
|
5694
|
+
}
|
|
5695
|
+
const token = authorization.slice("Bearer ".length).trim();
|
|
5696
|
+
const tenantId = resolveScimTenantFromToken(token);
|
|
5697
|
+
if (tenantId === null) {
|
|
5698
|
+
return jsonScimError("Invalid SCIM bearer token.", 401);
|
|
5699
|
+
}
|
|
5700
|
+
const tenant = await resolveTenant(tenantId);
|
|
5701
|
+
if (!tenant) {
|
|
5702
|
+
return jsonScimError("SCIM tenant not found.", 401);
|
|
5703
|
+
}
|
|
5704
|
+
return await runWithTenantDatabase(tenant, async () => {
|
|
5705
|
+
bindDatabaseConnection(getActiveDatabaseConnection(getDefaultDatabasePool()));
|
|
5706
|
+
try {
|
|
5707
|
+
return await next();
|
|
5708
|
+
} finally {
|
|
5709
|
+
resetBoundDatabaseConnection();
|
|
5710
|
+
}
|
|
5711
|
+
});
|
|
5712
|
+
};
|
|
5713
|
+
}
|
|
5714
|
+
function jsonScimError(detail, status) {
|
|
5715
|
+
return Response.json({
|
|
5716
|
+
schemas: ["urn:ietf:params:scim:api:messages:2.0:Error"],
|
|
5717
|
+
detail,
|
|
5718
|
+
status: String(status)
|
|
5719
|
+
}, {
|
|
5720
|
+
status,
|
|
5721
|
+
headers: { "content-type": "application/scim+json" }
|
|
5722
|
+
});
|
|
5723
|
+
}
|
|
5724
|
+
// ../../src/core/auth/tokenAbilityChecker.ts
|
|
5725
|
+
function tokenCan(user, ability) {
|
|
5726
|
+
if (!user) {
|
|
5727
|
+
return false;
|
|
5728
|
+
}
|
|
5729
|
+
const abilities = user.abilities ?? [];
|
|
5730
|
+
return abilities.includes("*") || abilities.includes(ability);
|
|
5731
|
+
}
|
|
5732
|
+
function createTokenAbilityChecker() {
|
|
5733
|
+
return {
|
|
5734
|
+
tokenCan,
|
|
5735
|
+
requireAbility(user, ability) {
|
|
5736
|
+
if (!tokenCan(user, ability)) {
|
|
5737
|
+
throw new ForbiddenError(`Missing ability: ${ability}`);
|
|
5738
|
+
}
|
|
5739
|
+
}
|
|
5740
|
+
};
|
|
5741
|
+
}
|
|
5742
|
+
// ../../src/core/cache/redisCacheStore.ts
|
|
5743
|
+
var {RedisClient } = globalThis.Bun;
|
|
5744
|
+
var CACHE_SCRIPT = `
|
|
5745
|
+
local expected = {'hash', 'zset', 'zset', 'hash', 'zset'}
|
|
5746
|
+
for i, key in ipairs(KEYS) do
|
|
5747
|
+
local kind = redis.call('TYPE', key).ok
|
|
5748
|
+
if kind ~= 'none' and kind ~= expected[i] then return redis.error_reply('Unexpected cache key type') end
|
|
5749
|
+
end
|
|
5750
|
+
-- Redis eviction can remove one container without its companions. Never read
|
|
5751
|
+
-- orphaned values or leases after losing the expiry/order/generation indexes.
|
|
5752
|
+
local entries = redis.call('HLEN', KEYS[1])
|
|
5753
|
+
local order = redis.call('ZCARD', KEYS[3])
|
|
5754
|
+
if redis.call('ZCARD', KEYS[2]) ~= order or entries ~= (order > 0 and order + 1 or 0) then
|
|
5755
|
+
redis.call('DEL', KEYS[1], KEYS[2], KEYS[3])
|
|
5756
|
+
end
|
|
5757
|
+
if redis.call('HLEN', KEYS[4]) ~= redis.call('ZCARD', KEYS[5]) then
|
|
5758
|
+
redis.call('DEL', KEYS[4], KEYS[5])
|
|
5759
|
+
end
|
|
5760
|
+
local r = cjson.decode(ARGV[1])
|
|
5761
|
+
local time = redis.call('TIME')
|
|
5762
|
+
local now = tonumber(time[1]) * 1000 + tonumber(time[2]) / 1000
|
|
5763
|
+
local function removeEntry(key)
|
|
5764
|
+
local removed = redis.call('HDEL', KEYS[1], 'e:' .. key)
|
|
5765
|
+
redis.call('ZREM', KEYS[2], key)
|
|
5766
|
+
redis.call('ZREM', KEYS[3], key)
|
|
5767
|
+
return removed
|
|
5768
|
+
end
|
|
5769
|
+
local function removeMeta(field)
|
|
5770
|
+
redis.call('HDEL', KEYS[4], field)
|
|
5771
|
+
redis.call('ZREM', KEYS[5], field)
|
|
5772
|
+
end
|
|
5773
|
+
for _, key in ipairs(redis.call('ZRANGEBYSCORE', KEYS[2], '-inf', now, 'LIMIT', 0, r.capacity)) do removeEntry(key) end
|
|
5774
|
+
for _, field in ipairs(redis.call('ZRANGEBYSCORE', KEYS[5], '-inf', now, 'LIMIT', 0, r.metadataLimit)) do removeMeta(field) end
|
|
5775
|
+
local function expireContainers()
|
|
5776
|
+
local last = redis.call('ZREVRANGE', KEYS[2], 0, 0, 'WITHSCORES')
|
|
5777
|
+
if #last == 0 then
|
|
5778
|
+
redis.call('DEL', KEYS[1], KEYS[2], KEYS[3])
|
|
5779
|
+
elseif last[2] == 'inf' then
|
|
5780
|
+
for i = 1, 3 do redis.call('PERSIST', KEYS[i]) end
|
|
5781
|
+
else
|
|
5782
|
+
local ttl = math.max(1, math.ceil(tonumber(last[2]) - now))
|
|
5783
|
+
for i = 1, 3 do redis.call('PEXPIRE', KEYS[i], ttl) end
|
|
5784
|
+
end
|
|
5785
|
+
for i = 4, 5 do redis.call('PEXPIRE', KEYS[i], r.metadataTtlMs) end
|
|
5786
|
+
end
|
|
5787
|
+
local function response(value)
|
|
5788
|
+
expireContainers()
|
|
5789
|
+
return cjson.encode(value)
|
|
5790
|
+
end
|
|
5791
|
+
local function addTags(existing, added)
|
|
5792
|
+
local seen = {}
|
|
5793
|
+
local result = {}
|
|
5794
|
+
for _, tags in ipairs({existing, added}) do
|
|
5795
|
+
for _, tag in ipairs(tags) do
|
|
5796
|
+
if not seen[tag] then seen[tag] = true; table.insert(result, tag) end
|
|
5797
|
+
end
|
|
5798
|
+
end
|
|
5799
|
+
if #result > 32 then error('Cache entries support at most 32 tags') end
|
|
5800
|
+
return result
|
|
5801
|
+
end
|
|
5802
|
+
local function touch(key)
|
|
5803
|
+
local sequence = redis.call('HINCRBY', KEYS[1], '_sequence', 1)
|
|
5804
|
+
redis.call('ZADD', KEYS[3], sequence, key)
|
|
5805
|
+
end
|
|
5806
|
+
local function read(key, tags)
|
|
5807
|
+
local raw = redis.call('HGET', KEYS[1], 'e:' .. key)
|
|
5808
|
+
if not raw then return nil end
|
|
5809
|
+
local entry = cjson.decode(raw)
|
|
5810
|
+
if #tags > 0 then
|
|
5811
|
+
entry.tags = addTags(entry.tags, tags)
|
|
5812
|
+
redis.call('HSET', KEYS[1], 'e:' .. key, cjson.encode(entry))
|
|
5813
|
+
end
|
|
5814
|
+
touch(key)
|
|
5815
|
+
return entry.payload
|
|
5816
|
+
end
|
|
5817
|
+
local function write(key, payload, tags)
|
|
5818
|
+
local entry = {payload=payload, tags=tags}
|
|
5819
|
+
redis.call('HSET', KEYS[1], 'e:' .. key, cjson.encode(entry))
|
|
5820
|
+
redis.call('ZADD', KEYS[2], r.ttlMs == 0 and '+inf' or now + r.ttlMs, key)
|
|
5821
|
+
touch(key)
|
|
5822
|
+
local overflow = redis.call('ZCARD', KEYS[3]) - r.capacity
|
|
5823
|
+
if overflow > 0 then
|
|
5824
|
+
for _, victim in ipairs(redis.call('ZRANGE', KEYS[3], 0, overflow - 1)) do removeEntry(victim) end
|
|
5825
|
+
end
|
|
5826
|
+
end
|
|
5827
|
+
local leaseField = 'l:' .. r.key
|
|
5828
|
+
local function lease()
|
|
5829
|
+
local raw = redis.call('HGET', KEYS[4], leaseField)
|
|
5830
|
+
if not raw then return nil end
|
|
5831
|
+
local value = cjson.decode(raw)
|
|
5832
|
+
if value.expires <= now or value.deadline <= now then removeMeta(leaseField); return nil end
|
|
5833
|
+
return value
|
|
5834
|
+
end
|
|
5835
|
+
local function metadataSpace(count)
|
|
5836
|
+
if redis.call('HLEN', KEYS[4]) + count > r.metadataLimit then
|
|
5837
|
+
-- Pressure sheds generation/lease state. Every old owner is fenced; cached
|
|
5838
|
+
-- entries remain valid because invalidation removes matching entries atomically.
|
|
5839
|
+
redis.call('DEL', KEYS[4], KEYS[5])
|
|
5840
|
+
end
|
|
5841
|
+
end
|
|
5842
|
+
local function putMeta(field, value, expires)
|
|
5843
|
+
redis.call('HSET', KEYS[4], field, value)
|
|
5844
|
+
redis.call('ZADD', KEYS[5], expires, field)
|
|
5845
|
+
end
|
|
5846
|
+
local function scopes()
|
|
5847
|
+
local fields = {}
|
|
5848
|
+
for _, tag in ipairs(r.tags) do table.insert(fields, 'g:tag:' .. tag) end
|
|
5849
|
+
for _, prefix in ipairs(r.prefixes) do table.insert(fields, 'g:prefix:' .. prefix) end
|
|
5850
|
+
return fields
|
|
5851
|
+
end
|
|
5852
|
+
local function matches(key, tags)
|
|
5853
|
+
if r.op == 'invalidate' then return key == r.key end
|
|
5854
|
+
if r.op == 'prefix' then return key == r.key or string.sub(key, 1, #r.key + 1) == r.key .. '?' end
|
|
5855
|
+
if r.op == 'tags' then
|
|
5856
|
+
for _, existing in ipairs(tags) do
|
|
5857
|
+
for _, wanted in ipairs(r.tags) do if wanted == existing then return true end end
|
|
5858
|
+
end
|
|
5859
|
+
end
|
|
5860
|
+
return false
|
|
5861
|
+
end
|
|
5862
|
+
if r.op == 'clear' then
|
|
5863
|
+
redis.call('DEL', unpack(KEYS))
|
|
5864
|
+
return cjson.encode({removed=0})
|
|
5865
|
+
elseif r.op == 'get' then
|
|
5866
|
+
local payload = read(r.key, r.tags)
|
|
5867
|
+
return response(payload and {kind='value', payload=payload} or {kind='missing'})
|
|
5868
|
+
elseif r.op == 'set' then
|
|
5869
|
+
local old = redis.call('HGET', KEYS[1], 'e:' .. r.key)
|
|
5870
|
+
local tags = old and cjson.decode(old).tags or {}
|
|
5871
|
+
write(r.key, r.payload, tags)
|
|
5872
|
+
removeMeta(leaseField)
|
|
5873
|
+
return response({written=true})
|
|
5874
|
+
elseif r.op == 'attach' then
|
|
5875
|
+
read(r.key, r.tags)
|
|
5876
|
+
return response({written=true})
|
|
5877
|
+
elseif r.op == 'size' then
|
|
5878
|
+
return response({size=redis.call('ZCARD', KEYS[3])})
|
|
5879
|
+
elseif r.op == 'tags' or r.op == 'prefix' or r.op == 'invalidate' then
|
|
5880
|
+
local generationFields = {}
|
|
5881
|
+
if r.op == 'tags' then
|
|
5882
|
+
for _, tag in ipairs(r.tags) do table.insert(generationFields, 'g:tag:' .. tag) end
|
|
5883
|
+
elseif r.op == 'prefix' then table.insert(generationFields, 'g:prefix:' .. r.key) end
|
|
5884
|
+
metadataSpace(#generationFields)
|
|
5885
|
+
for _, field in ipairs(generationFields) do putMeta(field, r.owner, now + r.metadataTtlMs) end
|
|
5886
|
+
local removed = 0
|
|
5887
|
+
for _, key in ipairs(redis.call('ZRANGE', KEYS[3], 0, -1)) do
|
|
5888
|
+
local raw = redis.call('HGET', KEYS[1], 'e:' .. key)
|
|
5889
|
+
if raw and matches(key, cjson.decode(raw).tags) then removed = removed + removeEntry(key) end
|
|
5890
|
+
end
|
|
5891
|
+
for _, field in ipairs(redis.call('HKEYS', KEYS[4])) do
|
|
5892
|
+
if string.sub(field, 1, 2) == 'l:' then
|
|
5893
|
+
local value = cjson.decode(redis.call('HGET', KEYS[4], field))
|
|
5894
|
+
if matches(string.sub(field, 3), value.tags) then removeMeta(field) end
|
|
5895
|
+
end
|
|
5896
|
+
end
|
|
5897
|
+
return response({removed=removed})
|
|
5898
|
+
elseif r.op == 'claim' then
|
|
5899
|
+
local payload = read(r.key, r.tags)
|
|
5900
|
+
if payload then return response({kind='value', payload=payload}) end
|
|
5901
|
+
local current = lease()
|
|
5902
|
+
if current then
|
|
5903
|
+
local shared = addTags(current.tags, r.tags)
|
|
5904
|
+
if #shared == #current.tags then return response({kind='wait'}) end
|
|
5905
|
+
-- A newly requested tag must not inherit a fill started before that tag's
|
|
5906
|
+
-- invalidation. Restart with the union of tags and a new owner.
|
|
5907
|
+
r.tags = shared
|
|
5908
|
+
removeMeta(leaseField)
|
|
5909
|
+
end
|
|
5910
|
+
local active = 0
|
|
5911
|
+
for _, field in ipairs(redis.call('HKEYS', KEYS[4])) do if string.sub(field, 1, 2) == 'l:' then active = active + 1 end end
|
|
5912
|
+
if active >= r.capacity then return response({kind='wait'}) end
|
|
5913
|
+
local fields = scopes()
|
|
5914
|
+
metadataSpace(#fields + 1)
|
|
5915
|
+
local generations = {}
|
|
5916
|
+
for _, field in ipairs(fields) do
|
|
5917
|
+
local token = redis.call('HGET', KEYS[4], field) or r.owner
|
|
5918
|
+
putMeta(field, token, now + r.metadataTtlMs)
|
|
5919
|
+
generations[field] = token
|
|
5920
|
+
end
|
|
5921
|
+
local value = {owner=r.owner, tags=r.tags, generations=generations, deadline=now + r.fillTimeoutMs, expires=now + r.leaseMs}
|
|
5922
|
+
putMeta(leaseField, cjson.encode(value), value.expires)
|
|
5923
|
+
return response({kind='owner'})
|
|
5924
|
+
elseif r.op == 'renew' or r.op == 'publish' or r.op == 'release' then
|
|
5925
|
+
local current = lease()
|
|
5926
|
+
if not current or current.owner ~= r.owner then return response({owned=false}) end
|
|
5927
|
+
if r.op == 'release' then removeMeta(leaseField); return response({owned=true}) end
|
|
5928
|
+
for field, token in pairs(current.generations) do
|
|
5929
|
+
if redis.call('HGET', KEYS[4], field) ~= token then removeMeta(leaseField); return response({owned=false}) end
|
|
5930
|
+
end
|
|
5931
|
+
if r.op == 'renew' then
|
|
5932
|
+
current.expires = math.min(current.deadline, now + r.leaseMs)
|
|
5933
|
+
putMeta(leaseField, cjson.encode(current), current.expires)
|
|
5934
|
+
else
|
|
5935
|
+
write(r.key, r.payload, current.tags)
|
|
5936
|
+
removeMeta(leaseField)
|
|
5937
|
+
end
|
|
5938
|
+
return response({owned=true})
|
|
5939
|
+
end
|
|
5940
|
+
return redis.error_reply('Unknown cache operation')
|
|
5941
|
+
`;
|
|
5942
|
+
|
|
5943
|
+
class RedisCacheStore {
|
|
5944
|
+
redisUrl;
|
|
5945
|
+
ttlMs;
|
|
5946
|
+
maxEntries;
|
|
5947
|
+
client;
|
|
5948
|
+
keys;
|
|
5949
|
+
options;
|
|
5950
|
+
fills = new Set;
|
|
5951
|
+
closed = false;
|
|
5952
|
+
constructor(redisUrl, ttlMs, maxEntries, options = {}) {
|
|
5953
|
+
this.redisUrl = redisUrl;
|
|
5954
|
+
this.ttlMs = ttlMs;
|
|
5955
|
+
this.maxEntries = maxEntries;
|
|
5956
|
+
if (!Number.isSafeInteger(ttlMs) || ttlMs < 0)
|
|
5957
|
+
throw new RangeError("ttlMs must be a non-negative safe integer.");
|
|
5958
|
+
if (!Number.isInteger(maxEntries) || maxEntries < 1 || maxEntries > 1e4)
|
|
5959
|
+
throw new RangeError("maxEntries must be between 1 and 10000.");
|
|
5960
|
+
this.options = {
|
|
5961
|
+
commandTimeoutMs: 5000,
|
|
5962
|
+
leaseMs: 1e4,
|
|
5963
|
+
fillTimeoutMs: 30000,
|
|
5964
|
+
waitTimeoutMs: 35000,
|
|
5965
|
+
pollMs: 25,
|
|
5966
|
+
metadataTtlMs: 60000,
|
|
5967
|
+
metadataLimit: 4096,
|
|
5968
|
+
...options
|
|
5969
|
+
};
|
|
5970
|
+
for (const [key, value] of Object.entries(this.options))
|
|
5971
|
+
if (!Number.isSafeInteger(value) || value < 1)
|
|
5972
|
+
throw new RangeError(`${key} must be a positive safe integer.`);
|
|
5973
|
+
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)
|
|
5974
|
+
throw new RangeError("Invalid Redis cache lease, deadline or metadata bounds.");
|
|
5975
|
+
const namespace = namespacedRedisKey("cache:v3:");
|
|
5976
|
+
const slot = new Bun.CryptoHasher("sha256").update(namespace).digest("hex");
|
|
5977
|
+
this.keys = ["entries", "expiry", "order", "metadata", "metadata-expiry"].map((name) => `${namespace}{${slot}}:${name}`);
|
|
5978
|
+
this.client = this.createClient();
|
|
5979
|
+
}
|
|
5980
|
+
createClient() {
|
|
5981
|
+
return new RedisClient(this.redisUrl, {
|
|
5982
|
+
connectionTimeout: this.options.commandTimeoutMs,
|
|
5983
|
+
maxRetries: 1
|
|
6120
5984
|
});
|
|
6121
5985
|
}
|
|
6122
|
-
|
|
6123
|
-
|
|
6124
|
-
|
|
6125
|
-
|
|
5986
|
+
close() {
|
|
5987
|
+
this.closed = true;
|
|
5988
|
+
for (const fill of this.fills)
|
|
5989
|
+
fill.abort(new Error("Redis cache store is closed."));
|
|
5990
|
+
this.client?.close();
|
|
5991
|
+
this.client = undefined;
|
|
5992
|
+
}
|
|
5993
|
+
async get(key) {
|
|
5994
|
+
const result = await this.execute("get", key);
|
|
5995
|
+
return result.kind === "value" ? JSON.parse(result.payload ?? "null") : undefined;
|
|
5996
|
+
}
|
|
5997
|
+
async set(key, value, ttlMs) {
|
|
5998
|
+
await this.execute("set", key, { payload: this.serialize(value), ttlMs: ttlMs ?? this.ttlMs });
|
|
5999
|
+
}
|
|
6000
|
+
async getOrSet(key, loader, ttlMs) {
|
|
6001
|
+
return this.getOrSetTagged(key, loader, [], ttlMs);
|
|
6002
|
+
}
|
|
6003
|
+
async getOrSetTagged(key, loader, tags, ttlMs) {
|
|
6004
|
+
const owner = crypto.randomUUID();
|
|
6005
|
+
const end = performance.now() + this.options.waitTimeoutMs;
|
|
6006
|
+
while (!this.closed) {
|
|
6007
|
+
const claim = await this.execute("claim", key, { owner, tags, ttlMs: ttlMs ?? this.ttlMs });
|
|
6008
|
+
if (claim.kind === "value")
|
|
6009
|
+
return JSON.parse(claim.payload ?? "null");
|
|
6010
|
+
if (claim.kind === "owner")
|
|
6011
|
+
return this.fill(key, owner, loader, ttlMs);
|
|
6012
|
+
if (performance.now() >= end)
|
|
6013
|
+
throw new Error("Redis cache fill wait deadline exceeded.");
|
|
6014
|
+
await Bun.sleep(this.options.pollMs);
|
|
6126
6015
|
}
|
|
6127
|
-
throw
|
|
6016
|
+
throw new Error("Redis cache store is closed.");
|
|
6128
6017
|
}
|
|
6129
|
-
async
|
|
6130
|
-
|
|
6131
|
-
|
|
6132
|
-
|
|
6018
|
+
async attachTags(key, tags) {
|
|
6019
|
+
await this.execute("attach", key, { tags });
|
|
6020
|
+
}
|
|
6021
|
+
async flushTags(tags) {
|
|
6022
|
+
return (await this.execute("tags", "", { tags })).removed ?? 0;
|
|
6023
|
+
}
|
|
6024
|
+
async invalidate(key) {
|
|
6025
|
+
return ((await this.execute("invalidate", key)).removed ?? 0) > 0;
|
|
6026
|
+
}
|
|
6027
|
+
async invalidateByPrefix(prefix) {
|
|
6028
|
+
return (await this.execute("prefix", prefix)).removed ?? 0;
|
|
6029
|
+
}
|
|
6030
|
+
async clear() {
|
|
6031
|
+
await this.execute("clear", "");
|
|
6032
|
+
}
|
|
6033
|
+
async size() {
|
|
6034
|
+
return (await this.execute("size", "")).size ?? 0;
|
|
6035
|
+
}
|
|
6036
|
+
async fill(key, owner, loader, ttlMs) {
|
|
6037
|
+
if (this.fills.size >= this.maxEntries) {
|
|
6038
|
+
await this.execute("release", key, { owner });
|
|
6039
|
+
throw new Error("Redis cache local fill capacity exceeded.");
|
|
6133
6040
|
}
|
|
6134
|
-
|
|
6135
|
-
|
|
6041
|
+
const controller = new AbortController;
|
|
6042
|
+
this.fills.add(controller);
|
|
6043
|
+
let done = false;
|
|
6044
|
+
let renewing = false;
|
|
6045
|
+
const renew = setInterval(() => {
|
|
6046
|
+
if (renewing || done)
|
|
6047
|
+
return;
|
|
6048
|
+
renewing = true;
|
|
6049
|
+
this.execute("renew", key, { owner }).then((result) => {
|
|
6050
|
+
if (!result.owned)
|
|
6051
|
+
clearInterval(renew);
|
|
6052
|
+
}).catch((error) => {
|
|
6053
|
+
if (!done)
|
|
6054
|
+
controller.abort(error);
|
|
6055
|
+
}).finally(() => {
|
|
6056
|
+
renewing = false;
|
|
6057
|
+
});
|
|
6058
|
+
}, Math.max(10, Math.floor(this.options.leaseMs / 3)));
|
|
6059
|
+
const timeout = setTimeout(() => controller.abort(new Error("Redis cache fill deadline exceeded.")), this.options.fillTimeoutMs);
|
|
6060
|
+
let rejectAbort;
|
|
6061
|
+
const aborted = new Promise((_, reject) => {
|
|
6062
|
+
rejectAbort = () => reject(controller.signal.reason);
|
|
6063
|
+
controller.signal.addEventListener("abort", rejectAbort, { once: true });
|
|
6136
6064
|
});
|
|
6065
|
+
try {
|
|
6066
|
+
const value = await Promise.race([
|
|
6067
|
+
Promise.resolve().then(() => loader(controller.signal)),
|
|
6068
|
+
aborted
|
|
6069
|
+
]);
|
|
6070
|
+
controller.signal.throwIfAborted();
|
|
6071
|
+
done = true;
|
|
6072
|
+
clearInterval(renew);
|
|
6073
|
+
await this.execute("publish", key, {
|
|
6074
|
+
owner,
|
|
6075
|
+
payload: this.serialize(value),
|
|
6076
|
+
ttlMs: ttlMs ?? this.ttlMs
|
|
6077
|
+
});
|
|
6078
|
+
return value;
|
|
6079
|
+
} finally {
|
|
6080
|
+
done = true;
|
|
6081
|
+
clearInterval(renew);
|
|
6082
|
+
clearTimeout(timeout);
|
|
6083
|
+
controller.signal.removeEventListener("abort", rejectAbort);
|
|
6084
|
+
this.fills.delete(controller);
|
|
6085
|
+
if (!this.closed)
|
|
6086
|
+
await this.execute("release", key, { owner }).catch(() => {});
|
|
6087
|
+
}
|
|
6137
6088
|
}
|
|
6138
|
-
|
|
6139
|
-
const
|
|
6140
|
-
|
|
6089
|
+
serialize(value) {
|
|
6090
|
+
const payload = JSON.stringify(value);
|
|
6091
|
+
if (typeof payload !== "string")
|
|
6092
|
+
throw new TypeError("Redis cache values must be JSON serializable.");
|
|
6093
|
+
return payload;
|
|
6141
6094
|
}
|
|
6142
|
-
async
|
|
6143
|
-
|
|
6144
|
-
|
|
6145
|
-
|
|
6146
|
-
|
|
6095
|
+
async execute(op, key, extra = {}) {
|
|
6096
|
+
if (this.closed)
|
|
6097
|
+
throw new Error("Redis cache store is closed.");
|
|
6098
|
+
if (Buffer.byteLength(key) > 4096)
|
|
6099
|
+
throw new RangeError("Invalid Redis cache identity.");
|
|
6100
|
+
const tags = Array.from(new Set(extra.tags ?? []));
|
|
6101
|
+
if (tags.length > 32 || tags.some((tag) => typeof tag !== "string" || Buffer.byteLength(tag) > 1024))
|
|
6102
|
+
throw new RangeError("Redis cache supports at most 32 tags of 1024 bytes each.");
|
|
6103
|
+
const prefixes = [key];
|
|
6104
|
+
for (let i = 0;i < key.length; i++)
|
|
6105
|
+
if (key[i] === "?")
|
|
6106
|
+
prefixes.push(key.slice(0, i));
|
|
6107
|
+
if (prefixes.length > 33)
|
|
6108
|
+
throw new RangeError("Redis cache identities support at most 32 query separators.");
|
|
6109
|
+
const ttlMs = extra.ttlMs ?? this.ttlMs;
|
|
6110
|
+
if (typeof ttlMs !== "number" || !Number.isSafeInteger(ttlMs) || ttlMs < 0)
|
|
6111
|
+
throw new RangeError("ttlMs must be a non-negative safe integer.");
|
|
6112
|
+
if (!this.client)
|
|
6113
|
+
this.client = this.createClient();
|
|
6114
|
+
const client = this.client;
|
|
6115
|
+
const retire = (reason) => {
|
|
6116
|
+
if (this.client !== client)
|
|
6117
|
+
return;
|
|
6118
|
+
this.client = undefined;
|
|
6119
|
+
for (const fill of this.fills)
|
|
6120
|
+
fill.abort(reason);
|
|
6121
|
+
client.close();
|
|
6122
|
+
};
|
|
6123
|
+
let timer;
|
|
6124
|
+
const timeout = new Promise((_, reject) => {
|
|
6125
|
+
timer = setTimeout(() => {
|
|
6126
|
+
const error = new Error("Redis cache command deadline exceeded.");
|
|
6127
|
+
reject(error);
|
|
6128
|
+
retire(error);
|
|
6129
|
+
}, this.options.commandTimeoutMs);
|
|
6130
|
+
});
|
|
6131
|
+
try {
|
|
6132
|
+
const raw = await Promise.race([
|
|
6133
|
+
client.send("EVAL", [
|
|
6134
|
+
CACHE_SCRIPT,
|
|
6135
|
+
String(this.keys.length),
|
|
6136
|
+
...this.keys,
|
|
6137
|
+
JSON.stringify({
|
|
6138
|
+
op,
|
|
6139
|
+
key,
|
|
6140
|
+
capacity: this.maxEntries,
|
|
6141
|
+
owner: crypto.randomUUID(),
|
|
6142
|
+
...this.options,
|
|
6143
|
+
...extra,
|
|
6144
|
+
tags,
|
|
6145
|
+
prefixes,
|
|
6146
|
+
ttlMs
|
|
6147
|
+
})
|
|
6148
|
+
]),
|
|
6149
|
+
timeout
|
|
6150
|
+
]);
|
|
6151
|
+
if (this.closed || this.client !== client)
|
|
6152
|
+
throw new Error("Redis cache connection was retired.");
|
|
6153
|
+
return JSON.parse(String(raw));
|
|
6154
|
+
} catch (error) {
|
|
6155
|
+
if (error && typeof error === "object" && "code" in error && typeof error.code === "string" && error.code.startsWith("ERR_REDIS_CONNECTION"))
|
|
6156
|
+
retire(error);
|
|
6157
|
+
throw error;
|
|
6158
|
+
} finally {
|
|
6159
|
+
clearTimeout(timer);
|
|
6160
|
+
}
|
|
6161
|
+
}
|
|
6162
|
+
}
|
|
6163
|
+
var redisCacheStore_default = RedisCacheStore;
|
|
6164
|
+
|
|
6165
|
+
// ../../src/core/cache/simpleCache.ts
|
|
6166
|
+
class SimpleCache {
|
|
6167
|
+
ttlMs;
|
|
6168
|
+
maxEntries;
|
|
6169
|
+
cache = new Map;
|
|
6170
|
+
inflight = new Map;
|
|
6171
|
+
tagIndex = new Map;
|
|
6172
|
+
keyTags = new Map;
|
|
6173
|
+
constructor(ttlMs = 3600000, maxEntries = 100) {
|
|
6174
|
+
this.ttlMs = ttlMs;
|
|
6175
|
+
this.maxEntries = maxEntries;
|
|
6176
|
+
if (!Number.isFinite(ttlMs) || ttlMs < 0) {
|
|
6177
|
+
throw new RangeError("ttlMs must be a non-negative number.");
|
|
6178
|
+
}
|
|
6179
|
+
if (!Number.isInteger(maxEntries) || maxEntries < 1) {
|
|
6180
|
+
throw new RangeError("maxEntries must be a positive integer.");
|
|
6181
|
+
}
|
|
6182
|
+
}
|
|
6183
|
+
get(key) {
|
|
6184
|
+
return this.getFreshEntry(key)?.value;
|
|
6185
|
+
}
|
|
6186
|
+
set(key, value, ttlMs) {
|
|
6187
|
+
const now = Date.now();
|
|
6188
|
+
const resolvedTtlMs = ttlMs ?? this.ttlMs;
|
|
6189
|
+
this.cache.set(key, {
|
|
6190
|
+
value,
|
|
6191
|
+
expiresAt: now + resolvedTtlMs,
|
|
6192
|
+
lastAccessedAt: now
|
|
6147
6193
|
});
|
|
6194
|
+
this.evictOverflow();
|
|
6148
6195
|
}
|
|
6149
|
-
async
|
|
6150
|
-
|
|
6151
|
-
|
|
6152
|
-
|
|
6153
|
-
return
|
|
6196
|
+
async getOrSet(key, loader, ttlMs) {
|
|
6197
|
+
this.pruneExpired();
|
|
6198
|
+
const cachedEntry = this.getFreshEntry(key);
|
|
6199
|
+
if (cachedEntry) {
|
|
6200
|
+
return cachedEntry.value;
|
|
6201
|
+
}
|
|
6202
|
+
const inflightRequest = this.inflight.get(key);
|
|
6203
|
+
if (inflightRequest) {
|
|
6204
|
+
return inflightRequest;
|
|
6205
|
+
}
|
|
6206
|
+
const pendingRequest = loader().then((value) => {
|
|
6207
|
+
this.set(key, value, ttlMs);
|
|
6208
|
+
return value;
|
|
6209
|
+
}).finally(() => {
|
|
6210
|
+
this.inflight.delete(key);
|
|
6154
6211
|
});
|
|
6212
|
+
this.inflight.set(key, pendingRequest);
|
|
6213
|
+
return pendingRequest;
|
|
6155
6214
|
}
|
|
6156
|
-
|
|
6157
|
-
|
|
6215
|
+
attachTags(key, tags) {
|
|
6216
|
+
if (tags.length === 0) {
|
|
6217
|
+
return;
|
|
6218
|
+
}
|
|
6219
|
+
let tagsForKey = this.keyTags.get(key);
|
|
6220
|
+
if (!tagsForKey) {
|
|
6221
|
+
tagsForKey = new Set;
|
|
6222
|
+
this.keyTags.set(key, tagsForKey);
|
|
6223
|
+
}
|
|
6224
|
+
for (const tag of tags) {
|
|
6225
|
+
tagsForKey.add(tag);
|
|
6226
|
+
let keysForTag = this.tagIndex.get(tag);
|
|
6227
|
+
if (!keysForTag) {
|
|
6228
|
+
keysForTag = new Set;
|
|
6229
|
+
this.tagIndex.set(tag, keysForTag);
|
|
6230
|
+
}
|
|
6231
|
+
keysForTag.add(key);
|
|
6232
|
+
}
|
|
6158
6233
|
}
|
|
6159
|
-
|
|
6160
|
-
|
|
6161
|
-
|
|
6162
|
-
const
|
|
6163
|
-
|
|
6164
|
-
|
|
6165
|
-
throw new Error(`Insert into ${this.table.name} did not return a record.`);
|
|
6234
|
+
flushTags(tags) {
|
|
6235
|
+
const keysToRemove = new Set;
|
|
6236
|
+
for (const tag of tags) {
|
|
6237
|
+
const keys = this.tagIndex.get(tag);
|
|
6238
|
+
if (!keys) {
|
|
6239
|
+
continue;
|
|
6166
6240
|
}
|
|
6167
|
-
const
|
|
6168
|
-
|
|
6169
|
-
|
|
6170
|
-
}
|
|
6241
|
+
for (const key of keys) {
|
|
6242
|
+
keysToRemove.add(key);
|
|
6243
|
+
}
|
|
6244
|
+
}
|
|
6245
|
+
let removed = 0;
|
|
6246
|
+
for (const key of keysToRemove) {
|
|
6247
|
+
if (this.invalidate(key)) {
|
|
6248
|
+
removed += 1;
|
|
6249
|
+
}
|
|
6250
|
+
}
|
|
6251
|
+
for (const tag of tags) {
|
|
6252
|
+
this.tagIndex.delete(tag);
|
|
6253
|
+
}
|
|
6254
|
+
return removed;
|
|
6171
6255
|
}
|
|
6172
|
-
|
|
6173
|
-
|
|
6256
|
+
invalidate(key) {
|
|
6257
|
+
const removed = this.cache.delete(key);
|
|
6258
|
+
if (removed) {
|
|
6259
|
+
this.detachKeyFromTags(key);
|
|
6260
|
+
}
|
|
6261
|
+
return removed;
|
|
6174
6262
|
}
|
|
6175
|
-
|
|
6176
|
-
|
|
6263
|
+
invalidateByPrefix(prefix) {
|
|
6264
|
+
let removed = 0;
|
|
6265
|
+
for (const key of [...this.cache.keys()]) {
|
|
6266
|
+
if (key === prefix || key.startsWith(`${prefix}?`)) {
|
|
6267
|
+
if (this.invalidate(key)) {
|
|
6268
|
+
removed += 1;
|
|
6269
|
+
}
|
|
6270
|
+
}
|
|
6271
|
+
}
|
|
6272
|
+
return removed;
|
|
6177
6273
|
}
|
|
6178
|
-
|
|
6179
|
-
|
|
6180
|
-
|
|
6274
|
+
clear() {
|
|
6275
|
+
this.cache.clear();
|
|
6276
|
+
this.inflight.clear();
|
|
6277
|
+
this.tagIndex.clear();
|
|
6278
|
+
this.keyTags.clear();
|
|
6181
6279
|
}
|
|
6182
|
-
|
|
6183
|
-
|
|
6184
|
-
return
|
|
6185
|
-
const rows = await this.connection.unsafe(text, params);
|
|
6186
|
-
if (currentSqlDialect().driver !== "mysql")
|
|
6187
|
-
return rows.length;
|
|
6188
|
-
const affected = rows[0]?.affectedRows;
|
|
6189
|
-
if (!Number.isSafeInteger(affected) || affected === undefined || affected < 0)
|
|
6190
|
-
throw new Error("MySQL conditional writes require affectedRows metadata from the framework adapter.");
|
|
6191
|
-
return affected;
|
|
6192
|
-
});
|
|
6280
|
+
size() {
|
|
6281
|
+
this.pruneExpired();
|
|
6282
|
+
return this.cache.size;
|
|
6193
6283
|
}
|
|
6194
|
-
|
|
6195
|
-
this.
|
|
6196
|
-
|
|
6197
|
-
|
|
6198
|
-
|
|
6199
|
-
|
|
6200
|
-
|
|
6201
|
-
|
|
6284
|
+
detachKeyFromTags(key) {
|
|
6285
|
+
const tags = this.keyTags.get(key);
|
|
6286
|
+
if (!tags) {
|
|
6287
|
+
return;
|
|
6288
|
+
}
|
|
6289
|
+
for (const tag of tags) {
|
|
6290
|
+
const keys = this.tagIndex.get(tag);
|
|
6291
|
+
if (!keys) {
|
|
6292
|
+
continue;
|
|
6293
|
+
}
|
|
6294
|
+
keys.delete(key);
|
|
6295
|
+
if (keys.size === 0) {
|
|
6296
|
+
this.tagIndex.delete(tag);
|
|
6202
6297
|
}
|
|
6203
|
-
return entity;
|
|
6204
|
-
}));
|
|
6205
|
-
}
|
|
6206
|
-
async updateByIdOrThrow(id, changes, errorFactory) {
|
|
6207
|
-
const record = await this.updateById(id, changes);
|
|
6208
|
-
if (record) {
|
|
6209
|
-
return record;
|
|
6210
6298
|
}
|
|
6211
|
-
|
|
6299
|
+
this.keyTags.delete(key);
|
|
6212
6300
|
}
|
|
6213
|
-
|
|
6214
|
-
|
|
6215
|
-
|
|
6301
|
+
getFreshEntry(key) {
|
|
6302
|
+
const entry = this.cache.get(key);
|
|
6303
|
+
if (!entry) {
|
|
6304
|
+
return;
|
|
6216
6305
|
}
|
|
6217
|
-
|
|
6306
|
+
if (entry.expiresAt <= Date.now()) {
|
|
6307
|
+
this.invalidate(key);
|
|
6308
|
+
return;
|
|
6309
|
+
}
|
|
6310
|
+
entry.lastAccessedAt = Date.now();
|
|
6311
|
+
return entry;
|
|
6218
6312
|
}
|
|
6219
|
-
|
|
6220
|
-
|
|
6221
|
-
|
|
6222
|
-
|
|
6223
|
-
|
|
6224
|
-
if (!record) {
|
|
6225
|
-
return false;
|
|
6313
|
+
pruneExpired() {
|
|
6314
|
+
const now = Date.now();
|
|
6315
|
+
for (const [key, entry] of this.cache.entries()) {
|
|
6316
|
+
if (entry.expiresAt <= now) {
|
|
6317
|
+
this.invalidate(key);
|
|
6226
6318
|
}
|
|
6227
|
-
|
|
6228
|
-
return true;
|
|
6229
|
-
}));
|
|
6319
|
+
}
|
|
6230
6320
|
}
|
|
6231
|
-
|
|
6232
|
-
this.
|
|
6233
|
-
|
|
6234
|
-
|
|
6235
|
-
const [
|
|
6236
|
-
|
|
6237
|
-
|
|
6321
|
+
evictOverflow() {
|
|
6322
|
+
while (this.cache.size > this.maxEntries) {
|
|
6323
|
+
let oldestKey;
|
|
6324
|
+
let oldestAccessTime = Number.POSITIVE_INFINITY;
|
|
6325
|
+
for (const [key, entry] of this.cache.entries()) {
|
|
6326
|
+
if (entry.lastAccessedAt < oldestAccessTime) {
|
|
6327
|
+
oldestAccessTime = entry.lastAccessedAt;
|
|
6328
|
+
oldestKey = key;
|
|
6329
|
+
}
|
|
6238
6330
|
}
|
|
6239
|
-
|
|
6240
|
-
|
|
6241
|
-
});
|
|
6242
|
-
return true;
|
|
6243
|
-
}));
|
|
6244
|
-
}
|
|
6245
|
-
async restoreById(id) {
|
|
6246
|
-
this.assertTransactionalEvent("restored");
|
|
6247
|
-
return await this.runModelMutation("restored", () => withDatabaseErrorHandling(async () => {
|
|
6248
|
-
const { text, params } = buildRestoreByIdQuery(this.table, id);
|
|
6249
|
-
const [record] = await this.connection.unsafe(text, params);
|
|
6250
|
-
if (!record) {
|
|
6251
|
-
return null;
|
|
6331
|
+
if (!oldestKey) {
|
|
6332
|
+
return;
|
|
6252
6333
|
}
|
|
6253
|
-
|
|
6254
|
-
await dispatchModelEvent(modelEventName(this.table.name, "restored"), entity);
|
|
6255
|
-
return entity;
|
|
6256
|
-
}));
|
|
6257
|
-
}
|
|
6258
|
-
primaryKeySeekDirection(options) {
|
|
6259
|
-
if (options.joins?.length || options.groupBy || options.having || options.select?.length || options.offset !== undefined || options.limit !== undefined || options.lock) {
|
|
6260
|
-
return null;
|
|
6261
|
-
}
|
|
6262
|
-
const orders = listOrders(options.orderBy ?? this.table.defaultOrderBy);
|
|
6263
|
-
const order = orders[0];
|
|
6264
|
-
if (orders.length !== 1 || !order || order.nulls || order.column !== this.table.primaryKey) {
|
|
6265
|
-
return null;
|
|
6334
|
+
this.invalidate(oldestKey);
|
|
6266
6335
|
}
|
|
6267
|
-
return order.direction;
|
|
6268
6336
|
}
|
|
6269
|
-
|
|
6270
|
-
|
|
6271
|
-
|
|
6272
|
-
|
|
6273
|
-
|
|
6337
|
+
}
|
|
6338
|
+
var simpleCache_default = SimpleCache;
|
|
6339
|
+
|
|
6340
|
+
// ../../src/core/cache/simpleCacheStore.ts
|
|
6341
|
+
class SimpleCacheStore {
|
|
6342
|
+
cache;
|
|
6343
|
+
constructor(cache) {
|
|
6344
|
+
this.cache = cache;
|
|
6274
6345
|
}
|
|
6275
|
-
|
|
6276
|
-
return this.
|
|
6346
|
+
get(key) {
|
|
6347
|
+
return Promise.resolve(this.cache.get(key));
|
|
6277
6348
|
}
|
|
6278
|
-
|
|
6279
|
-
|
|
6349
|
+
set(key, value, ttlMs) {
|
|
6350
|
+
this.cache.set(key, value, ttlMs);
|
|
6351
|
+
return Promise.resolve();
|
|
6280
6352
|
}
|
|
6281
|
-
|
|
6282
|
-
return
|
|
6353
|
+
getOrSet(key, loader, ttlMs) {
|
|
6354
|
+
return this.cache.getOrSet(key, loader, ttlMs);
|
|
6283
6355
|
}
|
|
6284
|
-
|
|
6285
|
-
|
|
6356
|
+
attachTags(key, tags) {
|
|
6357
|
+
this.cache.attachTags(key, tags);
|
|
6358
|
+
return Promise.resolve();
|
|
6286
6359
|
}
|
|
6287
|
-
|
|
6288
|
-
|
|
6289
|
-
const [row] = await this.connection.unsafe(text, params);
|
|
6290
|
-
return Number(row?.count ?? 0);
|
|
6360
|
+
flushTags(tags) {
|
|
6361
|
+
return Promise.resolve(this.cache.flushTags(tags));
|
|
6291
6362
|
}
|
|
6292
|
-
|
|
6293
|
-
|
|
6294
|
-
return await this.averageExpression(`AVG(${qualifiedColumn})`, "value", where);
|
|
6363
|
+
invalidate(key) {
|
|
6364
|
+
return Promise.resolve(this.cache.invalidate(key));
|
|
6295
6365
|
}
|
|
6296
|
-
|
|
6297
|
-
|
|
6298
|
-
const [row] = await this.connection.unsafe(text, params);
|
|
6299
|
-
return Number(row?.[alias] ?? 0);
|
|
6366
|
+
invalidateByPrefix(prefix) {
|
|
6367
|
+
return Promise.resolve(this.cache.invalidateByPrefix(prefix));
|
|
6300
6368
|
}
|
|
6301
|
-
|
|
6302
|
-
|
|
6303
|
-
|
|
6304
|
-
return withDatabaseErrorHandling(() => this.connection.unsafe(text, params));
|
|
6369
|
+
clear() {
|
|
6370
|
+
this.cache.clear();
|
|
6371
|
+
return Promise.resolve();
|
|
6305
6372
|
}
|
|
6306
|
-
|
|
6307
|
-
return
|
|
6373
|
+
size() {
|
|
6374
|
+
return Promise.resolve(this.cache.size());
|
|
6308
6375
|
}
|
|
6309
|
-
|
|
6310
|
-
|
|
6376
|
+
}
|
|
6377
|
+
var simpleCacheStore_default = SimpleCacheStore;
|
|
6378
|
+
|
|
6379
|
+
// ../../src/core/cache/createCacheStore.ts
|
|
6380
|
+
function createCacheStore(options) {
|
|
6381
|
+
if (options.driver === "redis") {
|
|
6382
|
+
if (!options.redisUrl) {
|
|
6383
|
+
throw new Error('CACHE_DRIVER="redis" requires REDIS_URL to be set.');
|
|
6384
|
+
}
|
|
6385
|
+
return new redisCacheStore_default(options.redisUrl, options.ttlMs, options.maxEntries, options.redis);
|
|
6311
6386
|
}
|
|
6312
|
-
|
|
6313
|
-
|
|
6387
|
+
return new simpleCacheStore_default(new simpleCache_default(options.ttlMs, options.maxEntries));
|
|
6388
|
+
}
|
|
6389
|
+
// ../../src/core/cache/taggedCache.ts
|
|
6390
|
+
class TaggedCache {
|
|
6391
|
+
store;
|
|
6392
|
+
tags;
|
|
6393
|
+
constructor(store, tags) {
|
|
6394
|
+
this.store = store;
|
|
6395
|
+
this.tags = tags;
|
|
6314
6396
|
}
|
|
6315
|
-
async
|
|
6316
|
-
|
|
6397
|
+
async remember(key, callback, ttlMs) {
|
|
6398
|
+
if (this.store.getOrSetTagged)
|
|
6399
|
+
return this.store.getOrSetTagged(key, callback, this.tags, ttlMs);
|
|
6400
|
+
const value = await this.store.getOrSet(key, callback, ttlMs);
|
|
6401
|
+
await this.store.attachTags(key, this.tags);
|
|
6402
|
+
return value;
|
|
6317
6403
|
}
|
|
6318
|
-
async
|
|
6319
|
-
|
|
6320
|
-
return await this.averageExpression(`${fn}(${qualifiedColumn})`, "value", where);
|
|
6404
|
+
async flush() {
|
|
6405
|
+
return this.store.flushTags(this.tags);
|
|
6321
6406
|
}
|
|
6322
|
-
|
|
6323
|
-
|
|
6324
|
-
|
|
6325
|
-
|
|
6326
|
-
|
|
6327
|
-
|
|
6328
|
-
|
|
6407
|
+
}
|
|
6408
|
+
var taggedCache_default = TaggedCache;
|
|
6409
|
+
|
|
6410
|
+
// ../../src/core/cache/repository.ts
|
|
6411
|
+
class CacheRepository {
|
|
6412
|
+
store;
|
|
6413
|
+
constructor(store) {
|
|
6414
|
+
this.store = store;
|
|
6329
6415
|
}
|
|
6330
|
-
async
|
|
6331
|
-
|
|
6332
|
-
const rows = await this.connection.unsafe(text, params);
|
|
6333
|
-
return rows.map(({ value, count }) => ({
|
|
6334
|
-
value,
|
|
6335
|
-
count: Number(count)
|
|
6336
|
-
}));
|
|
6416
|
+
async close() {
|
|
6417
|
+
await this.store.close?.();
|
|
6337
6418
|
}
|
|
6338
|
-
async
|
|
6339
|
-
return
|
|
6340
|
-
[relation.foreignKey]: parentId
|
|
6341
|
-
}, options);
|
|
6419
|
+
async get(key) {
|
|
6420
|
+
return this.store.get(key);
|
|
6342
6421
|
}
|
|
6343
|
-
async
|
|
6344
|
-
|
|
6345
|
-
return indexHasManyRelation(parents, [], relation);
|
|
6346
|
-
}
|
|
6347
|
-
const parentIds = [...new Set(parents.map((parent) => parent[relation.localKey]))];
|
|
6348
|
-
const children = await this.findWhere({
|
|
6349
|
-
[relation.foreignKey]: parentIds
|
|
6350
|
-
}, options);
|
|
6351
|
-
return indexHasManyRelation(parents, children, relation);
|
|
6422
|
+
async remember(key, callback, ttlMs) {
|
|
6423
|
+
return this.store.getOrSet(key, callback, ttlMs);
|
|
6352
6424
|
}
|
|
6353
|
-
async
|
|
6354
|
-
|
|
6355
|
-
return getByRelationKey(grouped, parentId) ?? [];
|
|
6425
|
+
async forget(key) {
|
|
6426
|
+
return this.store.invalidate(key);
|
|
6356
6427
|
}
|
|
6357
|
-
async
|
|
6358
|
-
|
|
6359
|
-
return indexHasManyThroughRelation(parents, [], relation);
|
|
6360
|
-
}
|
|
6361
|
-
if (options.joins?.some((join) => join.table === relation.throughTable)) {
|
|
6362
|
-
throw new Error("A related model scope joins the through table twice; use an explicitly aliased correlated predicate instead.");
|
|
6363
|
-
}
|
|
6364
|
-
const parentIds = [...new Set(parents.map((parent) => parent[relation.localKey]))];
|
|
6365
|
-
const throughParentKey = relation.throughParentKey ?? "__through_parent_id";
|
|
6366
|
-
const farTable = this.table.name;
|
|
6367
|
-
const columns = this.table.columns.map((column) => `${qualifyColumn(farTable, column)}`).join(", ");
|
|
6368
|
-
const placeholders = parentIds.map((_, index) => currentSqlDialect().placeholder(index + 1)).join(", ");
|
|
6369
|
-
const { text: extraWhere, params: extraParams } = this.buildThroughWhere(options, parentIds);
|
|
6370
|
-
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}`;
|
|
6371
|
-
const children = await this.connection.unsafe(sql, [
|
|
6372
|
-
...parentIds,
|
|
6373
|
-
...extraParams
|
|
6374
|
-
]);
|
|
6375
|
-
return indexHasManyThroughRelation(parents, children, relation);
|
|
6428
|
+
async flush() {
|
|
6429
|
+
await this.store.clear();
|
|
6376
6430
|
}
|
|
6377
|
-
|
|
6378
|
-
|
|
6379
|
-
const { clause } = buildQueryWhereClause(this.table, options, options.whereNodes, parameters);
|
|
6380
|
-
return {
|
|
6381
|
-
text: clause ? ` AND (${clause.replace(/^ WHERE /, "")})` : "",
|
|
6382
|
-
params: parameters.slice(parentIds.length)
|
|
6383
|
-
};
|
|
6431
|
+
tags(...names) {
|
|
6432
|
+
return new taggedCache_default(this.store, names);
|
|
6384
6433
|
}
|
|
6385
|
-
async
|
|
6386
|
-
|
|
6387
|
-
return new Map;
|
|
6388
|
-
}
|
|
6389
|
-
const ownerIds = [...new Set(children.map((child) => child[relation.foreignKey]))];
|
|
6390
|
-
const parents = await parentRepository.withConnection(this.connection).findWhere({
|
|
6391
|
-
[relation.ownerKey]: ownerIds
|
|
6392
|
-
}, options);
|
|
6393
|
-
return indexBelongsToRelation(children, parents, relation);
|
|
6434
|
+
async getOrSet(key, loader, ttlMs) {
|
|
6435
|
+
return this.remember(key, loader, ttlMs);
|
|
6394
6436
|
}
|
|
6395
|
-
async
|
|
6396
|
-
|
|
6397
|
-
return indexMorphManyRelation(parents, [], relation);
|
|
6398
|
-
}
|
|
6399
|
-
const parentIds = [...new Set(parents.map((parent) => parent[relation.localKey]))];
|
|
6400
|
-
const children = await this.findWhere({
|
|
6401
|
-
[relation.morphTypeKey]: relation.morphType,
|
|
6402
|
-
[relation.morphIdKey]: parentIds
|
|
6403
|
-
}, options);
|
|
6404
|
-
return indexMorphManyRelation(parents, children, relation);
|
|
6437
|
+
async invalidate(key) {
|
|
6438
|
+
return this.forget(key);
|
|
6405
6439
|
}
|
|
6406
|
-
async
|
|
6407
|
-
|
|
6408
|
-
const result = new Map;
|
|
6409
|
-
for (const parent of parents) {
|
|
6410
|
-
const matches = getByRelationKey(grouped, parent[relation.localKey]) ?? [];
|
|
6411
|
-
result.set(parent[relation.localKey], matches[0]);
|
|
6412
|
-
}
|
|
6413
|
-
return result;
|
|
6440
|
+
async invalidateByPrefix(prefix) {
|
|
6441
|
+
return this.store.invalidateByPrefix(prefix);
|
|
6414
6442
|
}
|
|
6415
|
-
async
|
|
6416
|
-
|
|
6417
|
-
return new Map;
|
|
6418
|
-
}
|
|
6419
|
-
const idsByType = new Map;
|
|
6420
|
-
for (const child of children) {
|
|
6421
|
-
const morphType = String(child[relation.morphTypeKey]);
|
|
6422
|
-
const morphId = child[relation.morphIdKey];
|
|
6423
|
-
const ids = idsByType.get(morphType) ?? new Set;
|
|
6424
|
-
ids.add(morphId);
|
|
6425
|
-
idsByType.set(morphType, ids);
|
|
6426
|
-
}
|
|
6427
|
-
const parentsByType = new Map;
|
|
6428
|
-
await Promise.all([...idsByType.entries()].map(async ([morphType, ids]) => {
|
|
6429
|
-
const repository = repositoriesByType.get(morphType);
|
|
6430
|
-
if (!repository) {
|
|
6431
|
-
return;
|
|
6432
|
-
}
|
|
6433
|
-
const ownerKey = repository.getTable().primaryKey;
|
|
6434
|
-
const parents = await repository.withConnection(this.connection).findWhere({
|
|
6435
|
-
[ownerKey]: [...ids]
|
|
6436
|
-
}, options);
|
|
6437
|
-
const indexed = new Map;
|
|
6438
|
-
for (const parent of parents) {
|
|
6439
|
-
indexed.set(relationMatchKey(parent[ownerKey]), parent);
|
|
6440
|
-
}
|
|
6441
|
-
parentsByType.set(morphType, indexed);
|
|
6442
|
-
}));
|
|
6443
|
-
return indexMorphToRelation(children, parentsByType, relation);
|
|
6443
|
+
async clear() {
|
|
6444
|
+
await this.flush();
|
|
6444
6445
|
}
|
|
6445
|
-
async
|
|
6446
|
-
|
|
6447
|
-
return indexBelongsToManyRelation(parents, [], [], relation);
|
|
6448
|
-
}
|
|
6449
|
-
const parentIds = [...new Set(parents.map((parent) => parent[relation.parentKey]))];
|
|
6450
|
-
const placeholders = parentIds.map((_, index) => currentSqlDialect().placeholder(index + 1)).join(", ");
|
|
6451
|
-
const pivotRows = await this.connection.unsafe(`SELECT * FROM ${relation.pivotTable} WHERE ${String(relation.foreignPivotKey)} IN (${placeholders})`, parentIds);
|
|
6452
|
-
if (pivotRows.length === 0) {
|
|
6453
|
-
return indexBelongsToManyRelation(parents, [], [], relation);
|
|
6454
|
-
}
|
|
6455
|
-
const relatedIds = [
|
|
6456
|
-
...new Set(pivotRows.map((row) => row[relation.relatedPivotKey]))
|
|
6457
|
-
];
|
|
6458
|
-
const relatedRows = await relatedRepository.withConnection(this.connection).findWhere({
|
|
6459
|
-
[relation.relatedKey]: relatedIds
|
|
6460
|
-
}, options);
|
|
6461
|
-
return indexBelongsToManyRelation(parents, pivotRows, relatedRows, relation);
|
|
6446
|
+
async size() {
|
|
6447
|
+
return this.store.size();
|
|
6462
6448
|
}
|
|
6463
6449
|
}
|
|
6464
|
-
var
|
|
6465
|
-
|
|
6450
|
+
var repository_default = CacheRepository;
|
|
6451
|
+
// ../../src/core/cache/tags.ts
|
|
6452
|
+
var CACHE_TAGS = {
|
|
6453
|
+
organizations: "organizations",
|
|
6454
|
+
projects: "projects",
|
|
6455
|
+
tasks: "tasks",
|
|
6456
|
+
comments: "comments",
|
|
6457
|
+
attachments: "attachments",
|
|
6458
|
+
reports: "reports",
|
|
6459
|
+
users: "users",
|
|
6460
|
+
departments: "departments",
|
|
6461
|
+
positions: "positions",
|
|
6462
|
+
applications: "applications",
|
|
6463
|
+
careers: "careers",
|
|
6464
|
+
offers: "offers"
|
|
6465
|
+
};
|
|
6466
6466
|
// ../../src/core/crypto/fieldEncryptionRotation.ts
|
|
6467
|
+
import { createHash as createHash3, randomUUID } from "crypto";
|
|
6467
6468
|
init_boundConnection();
|
|
6468
6469
|
|
|
6469
6470
|
// ../../src/core/database/migrations/rollbackGuard.ts
|
|
@@ -7128,11 +7129,6 @@ function createSchemaBuilder(db, driver) {
|
|
|
7128
7129
|
}
|
|
7129
7130
|
});
|
|
7130
7131
|
}
|
|
7131
|
-
// ../../src/core/database/table.ts
|
|
7132
|
-
function defineTable(definition) {
|
|
7133
|
-
return definition;
|
|
7134
|
-
}
|
|
7135
|
-
|
|
7136
7132
|
// ../../src/core/crypto/fieldEncryptionRotation.ts
|
|
7137
7133
|
init_transaction();
|
|
7138
7134
|
|
|
@@ -15929,7 +15925,6 @@ init_tenantDatabaseScope();
|
|
|
15929
15925
|
|
|
15930
15926
|
// ../../src/core/tenant/tenantMiddleware.ts
|
|
15931
15927
|
import { createHash as createHash8 } from "crypto";
|
|
15932
|
-
init_repositoryConnection();
|
|
15933
15928
|
init_databaseTenantContext();
|
|
15934
15929
|
|
|
15935
15930
|
// ../../src/core/tenant/publicTenantHost.ts
|
|
@@ -15985,18 +15980,18 @@ var DEFAULT_TENANT = {
|
|
|
15985
15980
|
id: 1,
|
|
15986
15981
|
slug: "default"
|
|
15987
15982
|
};
|
|
15983
|
+
var userTenants = new BaseRepository(defineTable({
|
|
15984
|
+
name: "users",
|
|
15985
|
+
primaryKey: "id",
|
|
15986
|
+
columns: ["id", "tenant_id"]
|
|
15987
|
+
}));
|
|
15988
15988
|
async function resolveUserTenantId(userId) {
|
|
15989
15989
|
if (!isTenancyEnabled()) {
|
|
15990
15990
|
return DEFAULT_TENANT.id;
|
|
15991
15991
|
}
|
|
15992
15992
|
return await runWithMigrationBypassForIdentifier(userId, async () => {
|
|
15993
|
-
const
|
|
15994
|
-
|
|
15995
|
-
FROM users
|
|
15996
|
-
WHERE id = ${userId}
|
|
15997
|
-
LIMIT 1
|
|
15998
|
-
`;
|
|
15999
|
-
return rows[0]?.tenant_id ?? DEFAULT_TENANT.id;
|
|
15993
|
+
const tenantId = await userTenants.query().where({ id: userId }).value("tenant_id");
|
|
15994
|
+
return tenantId ?? DEFAULT_TENANT.id;
|
|
16000
15995
|
});
|
|
16001
15996
|
}
|
|
16002
15997
|
function auditChecksum(payload) {
|