@getstrata/core 2.2.32 → 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 +4 -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
|
@@ -42,26 +42,81 @@ var init_connectionContext = __esm(() => {
|
|
|
42
42
|
activeConnection = createAsyncContextStore("@getstrata/databaseConnectionContext");
|
|
43
43
|
});
|
|
44
44
|
|
|
45
|
-
// ../../src/core/
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
45
|
+
// ../../src/core/events/eventBus.ts
|
|
46
|
+
class EventBus {
|
|
47
|
+
constructor() {}
|
|
48
|
+
transactionalListeners = new Map;
|
|
49
|
+
listenTransactional(event, listener) {
|
|
50
|
+
const handlers = this.transactionalListeners.get(event) ?? new Set;
|
|
51
|
+
handlers.add(listener);
|
|
52
|
+
this.transactionalListeners.set(event, handlers);
|
|
53
|
+
return () => {
|
|
54
|
+
handlers.delete(listener);
|
|
55
|
+
if (!handlers.size)
|
|
56
|
+
this.transactionalListeners.delete(event);
|
|
57
|
+
};
|
|
50
58
|
}
|
|
51
|
-
|
|
52
|
-
return
|
|
59
|
+
hasTransactionalListeners(event) {
|
|
60
|
+
return (this.transactionalListeners.get(event)?.size ?? 0) > 0;
|
|
61
|
+
}
|
|
62
|
+
async dispatchTransactional(event, payload) {
|
|
63
|
+
for (const listener of this.transactionalListeners.get(event) ?? [])
|
|
64
|
+
await listener(payload);
|
|
65
|
+
}
|
|
66
|
+
listeners = new Map;
|
|
67
|
+
on(event, listener) {
|
|
68
|
+
return this.listen(event, listener);
|
|
69
|
+
}
|
|
70
|
+
listen(event, listener) {
|
|
71
|
+
const handlers = this.listeners.get(event) ?? new Set;
|
|
72
|
+
handlers.add(listener);
|
|
73
|
+
this.listeners.set(event, handlers);
|
|
74
|
+
return () => {
|
|
75
|
+
handlers.delete(listener);
|
|
76
|
+
if (handlers.size === 0) {
|
|
77
|
+
this.listeners.delete(event);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
async emit(event, payload) {
|
|
82
|
+
await this.dispatch(event, payload);
|
|
83
|
+
}
|
|
84
|
+
async dispatch(event, payload) {
|
|
85
|
+
const handlers = this.listeners.get(event);
|
|
86
|
+
if (!handlers || handlers.size === 0) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
for (const handler of handlers) {
|
|
90
|
+
await handler(payload);
|
|
91
|
+
}
|
|
53
92
|
}
|
|
54
|
-
throw new Error(`TENANCY_DRIVER must be one of ${TENANCY_DRIVERS.join(", ")}; received "${raw}".`);
|
|
55
93
|
}
|
|
56
|
-
function
|
|
57
|
-
|
|
94
|
+
function readSharedEventBus() {
|
|
95
|
+
const globalBus = globalThis[EVENT_BUS_KEY];
|
|
96
|
+
if (globalBus) {
|
|
97
|
+
return globalBus;
|
|
98
|
+
}
|
|
99
|
+
const bus = new EventBus;
|
|
100
|
+
globalThis[EVENT_BUS_KEY] = bus;
|
|
101
|
+
return bus;
|
|
58
102
|
}
|
|
59
|
-
|
|
60
|
-
|
|
103
|
+
var EVENT_BUS_KEY, eventBus;
|
|
104
|
+
var init_eventBus = __esm(() => {
|
|
105
|
+
EVENT_BUS_KEY = Symbol.for("@getstrata/eventBus");
|
|
106
|
+
eventBus = readSharedEventBus();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// ../../src/core/tenant/tenantContext.ts
|
|
110
|
+
function runWithTenant(tenant, callback) {
|
|
111
|
+
return tenantContext.run(tenant, callback);
|
|
61
112
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
113
|
+
function currentTenant() {
|
|
114
|
+
return tenantContext.getStore() ?? null;
|
|
115
|
+
}
|
|
116
|
+
var tenantContext;
|
|
117
|
+
var init_tenantContext = __esm(() => {
|
|
118
|
+
init_asyncContextStore();
|
|
119
|
+
tenantContext = createAsyncContextStore("@getstrata/tenantContext");
|
|
65
120
|
});
|
|
66
121
|
|
|
67
122
|
// ../../src/core/database/transactionControl.ts
|
|
@@ -142,167 +197,6 @@ var init_transactionControl = __esm(() => {
|
|
|
142
197
|
transactionScopes = createAsyncContextStore("@getstrata/transactionScopes");
|
|
143
198
|
});
|
|
144
199
|
|
|
145
|
-
// ../../src/core/tenant/tenantContext.ts
|
|
146
|
-
function runWithTenant(tenant, callback) {
|
|
147
|
-
return tenantContext.run(tenant, callback);
|
|
148
|
-
}
|
|
149
|
-
function currentTenant() {
|
|
150
|
-
return tenantContext.getStore() ?? null;
|
|
151
|
-
}
|
|
152
|
-
var tenantContext;
|
|
153
|
-
var init_tenantContext = __esm(() => {
|
|
154
|
-
init_asyncContextStore();
|
|
155
|
-
tenantContext = createAsyncContextStore("@getstrata/tenantContext");
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
// ../../src/core/events/eventBus.ts
|
|
159
|
-
class EventBus {
|
|
160
|
-
constructor() {}
|
|
161
|
-
transactionalListeners = new Map;
|
|
162
|
-
listenTransactional(event, listener) {
|
|
163
|
-
const handlers = this.transactionalListeners.get(event) ?? new Set;
|
|
164
|
-
handlers.add(listener);
|
|
165
|
-
this.transactionalListeners.set(event, handlers);
|
|
166
|
-
return () => {
|
|
167
|
-
handlers.delete(listener);
|
|
168
|
-
if (!handlers.size)
|
|
169
|
-
this.transactionalListeners.delete(event);
|
|
170
|
-
};
|
|
171
|
-
}
|
|
172
|
-
hasTransactionalListeners(event) {
|
|
173
|
-
return (this.transactionalListeners.get(event)?.size ?? 0) > 0;
|
|
174
|
-
}
|
|
175
|
-
async dispatchTransactional(event, payload) {
|
|
176
|
-
for (const listener of this.transactionalListeners.get(event) ?? [])
|
|
177
|
-
await listener(payload);
|
|
178
|
-
}
|
|
179
|
-
listeners = new Map;
|
|
180
|
-
on(event, listener) {
|
|
181
|
-
return this.listen(event, listener);
|
|
182
|
-
}
|
|
183
|
-
listen(event, listener) {
|
|
184
|
-
const handlers = this.listeners.get(event) ?? new Set;
|
|
185
|
-
handlers.add(listener);
|
|
186
|
-
this.listeners.set(event, handlers);
|
|
187
|
-
return () => {
|
|
188
|
-
handlers.delete(listener);
|
|
189
|
-
if (handlers.size === 0) {
|
|
190
|
-
this.listeners.delete(event);
|
|
191
|
-
}
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
async emit(event, payload) {
|
|
195
|
-
await this.dispatch(event, payload);
|
|
196
|
-
}
|
|
197
|
-
async dispatch(event, payload) {
|
|
198
|
-
const handlers = this.listeners.get(event);
|
|
199
|
-
if (!handlers || handlers.size === 0) {
|
|
200
|
-
return;
|
|
201
|
-
}
|
|
202
|
-
for (const handler of handlers) {
|
|
203
|
-
await handler(payload);
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
function readSharedEventBus() {
|
|
208
|
-
const globalBus = globalThis[EVENT_BUS_KEY];
|
|
209
|
-
if (globalBus) {
|
|
210
|
-
return globalBus;
|
|
211
|
-
}
|
|
212
|
-
const bus = new EventBus;
|
|
213
|
-
globalThis[EVENT_BUS_KEY] = bus;
|
|
214
|
-
return bus;
|
|
215
|
-
}
|
|
216
|
-
var EVENT_BUS_KEY, eventBus;
|
|
217
|
-
var init_eventBus = __esm(() => {
|
|
218
|
-
EVENT_BUS_KEY = Symbol.for("@getstrata/eventBus");
|
|
219
|
-
eventBus = readSharedEventBus();
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
// ../../src/core/events/deferredModelEvents.ts
|
|
223
|
-
function sameTenant(left, right) {
|
|
224
|
-
if (left === null && right === null) {
|
|
225
|
-
return true;
|
|
226
|
-
}
|
|
227
|
-
if (left === null || right === null) {
|
|
228
|
-
return false;
|
|
229
|
-
}
|
|
230
|
-
return left.id === right.id;
|
|
231
|
-
}
|
|
232
|
-
function groupQueuedModelEvents(queued) {
|
|
233
|
-
const groups = [];
|
|
234
|
-
for (const item of queued) {
|
|
235
|
-
const lastGroup = groups[groups.length - 1];
|
|
236
|
-
const last = lastGroup?.[lastGroup.length - 1];
|
|
237
|
-
if (!lastGroup || !last || !sameTenant(last.tenant, item.tenant)) {
|
|
238
|
-
groups.push([item]);
|
|
239
|
-
} else {
|
|
240
|
-
lastGroup.push(item);
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
return groups;
|
|
244
|
-
}
|
|
245
|
-
async function dispatchEventBatch(batch) {
|
|
246
|
-
for (const item of batch) {
|
|
247
|
-
await eventBus.dispatch(item.event, item.payload);
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
async function dispatchQueuedModelEvents(queued) {
|
|
251
|
-
if (queued.length === 0) {
|
|
252
|
-
return;
|
|
253
|
-
}
|
|
254
|
-
await Promise.resolve().then(() => init_tenantDatabaseScope());
|
|
255
|
-
for (const batch of groupQueuedModelEvents(queued)) {
|
|
256
|
-
const tenant = batch[0]?.tenant ?? null;
|
|
257
|
-
if (tenant) {
|
|
258
|
-
await runWithTenantDatabase(tenant, () => dispatchEventBatch(batch));
|
|
259
|
-
} else {
|
|
260
|
-
await dispatchEventBatch(batch);
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
async function runWithDeferredModelEvents(callback) {
|
|
265
|
-
const existing = deferredModelEvents.getStore();
|
|
266
|
-
if (existing) {
|
|
267
|
-
existing.frames.push([]);
|
|
268
|
-
existing.rollbackFrames.push([]);
|
|
269
|
-
try {
|
|
270
|
-
const result = await callback();
|
|
271
|
-
const nested = existing.frames.pop() ?? [];
|
|
272
|
-
const rollback = existing.rollbackFrames.pop() ?? [];
|
|
273
|
-
existing.rollbackFrames.at(-1)?.push(...rollback);
|
|
274
|
-
const parent = existing.frames[existing.frames.length - 1];
|
|
275
|
-
if (parent) {
|
|
276
|
-
parent.push(...nested);
|
|
277
|
-
}
|
|
278
|
-
return result;
|
|
279
|
-
} catch (error) {
|
|
280
|
-
existing.frames.pop();
|
|
281
|
-
for (const rollback of (existing.rollbackFrames.pop() ?? []).reverse())
|
|
282
|
-
rollback();
|
|
283
|
-
throw error;
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
const state = { frames: [[]], rollbackFrames: [[]] };
|
|
287
|
-
let result;
|
|
288
|
-
try {
|
|
289
|
-
result = await deferredModelEvents.run(state, callback);
|
|
290
|
-
} catch (error) {
|
|
291
|
-
for (const rollback of (state.rollbackFrames[0] ?? []).reverse())
|
|
292
|
-
rollback();
|
|
293
|
-
throw error;
|
|
294
|
-
}
|
|
295
|
-
await dispatchQueuedModelEvents(state.frames[0] ?? []);
|
|
296
|
-
return result;
|
|
297
|
-
}
|
|
298
|
-
var deferredModelEvents;
|
|
299
|
-
var init_deferredModelEvents = __esm(() => {
|
|
300
|
-
init_asyncContextStore();
|
|
301
|
-
init_tenantContext();
|
|
302
|
-
init_eventBus();
|
|
303
|
-
deferredModelEvents = createAsyncContextStore("@getstrata/deferredModelEvents");
|
|
304
|
-
});
|
|
305
|
-
|
|
306
200
|
// ../../src/core/database/acquisitionMetrics.ts
|
|
307
201
|
async function observeTransactionAcquisition(metrics, acquire, signal) {
|
|
308
202
|
const state = states.get(metrics);
|
|
@@ -755,16 +649,38 @@ var init_transaction = __esm(() => {
|
|
|
755
649
|
outerFrames = new WeakMap;
|
|
756
650
|
});
|
|
757
651
|
|
|
758
|
-
// ../../src/core/tenant/
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
652
|
+
// ../../src/core/tenant/tenancyConfig.ts
|
|
653
|
+
function readTenancyDriver(env = process.env) {
|
|
654
|
+
const raw = env.TENANCY_DRIVER?.trim();
|
|
655
|
+
if (raw === undefined || raw === "") {
|
|
656
|
+
return "rls";
|
|
657
|
+
}
|
|
658
|
+
if (TENANCY_DRIVERS.includes(raw)) {
|
|
659
|
+
return raw;
|
|
660
|
+
}
|
|
661
|
+
throw new Error(`TENANCY_DRIVER must be one of ${TENANCY_DRIVERS.join(", ")}; received "${raw}".`);
|
|
662
|
+
}
|
|
663
|
+
function isTenancyEnabled(env = process.env) {
|
|
664
|
+
return readTenancyDriver(env) !== "none";
|
|
665
|
+
}
|
|
666
|
+
function isRlsTenancy(env = process.env) {
|
|
667
|
+
return readTenancyDriver(env) === "rls";
|
|
668
|
+
}
|
|
669
|
+
var TENANCY_DRIVERS;
|
|
670
|
+
var init_tenancyConfig = __esm(() => {
|
|
671
|
+
TENANCY_DRIVERS = ["rls", "column", "none"];
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
// ../../src/core/tenant/databaseTenantContext.ts
|
|
675
|
+
import { getDefaultDatabasePool as getDefaultDatabasePool2 } from "@getstrata/core/database/defaultConnection";
|
|
676
|
+
import { AsyncLocalStorage as AsyncLocalStorage3 } from "async_hooks";
|
|
677
|
+
async function applyBypassToTransaction(transaction, bypass) {
|
|
678
|
+
await transaction.unsafe(`SELECT set_config('app.bypass_rls', $1, true)`, [
|
|
679
|
+
bypass ? "true" : "false"
|
|
680
|
+
]);
|
|
681
|
+
}
|
|
682
|
+
function stringifyBypassIdentifier(identifier) {
|
|
683
|
+
if (typeof identifier === "number") {
|
|
768
684
|
return String(identifier);
|
|
769
685
|
}
|
|
770
686
|
return identifier.trim();
|
|
@@ -875,6 +791,104 @@ var init_tenantDatabaseScope = __esm(() => {
|
|
|
875
791
|
init_tenantContext();
|
|
876
792
|
});
|
|
877
793
|
|
|
794
|
+
// ../../src/core/events/deferredModelEvents.ts
|
|
795
|
+
function sameTenant(left, right) {
|
|
796
|
+
if (left === null && right === null) {
|
|
797
|
+
return true;
|
|
798
|
+
}
|
|
799
|
+
if (left === null || right === null) {
|
|
800
|
+
return false;
|
|
801
|
+
}
|
|
802
|
+
return left.id === right.id;
|
|
803
|
+
}
|
|
804
|
+
function groupQueuedModelEvents(queued) {
|
|
805
|
+
const groups = [];
|
|
806
|
+
for (const item of queued) {
|
|
807
|
+
const lastGroup = groups[groups.length - 1];
|
|
808
|
+
const last = lastGroup?.[lastGroup.length - 1];
|
|
809
|
+
if (!lastGroup || !last || !sameTenant(last.tenant, item.tenant)) {
|
|
810
|
+
groups.push([item]);
|
|
811
|
+
} else {
|
|
812
|
+
lastGroup.push(item);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
return groups;
|
|
816
|
+
}
|
|
817
|
+
async function dispatchEventBatch(batch) {
|
|
818
|
+
for (const item of batch) {
|
|
819
|
+
await eventBus.dispatch(item.event, item.payload);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
async function dispatchQueuedModelEvents(queued) {
|
|
823
|
+
if (queued.length === 0) {
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
await Promise.resolve().then(() => init_tenantDatabaseScope());
|
|
827
|
+
for (const batch of groupQueuedModelEvents(queued)) {
|
|
828
|
+
const tenant = batch[0]?.tenant ?? null;
|
|
829
|
+
if (tenant) {
|
|
830
|
+
await runWithTenantDatabase(tenant, () => dispatchEventBatch(batch));
|
|
831
|
+
} else {
|
|
832
|
+
await dispatchEventBatch(batch);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
async function dispatchModelEvent(event, payload) {
|
|
837
|
+
await eventBus.dispatchTransactional(event, payload);
|
|
838
|
+
const state = deferredModelEvents.getStore();
|
|
839
|
+
const frame = state?.frames[state.frames.length - 1];
|
|
840
|
+
if (frame) {
|
|
841
|
+
frame.push({
|
|
842
|
+
event,
|
|
843
|
+
payload,
|
|
844
|
+
tenant: currentTenant()
|
|
845
|
+
});
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
await eventBus.dispatch(event, payload);
|
|
849
|
+
}
|
|
850
|
+
async function runWithDeferredModelEvents(callback) {
|
|
851
|
+
const existing = deferredModelEvents.getStore();
|
|
852
|
+
if (existing) {
|
|
853
|
+
existing.frames.push([]);
|
|
854
|
+
existing.rollbackFrames.push([]);
|
|
855
|
+
try {
|
|
856
|
+
const result = await callback();
|
|
857
|
+
const nested = existing.frames.pop() ?? [];
|
|
858
|
+
const rollback = existing.rollbackFrames.pop() ?? [];
|
|
859
|
+
existing.rollbackFrames.at(-1)?.push(...rollback);
|
|
860
|
+
const parent = existing.frames[existing.frames.length - 1];
|
|
861
|
+
if (parent) {
|
|
862
|
+
parent.push(...nested);
|
|
863
|
+
}
|
|
864
|
+
return result;
|
|
865
|
+
} catch (error) {
|
|
866
|
+
existing.frames.pop();
|
|
867
|
+
for (const rollback of (existing.rollbackFrames.pop() ?? []).reverse())
|
|
868
|
+
rollback();
|
|
869
|
+
throw error;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
const state = { frames: [[]], rollbackFrames: [[]] };
|
|
873
|
+
let result;
|
|
874
|
+
try {
|
|
875
|
+
result = await deferredModelEvents.run(state, callback);
|
|
876
|
+
} catch (error) {
|
|
877
|
+
for (const rollback of (state.rollbackFrames[0] ?? []).reverse())
|
|
878
|
+
rollback();
|
|
879
|
+
throw error;
|
|
880
|
+
}
|
|
881
|
+
await dispatchQueuedModelEvents(state.frames[0] ?? []);
|
|
882
|
+
return result;
|
|
883
|
+
}
|
|
884
|
+
var deferredModelEvents;
|
|
885
|
+
var init_deferredModelEvents = __esm(() => {
|
|
886
|
+
init_asyncContextStore();
|
|
887
|
+
init_tenantContext();
|
|
888
|
+
init_eventBus();
|
|
889
|
+
deferredModelEvents = createAsyncContextStore("@getstrata/deferredModelEvents");
|
|
890
|
+
});
|
|
891
|
+
|
|
878
892
|
// ../../src/core/auth/scimAuthMiddleware.ts
|
|
879
893
|
init_connectionContext();
|
|
880
894
|
import { bindDatabaseConnection } from "@getstrata/core/database/bindConnection";
|
|
@@ -932,9 +946,2137 @@ function resolveScimTenantFromToken(token) {
|
|
|
932
946
|
return null;
|
|
933
947
|
}
|
|
934
948
|
|
|
949
|
+
// ../../src/core/database/baseRepository.ts
|
|
950
|
+
init_eventBus();
|
|
951
|
+
|
|
952
|
+
// ../../src/core/events/index.ts
|
|
953
|
+
init_deferredModelEvents();
|
|
954
|
+
init_eventBus();
|
|
955
|
+
function modelEventName(tableName, action) {
|
|
956
|
+
return `${tableName}.${action}`;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// ../../src/core/pagination/index.ts
|
|
960
|
+
function buildPaginationMeta(input) {
|
|
961
|
+
const lastPage = Math.max(1, Math.ceil(input.total / input.perPage));
|
|
962
|
+
return {
|
|
963
|
+
page: input.page,
|
|
964
|
+
per_page: input.perPage,
|
|
965
|
+
total: input.total,
|
|
966
|
+
last_page: lastPage
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// ../../src/core/database/baseRepository.ts
|
|
971
|
+
init_connectionContext();
|
|
972
|
+
init_dialect();
|
|
973
|
+
|
|
974
|
+
// ../../src/core/database/errors.ts
|
|
975
|
+
import {
|
|
976
|
+
BadRequestError,
|
|
977
|
+
ConflictError,
|
|
978
|
+
InternalServerError,
|
|
979
|
+
toHttpError,
|
|
980
|
+
UnprocessableEntityError
|
|
981
|
+
} from "@getstrata/core/errors/http";
|
|
982
|
+
function isPostgresError(error) {
|
|
983
|
+
return typeof error === "object" && error !== null && (("errno" in error) || ("code" in error));
|
|
984
|
+
}
|
|
985
|
+
function getPostgresSqlState(error) {
|
|
986
|
+
if (typeof error.errno === "string" && /^\d{5}$/.test(error.errno)) {
|
|
987
|
+
return error.errno;
|
|
988
|
+
}
|
|
989
|
+
if (typeof error.errno === "number") {
|
|
990
|
+
return String(error.errno).padStart(5, "0");
|
|
991
|
+
}
|
|
992
|
+
if (typeof error.code === "string" && /^\d{5}$/.test(error.code)) {
|
|
993
|
+
return error.code;
|
|
994
|
+
}
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
var MYSQL_ERRNO_MESSAGES = {
|
|
998
|
+
1062: () => new ConflictError("A record with these values already exists."),
|
|
999
|
+
1451: () => new UnprocessableEntityError("Record is still referenced by other records."),
|
|
1000
|
+
1452: () => new UnprocessableEntityError("Referenced record does not exist."),
|
|
1001
|
+
1048: () => new BadRequestError("Required field is missing."),
|
|
1002
|
+
3819: () => new BadRequestError("Value violates a database constraint.")
|
|
1003
|
+
};
|
|
1004
|
+
function mapSqliteError(error) {
|
|
1005
|
+
const code = typeof error.code === "string" ? error.code : "";
|
|
1006
|
+
if (!code.startsWith("SQLITE_")) {
|
|
1007
|
+
return null;
|
|
1008
|
+
}
|
|
1009
|
+
if (code === "SQLITE_CONSTRAINT_UNIQUE" || code === "SQLITE_CONSTRAINT_PRIMARYKEY") {
|
|
1010
|
+
return new ConflictError("A record with these values already exists.");
|
|
1011
|
+
}
|
|
1012
|
+
if (code === "SQLITE_CONSTRAINT_FOREIGNKEY") {
|
|
1013
|
+
return new UnprocessableEntityError("Operation conflicts with related records.");
|
|
1014
|
+
}
|
|
1015
|
+
if (code === "SQLITE_CONSTRAINT_NOTNULL") {
|
|
1016
|
+
return new BadRequestError("Required field is missing.");
|
|
1017
|
+
}
|
|
1018
|
+
if (code.startsWith("SQLITE_CONSTRAINT")) {
|
|
1019
|
+
return new BadRequestError("Value violates a database constraint.");
|
|
1020
|
+
}
|
|
1021
|
+
return new InternalServerError("Database operation failed.");
|
|
1022
|
+
}
|
|
1023
|
+
function mapMysqlError(error) {
|
|
1024
|
+
const code = typeof error.code === "string" ? error.code : "";
|
|
1025
|
+
if (!code.startsWith("ER_")) {
|
|
1026
|
+
return null;
|
|
1027
|
+
}
|
|
1028
|
+
const factory = typeof error.errno === "number" ? MYSQL_ERRNO_MESSAGES[error.errno] : undefined;
|
|
1029
|
+
return factory ? factory() : new InternalServerError("Database operation failed.");
|
|
1030
|
+
}
|
|
1031
|
+
function mapDatabaseError(error) {
|
|
1032
|
+
const httpError = toHttpError(error);
|
|
1033
|
+
if (httpError) {
|
|
1034
|
+
return httpError;
|
|
1035
|
+
}
|
|
1036
|
+
if (!isPostgresError(error)) {
|
|
1037
|
+
return new InternalServerError;
|
|
1038
|
+
}
|
|
1039
|
+
const sqlite = mapSqliteError(error);
|
|
1040
|
+
if (sqlite) {
|
|
1041
|
+
return sqlite;
|
|
1042
|
+
}
|
|
1043
|
+
const mysql = mapMysqlError(error);
|
|
1044
|
+
if (mysql) {
|
|
1045
|
+
return mysql;
|
|
1046
|
+
}
|
|
1047
|
+
const sqlState = getPostgresSqlState(error);
|
|
1048
|
+
switch (sqlState) {
|
|
1049
|
+
case "23505":
|
|
1050
|
+
if (error.detail) {
|
|
1051
|
+
console.error("[database] unique violation", error.detail);
|
|
1052
|
+
}
|
|
1053
|
+
return new ConflictError("A record with these values already exists.");
|
|
1054
|
+
case "23503":
|
|
1055
|
+
if (error.detail) {
|
|
1056
|
+
console.error("[database] foreign-key violation", error.detail);
|
|
1057
|
+
}
|
|
1058
|
+
return new UnprocessableEntityError("Operation conflicts with related records.");
|
|
1059
|
+
case "23502":
|
|
1060
|
+
if (error.detail) {
|
|
1061
|
+
console.error("[database] not-null violation", error.detail);
|
|
1062
|
+
}
|
|
1063
|
+
return new BadRequestError("Required field is missing.");
|
|
1064
|
+
case "23514":
|
|
1065
|
+
if (error.detail) {
|
|
1066
|
+
console.error("[database] check violation", error.detail);
|
|
1067
|
+
}
|
|
1068
|
+
return new BadRequestError("Value violates a database constraint.");
|
|
1069
|
+
default:
|
|
1070
|
+
return new InternalServerError("Database operation failed.");
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
async function withDatabaseErrorHandling(operation) {
|
|
1074
|
+
try {
|
|
1075
|
+
return await operation();
|
|
1076
|
+
} catch (error) {
|
|
1077
|
+
throw mapDatabaseError(error);
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
function isUniqueConstraintError(error) {
|
|
1081
|
+
if (!isPostgresError(error)) {
|
|
1082
|
+
return false;
|
|
1083
|
+
}
|
|
1084
|
+
if (error.code === "SQLITE_CONSTRAINT_UNIQUE" || error.code === "SQLITE_CONSTRAINT_PRIMARYKEY") {
|
|
1085
|
+
return true;
|
|
1086
|
+
}
|
|
1087
|
+
if (error.errno === 1062) {
|
|
1088
|
+
return true;
|
|
1089
|
+
}
|
|
1090
|
+
return getPostgresSqlState(error) === "23505";
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
// ../../src/core/database/keyset.ts
|
|
1094
|
+
init_dialect();
|
|
1095
|
+
var KEYSET_ALIAS_PREFIX = "__strata_keyset_";
|
|
1096
|
+
function validateKeyset(table, options) {
|
|
1097
|
+
if (!Number.isInteger(options.perPage) || options.perPage < 1 || options.perPage > 1000)
|
|
1098
|
+
throw new RangeError("Keyset page size must be between 1 and 1000.");
|
|
1099
|
+
if (!Array.isArray(options.orderBy) || options.orderBy.length < 1 || options.orderBy.length > 8)
|
|
1100
|
+
throw new TypeError("Keyset ordering requires between 1 and 8 columns.");
|
|
1101
|
+
if (table.columns.some((column) => column.toLowerCase().startsWith(KEYSET_ALIAS_PREFIX)))
|
|
1102
|
+
throw new TypeError("Table columns conflict with reserved keyset aliases.");
|
|
1103
|
+
const seen = new Set;
|
|
1104
|
+
const order = Array.from(options.orderBy).map(({ column, direction }) => {
|
|
1105
|
+
if (!table.columns.includes(column) || seen.has(column) || !["asc", "desc"].includes(direction))
|
|
1106
|
+
throw new TypeError("Invalid or duplicate keyset ordering column/direction.");
|
|
1107
|
+
seen.add(column);
|
|
1108
|
+
return { column, direction };
|
|
1109
|
+
});
|
|
1110
|
+
if (order.at(-1)?.column !== table.primaryKey)
|
|
1111
|
+
throw new TypeError("Keyset ordering must end with the table primary key.");
|
|
1112
|
+
if (options.cursor !== undefined) {
|
|
1113
|
+
const cursor = options.cursor;
|
|
1114
|
+
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))
|
|
1115
|
+
throw new TypeError("Invalid keyset cursor or ordering mismatch.");
|
|
1116
|
+
}
|
|
1117
|
+
return order;
|
|
1118
|
+
}
|
|
1119
|
+
function keysetBoundary(order, cursor) {
|
|
1120
|
+
if (!cursor)
|
|
1121
|
+
return [];
|
|
1122
|
+
if (currentSqlDialect().driver === "pgsql" && order.length > 1 && order.every((item) => item.direction === order[0]?.direction)) {
|
|
1123
|
+
return [
|
|
1124
|
+
{
|
|
1125
|
+
kind: "and",
|
|
1126
|
+
compareRow: {
|
|
1127
|
+
columns: order.map((item) => item.column),
|
|
1128
|
+
operator: order[0]?.direction === "asc" ? "gt" : "lt",
|
|
1129
|
+
values: [...cursor.values]
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
];
|
|
1133
|
+
}
|
|
1134
|
+
const branches = order.map((item, i) => {
|
|
1135
|
+
const where = {};
|
|
1136
|
+
order.slice(0, i).forEach((prefix, j) => {
|
|
1137
|
+
where[prefix.column] = cursor.values[j];
|
|
1138
|
+
});
|
|
1139
|
+
where[item.column] = { [item.direction === "asc" ? "gt" : "lt"]: cursor.values[i] };
|
|
1140
|
+
return { kind: i === 0 ? "and" : "or", where };
|
|
1141
|
+
});
|
|
1142
|
+
return [{ kind: "and", group: branches }];
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
// ../../src/core/database/query.ts
|
|
1146
|
+
import { currentSqlDialect as currentSqlDialect2 } from "@getstrata/core/database/dialect";
|
|
1147
|
+
function quoteIdentifier(identifier) {
|
|
1148
|
+
return currentSqlDialect2().quoteIdentifier(identifier);
|
|
1149
|
+
}
|
|
1150
|
+
function returningSuffix(columns) {
|
|
1151
|
+
return currentSqlDialect2().returningClause(columns);
|
|
1152
|
+
}
|
|
1153
|
+
function qualifyColumn(tableName, column) {
|
|
1154
|
+
return `${quoteIdentifier(tableName)}.${quoteIdentifier(column)}`;
|
|
1155
|
+
}
|
|
1156
|
+
function resolveQualifiedColumn(defaultTable, columnName) {
|
|
1157
|
+
if (columnName.includes(".")) {
|
|
1158
|
+
const [table, column] = columnName.split(".", 2);
|
|
1159
|
+
if (!table || !column) {
|
|
1160
|
+
throw new Error(`Invalid qualified column: ${columnName}`);
|
|
1161
|
+
}
|
|
1162
|
+
return qualifyColumn(table, column);
|
|
1163
|
+
}
|
|
1164
|
+
return qualifyColumn(defaultTable, columnName);
|
|
1165
|
+
}
|
|
1166
|
+
function parseQualifiedColumn(reference) {
|
|
1167
|
+
const [table, column] = reference.split(".", 2);
|
|
1168
|
+
if (!table || !column) {
|
|
1169
|
+
throw new Error(`Join columns must be qualified as table.column: ${reference}`);
|
|
1170
|
+
}
|
|
1171
|
+
return { table, column };
|
|
1172
|
+
}
|
|
1173
|
+
function normalizeDirection(direction = "ASC") {
|
|
1174
|
+
return direction.toUpperCase() === "DESC" ? "DESC" : "ASC";
|
|
1175
|
+
}
|
|
1176
|
+
function isQueryOperator(value) {
|
|
1177
|
+
return value !== null && !Array.isArray(value) && !(value instanceof Date) && typeof value === "object";
|
|
1178
|
+
}
|
|
1179
|
+
function pushParam(values, value) {
|
|
1180
|
+
values.push(value);
|
|
1181
|
+
return currentSqlDialect2().placeholder(values.length);
|
|
1182
|
+
}
|
|
1183
|
+
var SUPPORTED_OPERATORS = new Set([
|
|
1184
|
+
"eq",
|
|
1185
|
+
"ne",
|
|
1186
|
+
"in",
|
|
1187
|
+
"notIn",
|
|
1188
|
+
"gt",
|
|
1189
|
+
"gte",
|
|
1190
|
+
"lt",
|
|
1191
|
+
"lte",
|
|
1192
|
+
"isNull",
|
|
1193
|
+
"ilike",
|
|
1194
|
+
"tsMatch"
|
|
1195
|
+
]);
|
|
1196
|
+
function buildInClause(column, values, params, negated = false) {
|
|
1197
|
+
if (values.length === 0) {
|
|
1198
|
+
return negated ? "1 = 1" : "1 = 0";
|
|
1199
|
+
}
|
|
1200
|
+
const placeholders = values.map((value) => pushParam(params, value)).join(", ");
|
|
1201
|
+
return `${column}${negated ? " NOT" : ""} IN (${placeholders})`;
|
|
1202
|
+
}
|
|
1203
|
+
function buildOperatorClauses(column, operator, params) {
|
|
1204
|
+
const clauses = [];
|
|
1205
|
+
const unsupported = Object.keys(operator).filter((key) => !SUPPORTED_OPERATORS.has(key));
|
|
1206
|
+
if (unsupported.length > 0) {
|
|
1207
|
+
throw new Error(`Unsupported query operator(s) for ${column}: ${unsupported.join(", ")}. Supported: ${[...SUPPORTED_OPERATORS].join(", ")}.`);
|
|
1208
|
+
}
|
|
1209
|
+
if (operator.isNull === true) {
|
|
1210
|
+
clauses.push(`${column} IS NULL`);
|
|
1211
|
+
}
|
|
1212
|
+
if (operator.isNull === false) {
|
|
1213
|
+
clauses.push(`${column} IS NOT NULL`);
|
|
1214
|
+
}
|
|
1215
|
+
if (operator.eq !== undefined) {
|
|
1216
|
+
if (operator.eq === null) {
|
|
1217
|
+
clauses.push(`${column} IS NULL`);
|
|
1218
|
+
} else {
|
|
1219
|
+
clauses.push(`${column} = ${pushParam(params, operator.eq)}`);
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
if (operator.ne !== undefined) {
|
|
1223
|
+
if (operator.ne === null) {
|
|
1224
|
+
clauses.push(`${column} IS NOT NULL`);
|
|
1225
|
+
} else {
|
|
1226
|
+
clauses.push(`${column} <> ${pushParam(params, operator.ne)}`);
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
if (operator.in !== undefined) {
|
|
1230
|
+
clauses.push(buildInClause(column, operator.in, params));
|
|
1231
|
+
}
|
|
1232
|
+
if (operator.notIn !== undefined) {
|
|
1233
|
+
clauses.push(buildInClause(column, operator.notIn, params, true));
|
|
1234
|
+
}
|
|
1235
|
+
if (operator.gt !== undefined) {
|
|
1236
|
+
clauses.push(`${column} > ${pushParam(params, operator.gt)}`);
|
|
1237
|
+
}
|
|
1238
|
+
if (operator.gte !== undefined) {
|
|
1239
|
+
clauses.push(`${column} >= ${pushParam(params, operator.gte)}`);
|
|
1240
|
+
}
|
|
1241
|
+
if (operator.lt !== undefined) {
|
|
1242
|
+
clauses.push(`${column} < ${pushParam(params, operator.lt)}`);
|
|
1243
|
+
}
|
|
1244
|
+
if (operator.lte !== undefined) {
|
|
1245
|
+
clauses.push(`${column} <= ${pushParam(params, operator.lte)}`);
|
|
1246
|
+
}
|
|
1247
|
+
if (operator.ilike !== undefined) {
|
|
1248
|
+
clauses.push(`${column} ${currentSqlDialect2().ilikeOperator()} ${pushParam(params, operator.ilike)}`);
|
|
1249
|
+
}
|
|
1250
|
+
if (operator.tsMatch !== undefined) {
|
|
1251
|
+
if (currentSqlDialect2().driver !== "pgsql") {
|
|
1252
|
+
throw new Error("Full-text search (tsMatch) is only available on PostgreSQL.");
|
|
1253
|
+
}
|
|
1254
|
+
clauses.push(`${column} @@ plainto_tsquery('english', ${pushParam(params, operator.tsMatch)})`);
|
|
1255
|
+
}
|
|
1256
|
+
return clauses;
|
|
1257
|
+
}
|
|
1258
|
+
function appendWhereParts(tableName, where, params) {
|
|
1259
|
+
const clauses = [];
|
|
1260
|
+
for (const [columnName, filterValue] of Object.entries(where)) {
|
|
1261
|
+
if (filterValue === undefined) {
|
|
1262
|
+
continue;
|
|
1263
|
+
}
|
|
1264
|
+
const column = resolveQualifiedColumn(tableName, columnName);
|
|
1265
|
+
if (Array.isArray(filterValue)) {
|
|
1266
|
+
clauses.push(buildInClause(column, filterValue, params));
|
|
1267
|
+
continue;
|
|
1268
|
+
}
|
|
1269
|
+
if (isQueryOperator(filterValue)) {
|
|
1270
|
+
clauses.push(...buildOperatorClauses(column, filterValue, params));
|
|
1271
|
+
continue;
|
|
1272
|
+
}
|
|
1273
|
+
if (filterValue === null) {
|
|
1274
|
+
clauses.push(`${column} IS NULL`);
|
|
1275
|
+
continue;
|
|
1276
|
+
}
|
|
1277
|
+
clauses.push(`${column} = ${pushParam(params, filterValue)}`);
|
|
1278
|
+
}
|
|
1279
|
+
return clauses.join(" AND ");
|
|
1280
|
+
}
|
|
1281
|
+
function buildWhereClause(tableName, where = {}) {
|
|
1282
|
+
const params = [];
|
|
1283
|
+
const body = appendWhereParts(tableName, where, params);
|
|
1284
|
+
return {
|
|
1285
|
+
clause: body.length > 0 ? ` WHERE ${body}` : "",
|
|
1286
|
+
params
|
|
1287
|
+
};
|
|
1288
|
+
}
|
|
1289
|
+
function remapExistsSql(sql, existsParams, params) {
|
|
1290
|
+
const offset = params.length;
|
|
1291
|
+
params.push(...existsParams);
|
|
1292
|
+
return sql.replace(/\$(\d+)/g, (_match, index) => `$${offset + Number(index)}`);
|
|
1293
|
+
}
|
|
1294
|
+
function buildWhereNodeClause(tableName, node, params) {
|
|
1295
|
+
if ("compareRow" in node) {
|
|
1296
|
+
const { columns, operator, values } = node.compareRow;
|
|
1297
|
+
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))
|
|
1298
|
+
throw new TypeError("Invalid row comparison columns, values or operator.");
|
|
1299
|
+
return `(${columns.map((column) => qualifyColumn(tableName, column)).join(", ")}) ${operator === "lt" ? "<" : ">"} (${values.map((value) => pushParam(params, value)).join(", ")})`;
|
|
1300
|
+
}
|
|
1301
|
+
if ("exists" in node) {
|
|
1302
|
+
const body = remapExistsSql(node.exists.sql, node.exists.params, params);
|
|
1303
|
+
return `${node.exists.not ? "NOT " : ""}EXISTS (${body})`;
|
|
1304
|
+
}
|
|
1305
|
+
if ("where" in node) {
|
|
1306
|
+
return appendWhereParts(tableName, node.where, params);
|
|
1307
|
+
}
|
|
1308
|
+
const grouped = buildWhereGroupClause(tableName, node.group, params);
|
|
1309
|
+
if (!grouped) {
|
|
1310
|
+
return "";
|
|
1311
|
+
}
|
|
1312
|
+
return grouped.includes(" OR ") ? `(${grouped})` : grouped;
|
|
1313
|
+
}
|
|
1314
|
+
function buildWhereGroupClause(tableName, nodes, params) {
|
|
1315
|
+
let result = "";
|
|
1316
|
+
for (const node of nodes) {
|
|
1317
|
+
const part = buildWhereNodeClause(tableName, node, params);
|
|
1318
|
+
if (!part) {
|
|
1319
|
+
continue;
|
|
1320
|
+
}
|
|
1321
|
+
if (!result) {
|
|
1322
|
+
result = part;
|
|
1323
|
+
continue;
|
|
1324
|
+
}
|
|
1325
|
+
result = node.kind === "or" ? `${result} OR ${part}` : `${result} AND ${part}`;
|
|
1326
|
+
}
|
|
1327
|
+
if (!result) {
|
|
1328
|
+
return "";
|
|
1329
|
+
}
|
|
1330
|
+
return result;
|
|
1331
|
+
}
|
|
1332
|
+
function buildAdvancedWhereClause(tableName, where = {}, whereNodes = [], params = []) {
|
|
1333
|
+
const nodes = [];
|
|
1334
|
+
if (Object.keys(where).length > 0) {
|
|
1335
|
+
nodes.push({ kind: "and", where });
|
|
1336
|
+
}
|
|
1337
|
+
nodes.push(...whereNodes);
|
|
1338
|
+
const combined = buildWhereGroupClause(tableName, nodes, params);
|
|
1339
|
+
return {
|
|
1340
|
+
clause: combined ? ` WHERE ${combined}` : "",
|
|
1341
|
+
params
|
|
1342
|
+
};
|
|
1343
|
+
}
|
|
1344
|
+
function resolveSoftDeleteColumn(table) {
|
|
1345
|
+
if (!table.softDeletes) {
|
|
1346
|
+
return null;
|
|
1347
|
+
}
|
|
1348
|
+
if (table.softDeletes === true) {
|
|
1349
|
+
return "deleted_at";
|
|
1350
|
+
}
|
|
1351
|
+
return table.softDeletes.column ?? "deleted_at";
|
|
1352
|
+
}
|
|
1353
|
+
function appendSoftDeleteScope(table, options, clauses) {
|
|
1354
|
+
const column = resolveSoftDeleteColumn(table);
|
|
1355
|
+
if (!column) {
|
|
1356
|
+
return;
|
|
1357
|
+
}
|
|
1358
|
+
const qualifiedColumn = qualifyColumn(table.name, column);
|
|
1359
|
+
if (options.onlyTrashed) {
|
|
1360
|
+
clauses.push(`${qualifiedColumn} IS NOT NULL`);
|
|
1361
|
+
return;
|
|
1362
|
+
}
|
|
1363
|
+
if (!options.withTrashed) {
|
|
1364
|
+
clauses.push(`${qualifiedColumn} IS NULL`);
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
function buildQueryWhereClause(table, options = {}, whereNodes = [], params = []) {
|
|
1368
|
+
const { clause, params: whereParams } = buildAdvancedWhereClause(table.name, options.where ?? {}, whereNodes, params);
|
|
1369
|
+
const softDeleteClauses = [];
|
|
1370
|
+
appendSoftDeleteScope(table, options, softDeleteClauses);
|
|
1371
|
+
if (softDeleteClauses.length === 0) {
|
|
1372
|
+
return { clause, params: whereParams };
|
|
1373
|
+
}
|
|
1374
|
+
const base = clause.replace(/^ WHERE /, "");
|
|
1375
|
+
const scope = softDeleteClauses.join(" AND ");
|
|
1376
|
+
return {
|
|
1377
|
+
clause: base ? ` WHERE (${base}) AND ${scope}` : ` WHERE ${scope}`,
|
|
1378
|
+
params: whereParams
|
|
1379
|
+
};
|
|
1380
|
+
}
|
|
1381
|
+
function isQueryOrder(value) {
|
|
1382
|
+
return "column" in value;
|
|
1383
|
+
}
|
|
1384
|
+
function normalizeOrderBy(orderBy) {
|
|
1385
|
+
if (!orderBy) {
|
|
1386
|
+
return [];
|
|
1387
|
+
}
|
|
1388
|
+
if (Array.isArray(orderBy)) {
|
|
1389
|
+
return orderBy;
|
|
1390
|
+
}
|
|
1391
|
+
if (isQueryOrder(orderBy)) {
|
|
1392
|
+
return [orderBy];
|
|
1393
|
+
}
|
|
1394
|
+
return Object.entries(orderBy).map(([column, direction]) => ({
|
|
1395
|
+
column,
|
|
1396
|
+
direction
|
|
1397
|
+
}));
|
|
1398
|
+
}
|
|
1399
|
+
function buildOrderByClause(tableName, orderBy) {
|
|
1400
|
+
const parts = normalizeOrderBy(orderBy).map(({ column, direction, nulls }) => {
|
|
1401
|
+
if (nulls !== undefined && nulls !== "first" && nulls !== "last") {
|
|
1402
|
+
throw new TypeError("Invalid null ordering: expected first or last.");
|
|
1403
|
+
}
|
|
1404
|
+
const reference = resolveQualifiedColumn(tableName, column);
|
|
1405
|
+
const ordered = `${reference} ${normalizeDirection(direction)}`;
|
|
1406
|
+
if (nulls === undefined)
|
|
1407
|
+
return ordered;
|
|
1408
|
+
if (currentSqlDialect2().driver === "mysql") {
|
|
1409
|
+
return `(${reference} IS NULL) ${nulls === "first" ? "DESC" : "ASC"}, ${ordered}`;
|
|
1410
|
+
}
|
|
1411
|
+
return `${ordered} NULLS ${nulls.toUpperCase()}`;
|
|
1412
|
+
});
|
|
1413
|
+
return parts.length > 0 ? ` ORDER BY ${parts.join(", ")}` : "";
|
|
1414
|
+
}
|
|
1415
|
+
function buildGroupByClause(tableName, groupBy) {
|
|
1416
|
+
if (!groupBy) {
|
|
1417
|
+
return "";
|
|
1418
|
+
}
|
|
1419
|
+
const columns = (Array.isArray(groupBy) ? groupBy : [groupBy]).map((column) => String(column));
|
|
1420
|
+
const parts = columns.map((column) => resolveQualifiedColumn(tableName, column));
|
|
1421
|
+
return parts.length > 0 ? ` GROUP BY ${parts.join(", ")}` : "";
|
|
1422
|
+
}
|
|
1423
|
+
function buildHavingClause(tableName, having, params) {
|
|
1424
|
+
if (!having) {
|
|
1425
|
+
return "";
|
|
1426
|
+
}
|
|
1427
|
+
const body = appendWhereParts(tableName, having, params);
|
|
1428
|
+
return body.length > 0 ? ` HAVING ${body}` : "";
|
|
1429
|
+
}
|
|
1430
|
+
function buildJoinClause(joins = []) {
|
|
1431
|
+
return joins.map((join) => {
|
|
1432
|
+
const joinType = join.type === "left" ? "LEFT JOIN" : "INNER JOIN";
|
|
1433
|
+
const onClause = join.on.map(({ left, right }) => `${qualifyColumn(left.table, left.column)} = ${qualifyColumn(right.table, right.column)}`).join(" AND ");
|
|
1434
|
+
return ` ${joinType} ${quoteIdentifier(join.table)} ON ${onClause}`;
|
|
1435
|
+
}).join("");
|
|
1436
|
+
}
|
|
1437
|
+
function buildLimitClause(limit) {
|
|
1438
|
+
if (limit === undefined) {
|
|
1439
|
+
return "";
|
|
1440
|
+
}
|
|
1441
|
+
if (!Number.isInteger(limit) || limit <= 0) {
|
|
1442
|
+
throw new Error("Query limit must be a positive integer.");
|
|
1443
|
+
}
|
|
1444
|
+
return ` LIMIT ${limit}`;
|
|
1445
|
+
}
|
|
1446
|
+
function buildOffsetClause(offset) {
|
|
1447
|
+
if (offset === undefined) {
|
|
1448
|
+
return "";
|
|
1449
|
+
}
|
|
1450
|
+
if (!Number.isInteger(offset) || offset < 0) {
|
|
1451
|
+
throw new Error("Query offset must be a non-negative integer.");
|
|
1452
|
+
}
|
|
1453
|
+
return ` OFFSET ${offset}`;
|
|
1454
|
+
}
|
|
1455
|
+
function buildReturningColumns(table) {
|
|
1456
|
+
return table.columns.map((column) => qualifyColumn(table.name, column)).join(", ");
|
|
1457
|
+
}
|
|
1458
|
+
function buildSelectList(table, select, params = []) {
|
|
1459
|
+
if (!select || select.length === 0) {
|
|
1460
|
+
return buildReturningColumns(table);
|
|
1461
|
+
}
|
|
1462
|
+
return select.map((item) => {
|
|
1463
|
+
if (item.kind === "column") {
|
|
1464
|
+
const column = qualifyColumn(item.table, item.column);
|
|
1465
|
+
return item.as ? `${column} AS ${quoteIdentifier(item.as)}` : column;
|
|
1466
|
+
}
|
|
1467
|
+
if (item.kind === "textColumn") {
|
|
1468
|
+
const dialect = currentSqlDialect2();
|
|
1469
|
+
const column = qualifyColumn(item.table, item.column);
|
|
1470
|
+
const text = dialect.driver === "sqlite" ? `CASE WHEN typeof(${column}) = 'real' THEN printf('%!.26g', ${column}) ELSE ${dialect.castToText(column)} END` : dialect.castToText(column);
|
|
1471
|
+
return `${text} AS ${quoteIdentifier(item.as)}`;
|
|
1472
|
+
}
|
|
1473
|
+
if (item.kind === "literalText") {
|
|
1474
|
+
return `${currentSqlDialect2().castToText(pushParam(params, item.value))} AS ${quoteIdentifier(item.as)}`;
|
|
1475
|
+
}
|
|
1476
|
+
if (item.kind === "subqueryCount") {
|
|
1477
|
+
const body = remapExistsSql(item.sql, item.params, params);
|
|
1478
|
+
return `(${body}) AS ${quoteIdentifier(item.as)}`;
|
|
1479
|
+
}
|
|
1480
|
+
const column = qualifyColumn(item.table, item.column);
|
|
1481
|
+
const placeholder = pushParam(params, item.query);
|
|
1482
|
+
return `ts_rank(${column}, plainto_tsquery('english', ${placeholder})) AS ${quoteIdentifier(item.as)}`;
|
|
1483
|
+
}).join(", ");
|
|
1484
|
+
}
|
|
1485
|
+
function getDefinedColumnEntries(table, values, options = {}) {
|
|
1486
|
+
const record = values;
|
|
1487
|
+
const excluded = new Set(options.exclude ?? []);
|
|
1488
|
+
return table.columns.flatMap((column) => {
|
|
1489
|
+
if (excluded.has(column) || !Object.hasOwn(record, column)) {
|
|
1490
|
+
return [];
|
|
1491
|
+
}
|
|
1492
|
+
const value = record[column];
|
|
1493
|
+
if (value === undefined) {
|
|
1494
|
+
return [];
|
|
1495
|
+
}
|
|
1496
|
+
return [[column, value]];
|
|
1497
|
+
});
|
|
1498
|
+
}
|
|
1499
|
+
function buildSelectLock(table, options) {
|
|
1500
|
+
const lock = options.lock;
|
|
1501
|
+
if (!lock)
|
|
1502
|
+
return "";
|
|
1503
|
+
const driver = currentSqlDialect2().driver;
|
|
1504
|
+
if (driver === "sqlite")
|
|
1505
|
+
throw new Error("Row locks are not supported on SQLite; use an explicit write transaction.");
|
|
1506
|
+
if (lock.mode !== "update" && lock.mode !== "share")
|
|
1507
|
+
throw new TypeError("Invalid row lock mode.");
|
|
1508
|
+
if (lock.wait !== undefined && !["wait", "nowait", "skipLocked"].includes(lock.wait))
|
|
1509
|
+
throw new TypeError("Invalid row lock wait policy.");
|
|
1510
|
+
if (options.groupBy || options.having)
|
|
1511
|
+
throw new Error("Row locks cannot be combined with grouped/aggregate queries.");
|
|
1512
|
+
let target = "";
|
|
1513
|
+
if (lock.of !== undefined) {
|
|
1514
|
+
if (driver !== "pgsql")
|
|
1515
|
+
throw new Error("Row lock targets are supported only on PostgreSQL.");
|
|
1516
|
+
const tables = [table.name, ...(options.joins ?? []).map((join) => join.table)];
|
|
1517
|
+
if (!lock.of.length || lock.of.some((name) => !tables.includes(name)))
|
|
1518
|
+
throw new TypeError("Row lock targets must name tables in this query.");
|
|
1519
|
+
target = ` OF ${lock.of.map(quoteIdentifier).join(", ")}`;
|
|
1520
|
+
}
|
|
1521
|
+
const wait = lock.wait === "nowait" ? " NOWAIT" : lock.wait === "skipLocked" ? " SKIP LOCKED" : "";
|
|
1522
|
+
return ` FOR ${lock.mode === "update" ? "UPDATE" : "SHARE"}${target}${wait}`;
|
|
1523
|
+
}
|
|
1524
|
+
function buildSelectQuery(table, options = {}, whereNodes = []) {
|
|
1525
|
+
const params = [];
|
|
1526
|
+
const columns = buildSelectList(table, options.select, params);
|
|
1527
|
+
const { clause } = buildQueryWhereClause(table, options, whereNodes, params);
|
|
1528
|
+
const joins = buildJoinClause(options.joins);
|
|
1529
|
+
const groupBy = buildGroupByClause(table.name, options.groupBy);
|
|
1530
|
+
const havingClause = buildHavingClause(table.name, options.having, params);
|
|
1531
|
+
const orderBy = buildOrderByClause(table.name, options.orderBy ?? table.defaultOrderBy);
|
|
1532
|
+
const limit = buildLimitClause(options.limit);
|
|
1533
|
+
const offset = buildOffsetClause(options.offset);
|
|
1534
|
+
const lock = buildSelectLock(table, options);
|
|
1535
|
+
return {
|
|
1536
|
+
text: `SELECT ${columns} FROM ${quoteIdentifier(table.name)}${joins}${clause}${groupBy}${havingClause}${orderBy}${limit}${offset}${lock}`,
|
|
1537
|
+
params
|
|
1538
|
+
};
|
|
1539
|
+
}
|
|
1540
|
+
function buildCountQuery(table, where = {}, options = {}, whereNodes = []) {
|
|
1541
|
+
const params = [];
|
|
1542
|
+
const { clause, params: whereParams } = buildQueryWhereClause(table, {
|
|
1543
|
+
where,
|
|
1544
|
+
withTrashed: options.withTrashed,
|
|
1545
|
+
onlyTrashed: options.onlyTrashed
|
|
1546
|
+
}, whereNodes);
|
|
1547
|
+
params.push(...whereParams);
|
|
1548
|
+
const joins = buildJoinClause(options.joins);
|
|
1549
|
+
const groupBy = buildGroupByClause(table.name, options.groupBy);
|
|
1550
|
+
return {
|
|
1551
|
+
text: `SELECT COUNT(*) AS count FROM ${quoteIdentifier(table.name)}${joins}${clause}${groupBy}`,
|
|
1552
|
+
params
|
|
1553
|
+
};
|
|
1554
|
+
}
|
|
1555
|
+
function buildProjectionQuery(table, expression, alias, options = {}, whereNodes = []) {
|
|
1556
|
+
assertSafeProjectionExpression(expression);
|
|
1557
|
+
const params = [];
|
|
1558
|
+
const { clause, params: whereParams } = buildQueryWhereClause(table, options, whereNodes);
|
|
1559
|
+
params.push(...whereParams);
|
|
1560
|
+
const joins = buildJoinClause(options.joins);
|
|
1561
|
+
const groupBy = buildGroupByClause(table.name, options.groupBy);
|
|
1562
|
+
const orderBy = buildOrderByClause(table.name, options.orderBy);
|
|
1563
|
+
const limit = buildLimitClause(options.limit);
|
|
1564
|
+
return {
|
|
1565
|
+
text: `SELECT ${expression} AS ${quoteIdentifier(alias)} FROM ${quoteIdentifier(table.name)}${joins}${clause}${groupBy}${orderBy}${limit}`,
|
|
1566
|
+
params
|
|
1567
|
+
};
|
|
1568
|
+
}
|
|
1569
|
+
var SAFE_PROJECTION_EXPRESSION = /^(COUNT\(\*\)|(?:"[\w_]+"(?:\."[\w_]+")?)|(?:AVG|SUM|MIN|MAX)\((?:"[\w_]+"(?:\."[\w_]+")?)\))$/;
|
|
1570
|
+
function assertSafeProjectionExpression(expression) {
|
|
1571
|
+
if (!SAFE_PROJECTION_EXPRESSION.test(expression.trim())) {
|
|
1572
|
+
throw new Error(`Unsafe projection expression: ${expression}`);
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
function buildGroupedSumQuery(table, groupColumn, column, options = {}, whereNodes = options.whereNodes ?? []) {
|
|
1576
|
+
if (!table.columns.includes(groupColumn) || !table.columns.includes(column))
|
|
1577
|
+
throw new TypeError("Grouped sums require declared columns.");
|
|
1578
|
+
for (const key of Object.keys(options))
|
|
1579
|
+
if (!["where", "whereNodes", "withTrashed", "onlyTrashed"].includes(key))
|
|
1580
|
+
throw new TypeError("Grouped sums do not support joins, selection, ordering, pagination, grouping or locks.");
|
|
1581
|
+
const group = qualifyColumn(table.name, groupColumn);
|
|
1582
|
+
const total = currentSqlDialect2().castToText(`SUM(${qualifyColumn(table.name, column)})`);
|
|
1583
|
+
const { clause, params } = buildQueryWhereClause(table, options, whereNodes);
|
|
1584
|
+
return {
|
|
1585
|
+
text: `SELECT ${group} AS ${quoteIdentifier("value")}, ${total} AS ${quoteIdentifier("sum")} FROM ${quoteIdentifier(table.name)}${clause} GROUP BY ${group} ORDER BY ${group} ASC`,
|
|
1586
|
+
params
|
|
1587
|
+
};
|
|
1588
|
+
}
|
|
1589
|
+
function buildGroupedCountQuery(table, column, where = {}, options = {}) {
|
|
1590
|
+
const qualifiedColumn = qualifyColumn(table.name, column);
|
|
1591
|
+
const { clause, params } = buildQueryWhereClause(table, {
|
|
1592
|
+
where,
|
|
1593
|
+
...options
|
|
1594
|
+
});
|
|
1595
|
+
return {
|
|
1596
|
+
text: `SELECT ${qualifiedColumn} AS ${quoteIdentifier("value")}, COUNT(*) AS ${quoteIdentifier("count")} FROM ${quoteIdentifier(table.name)}${clause} GROUP BY ${qualifiedColumn} ORDER BY ${qualifiedColumn} ASC`,
|
|
1597
|
+
params
|
|
1598
|
+
};
|
|
1599
|
+
}
|
|
1600
|
+
function buildInsertQuery(table, values) {
|
|
1601
|
+
const entries = getDefinedColumnEntries(table, values);
|
|
1602
|
+
if (entries.length === 0) {
|
|
1603
|
+
throw new Error(`Cannot insert into ${table.name} without any column values.`);
|
|
1604
|
+
}
|
|
1605
|
+
const params = [];
|
|
1606
|
+
const columns = entries.map(([column]) => quoteIdentifier(column)).join(", ");
|
|
1607
|
+
const placeholders = entries.map(([, value]) => pushParam(params, value)).join(", ");
|
|
1608
|
+
const returningColumns = buildReturningColumns(table);
|
|
1609
|
+
return {
|
|
1610
|
+
text: `INSERT INTO ${quoteIdentifier(table.name)} (${columns}) VALUES (${placeholders})${returningSuffix(returningColumns)}`,
|
|
1611
|
+
params
|
|
1612
|
+
};
|
|
1613
|
+
}
|
|
1614
|
+
function buildUpsertQuery(table, values, conflictColumns, updateColumns) {
|
|
1615
|
+
const entries = getDefinedColumnEntries(table, values);
|
|
1616
|
+
if (entries.length === 0) {
|
|
1617
|
+
throw new Error(`Cannot upsert into ${table.name} without any column values.`);
|
|
1618
|
+
}
|
|
1619
|
+
if (conflictColumns.length === 0) {
|
|
1620
|
+
throw new Error(`Cannot upsert into ${table.name} without any conflict columns.`);
|
|
1621
|
+
}
|
|
1622
|
+
const insertable = new Set(entries.map(([column]) => column));
|
|
1623
|
+
const conflict = new Set(conflictColumns);
|
|
1624
|
+
const updatable = (updateColumns ?? entries.map(([column]) => column).filter((c) => !conflict.has(c))).filter((column) => insertable.has(column));
|
|
1625
|
+
const params = [];
|
|
1626
|
+
const columns = entries.map(([column]) => quoteIdentifier(column)).join(", ");
|
|
1627
|
+
const placeholders = entries.map(([, value]) => pushParam(params, value)).join(", ");
|
|
1628
|
+
const returningColumns = buildReturningColumns(table);
|
|
1629
|
+
const suffix = currentSqlDialect2().upsertSuffix(conflictColumns, updatable);
|
|
1630
|
+
return {
|
|
1631
|
+
text: `INSERT INTO ${quoteIdentifier(table.name)} (${columns}) VALUES (${placeholders})${suffix}${returningSuffix(returningColumns)}`,
|
|
1632
|
+
params
|
|
1633
|
+
};
|
|
1634
|
+
}
|
|
1635
|
+
function buildIncrementQuery(table, id, column, amount, extra = {}) {
|
|
1636
|
+
if (!Number.isFinite(amount)) {
|
|
1637
|
+
throw new Error("Increment amount must be a finite number.");
|
|
1638
|
+
}
|
|
1639
|
+
if (!table.columns.includes(column)) {
|
|
1640
|
+
throw new Error(`Unknown column ${String(column)} on ${table.name}.`);
|
|
1641
|
+
}
|
|
1642
|
+
const params = [];
|
|
1643
|
+
const target = quoteIdentifier(column);
|
|
1644
|
+
const assignments = [`${target} = ${target} + ${pushParam(params, amount)}`];
|
|
1645
|
+
for (const [name, value] of getDefinedColumnEntries(table, extra, {
|
|
1646
|
+
exclude: [table.primaryKey, column]
|
|
1647
|
+
})) {
|
|
1648
|
+
assignments.push(`${quoteIdentifier(name)} = ${pushParam(params, value)}`);
|
|
1649
|
+
}
|
|
1650
|
+
const primaryKeyPlaceholder = pushParam(params, id);
|
|
1651
|
+
const returningColumns = buildReturningColumns(table);
|
|
1652
|
+
const scopeClauses = [];
|
|
1653
|
+
appendSoftDeleteScope(table, {}, scopeClauses);
|
|
1654
|
+
const scopeSuffix = scopeClauses.length > 0 ? ` AND ${scopeClauses.join(" AND ")}` : "";
|
|
1655
|
+
return {
|
|
1656
|
+
text: `UPDATE ${quoteIdentifier(table.name)} SET ${assignments.join(", ")} WHERE ${quoteIdentifier(table.primaryKey)} = ${primaryKeyPlaceholder}${scopeSuffix}${returningSuffix(returningColumns)}`,
|
|
1657
|
+
params
|
|
1658
|
+
};
|
|
1659
|
+
}
|
|
1660
|
+
function buildUpdateQuery(table, id, changes) {
|
|
1661
|
+
const entries = getDefinedColumnEntries(table, changes, {
|
|
1662
|
+
exclude: [table.primaryKey]
|
|
1663
|
+
});
|
|
1664
|
+
if (entries.length === 0) {
|
|
1665
|
+
throw new Error(`Cannot update ${table.name} without any changed column values.`);
|
|
1666
|
+
}
|
|
1667
|
+
const params = [];
|
|
1668
|
+
const setClause = entries.map(([column, value]) => `${quoteIdentifier(column)} = ${pushParam(params, value)}`).join(", ");
|
|
1669
|
+
const primaryKeyPlaceholder = pushParam(params, id);
|
|
1670
|
+
const returningColumns = buildReturningColumns(table);
|
|
1671
|
+
const scopeClauses = [];
|
|
1672
|
+
appendSoftDeleteScope(table, {}, scopeClauses);
|
|
1673
|
+
const scopeSuffix = scopeClauses.length > 0 ? ` AND ${scopeClauses.join(" AND ")}` : "";
|
|
1674
|
+
return {
|
|
1675
|
+
text: `UPDATE ${quoteIdentifier(table.name)} SET ${setClause} WHERE ${quoteIdentifier(table.primaryKey)} = ${primaryKeyPlaceholder}${scopeSuffix}${returningSuffix(returningColumns)}`,
|
|
1676
|
+
params
|
|
1677
|
+
};
|
|
1678
|
+
}
|
|
1679
|
+
function buildSoftDeleteByIdQuery(table, id, deletedAt) {
|
|
1680
|
+
const deletedAtColumn = resolveSoftDeleteColumn(table);
|
|
1681
|
+
if (!deletedAtColumn) {
|
|
1682
|
+
throw new Error(`Table ${table.name} does not support soft deletes.`);
|
|
1683
|
+
}
|
|
1684
|
+
const returningColumns = buildReturningColumns(table);
|
|
1685
|
+
const scopeClauses = [];
|
|
1686
|
+
appendSoftDeleteScope(table, {}, scopeClauses);
|
|
1687
|
+
const scopeSuffix = scopeClauses.length > 0 ? ` AND ${scopeClauses.join(" AND ")}` : "";
|
|
1688
|
+
const params = [];
|
|
1689
|
+
const deletedAtPlaceholder = pushParam(params, deletedAt);
|
|
1690
|
+
const idPlaceholder = pushParam(params, id);
|
|
1691
|
+
return {
|
|
1692
|
+
text: `UPDATE ${quoteIdentifier(table.name)} SET ${quoteIdentifier(deletedAtColumn)} = ${deletedAtPlaceholder} WHERE ${quoteIdentifier(table.primaryKey)} = ${idPlaceholder}${scopeSuffix}${returningSuffix(returningColumns)}`,
|
|
1693
|
+
params
|
|
1694
|
+
};
|
|
1695
|
+
}
|
|
1696
|
+
function buildRestoreByIdQuery(table, id) {
|
|
1697
|
+
const deletedAtColumn = resolveSoftDeleteColumn(table);
|
|
1698
|
+
if (!deletedAtColumn) {
|
|
1699
|
+
throw new Error(`Table ${table.name} does not support soft deletes.`);
|
|
1700
|
+
}
|
|
1701
|
+
const returningColumns = buildReturningColumns(table);
|
|
1702
|
+
const params = [];
|
|
1703
|
+
const deletedAtPlaceholder = pushParam(params, null);
|
|
1704
|
+
const idPlaceholder = pushParam(params, id);
|
|
1705
|
+
return {
|
|
1706
|
+
text: `UPDATE ${quoteIdentifier(table.name)} SET ${quoteIdentifier(deletedAtColumn)} = ${deletedAtPlaceholder} WHERE ${quoteIdentifier(table.primaryKey)} = ${idPlaceholder} AND ${qualifyColumn(table.name, deletedAtColumn)} IS NOT NULL${returningSuffix(returningColumns)}`,
|
|
1707
|
+
params
|
|
1708
|
+
};
|
|
1709
|
+
}
|
|
1710
|
+
function buildDeleteByIdQuery(table, id) {
|
|
1711
|
+
const params = [];
|
|
1712
|
+
const idPlaceholder = pushParam(params, id);
|
|
1713
|
+
const returning = returningSuffix(`${quoteIdentifier(table.primaryKey)} AS ${quoteIdentifier("deleted_id")}`);
|
|
1714
|
+
return {
|
|
1715
|
+
text: `DELETE FROM ${quoteIdentifier(table.name)} WHERE ${quoteIdentifier(table.primaryKey)} = ${idPlaceholder}${returning}`,
|
|
1716
|
+
params
|
|
1717
|
+
};
|
|
1718
|
+
}
|
|
1719
|
+
function buildConditionalWriteQuery(table, operation, changes, options, returning) {
|
|
1720
|
+
for (const key of [
|
|
1721
|
+
"joins",
|
|
1722
|
+
"groupBy",
|
|
1723
|
+
"having",
|
|
1724
|
+
"select",
|
|
1725
|
+
"orderBy",
|
|
1726
|
+
"limit",
|
|
1727
|
+
"offset",
|
|
1728
|
+
"lock"
|
|
1729
|
+
]) {
|
|
1730
|
+
if (options[key] !== undefined)
|
|
1731
|
+
throw new Error(`Conditional writes do not support ${key}.`);
|
|
1732
|
+
}
|
|
1733
|
+
const params = [];
|
|
1734
|
+
const predicate = buildAdvancedWhereClause(table.name, options.where ?? {}, options.whereNodes ?? []);
|
|
1735
|
+
if (!predicate.clause)
|
|
1736
|
+
throw new Error("Conditional writes require an explicit predicate.");
|
|
1737
|
+
if (returning !== undefined && currentSqlDialect2().driver === "mysql")
|
|
1738
|
+
throw new Error("Mutation RETURNING is not supported on MySQL.");
|
|
1739
|
+
const columns = returning ?? [table.primaryKey];
|
|
1740
|
+
if (!columns.length || columns.some((column) => !table.columns.includes(column)))
|
|
1741
|
+
throw new Error("RETURNING columns must be nonempty declared table columns.");
|
|
1742
|
+
const deletedAt = resolveSoftDeleteColumn(table);
|
|
1743
|
+
const values = operation === "delete" && deletedAt ? { [deletedAt]: new Date } : changes;
|
|
1744
|
+
let prefix;
|
|
1745
|
+
if (operation === "update" || deletedAt) {
|
|
1746
|
+
const entries = Object.entries(values).filter(([, value]) => value !== undefined);
|
|
1747
|
+
if (!entries.length)
|
|
1748
|
+
throw new Error("Conditional updates require at least one value.");
|
|
1749
|
+
if (entries.some(([column]) => column === table.primaryKey || !table.columns.includes(column)))
|
|
1750
|
+
throw new Error("Conditional updates require declared non-primary-key columns.");
|
|
1751
|
+
const assignments = entries.map(([column, value]) => `${quoteIdentifier(column)} = ${pushParam(params, value)}`);
|
|
1752
|
+
prefix = `UPDATE ${quoteIdentifier(table.name)} SET ${assignments.join(", ")}`;
|
|
1753
|
+
} else
|
|
1754
|
+
prefix = `DELETE FROM ${quoteIdentifier(table.name)}`;
|
|
1755
|
+
const { clause } = buildQueryWhereClause(table, options, options.whereNodes ?? [], params);
|
|
1756
|
+
return {
|
|
1757
|
+
text: prefix + clause + returningSuffix(columns.map(quoteIdentifier).join(", ")),
|
|
1758
|
+
params
|
|
1759
|
+
};
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
// ../../src/core/database/relationships.ts
|
|
1763
|
+
function hasMany(definition) {
|
|
1764
|
+
return {
|
|
1765
|
+
type: "hasMany",
|
|
1766
|
+
...definition
|
|
1767
|
+
};
|
|
1768
|
+
}
|
|
1769
|
+
function hasOne(definition) {
|
|
1770
|
+
return {
|
|
1771
|
+
type: "hasOne",
|
|
1772
|
+
...definition
|
|
1773
|
+
};
|
|
1774
|
+
}
|
|
1775
|
+
function belongsTo(definition) {
|
|
1776
|
+
return {
|
|
1777
|
+
type: "belongsTo",
|
|
1778
|
+
...definition
|
|
1779
|
+
};
|
|
1780
|
+
}
|
|
1781
|
+
function hasManyThrough(definition) {
|
|
1782
|
+
return {
|
|
1783
|
+
type: "hasManyThrough",
|
|
1784
|
+
throughParentKey: "__through_parent_id",
|
|
1785
|
+
...definition
|
|
1786
|
+
};
|
|
1787
|
+
}
|
|
1788
|
+
function belongsToMany(definition) {
|
|
1789
|
+
return {
|
|
1790
|
+
type: "belongsToMany",
|
|
1791
|
+
...definition
|
|
1792
|
+
};
|
|
1793
|
+
}
|
|
1794
|
+
function relationMatchKey(value) {
|
|
1795
|
+
if (value === null || value === undefined) {
|
|
1796
|
+
return "";
|
|
1797
|
+
}
|
|
1798
|
+
if (typeof value === "bigint") {
|
|
1799
|
+
return value.toString();
|
|
1800
|
+
}
|
|
1801
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
1802
|
+
return String(value);
|
|
1803
|
+
}
|
|
1804
|
+
if (typeof value === "string" && /^-?\d+$/.test(value)) {
|
|
1805
|
+
return BigInt(value).toString();
|
|
1806
|
+
}
|
|
1807
|
+
return String(value);
|
|
1808
|
+
}
|
|
1809
|
+
var relationLookupCache = new WeakMap;
|
|
1810
|
+
function indexedRelationLookup(map) {
|
|
1811
|
+
const cached = relationLookupCache.get(map);
|
|
1812
|
+
if (cached) {
|
|
1813
|
+
return cached;
|
|
1814
|
+
}
|
|
1815
|
+
const indexed = new Map;
|
|
1816
|
+
for (const [existing, value] of map) {
|
|
1817
|
+
indexed.set(relationMatchKey(existing), value);
|
|
1818
|
+
}
|
|
1819
|
+
relationLookupCache.set(map, indexed);
|
|
1820
|
+
return indexed;
|
|
1821
|
+
}
|
|
1822
|
+
function getByRelationKey(map, key) {
|
|
1823
|
+
if (map.has(key)) {
|
|
1824
|
+
return map.get(key);
|
|
1825
|
+
}
|
|
1826
|
+
const want = relationMatchKey(key);
|
|
1827
|
+
if (want === "") {
|
|
1828
|
+
return;
|
|
1829
|
+
}
|
|
1830
|
+
return indexedRelationLookup(map).get(want);
|
|
1831
|
+
}
|
|
1832
|
+
function indexHasManyRelation(parents, children, relation) {
|
|
1833
|
+
const groups = new Map;
|
|
1834
|
+
const originalKeys = new Map;
|
|
1835
|
+
for (const parent of parents) {
|
|
1836
|
+
const key = relationMatchKey(parent[relation.localKey]);
|
|
1837
|
+
if (!groups.has(key)) {
|
|
1838
|
+
groups.set(key, []);
|
|
1839
|
+
originalKeys.set(key, parent[relation.localKey]);
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
for (const child of children) {
|
|
1843
|
+
const group = groups.get(relationMatchKey(child[relation.foreignKey]));
|
|
1844
|
+
if (!group) {
|
|
1845
|
+
continue;
|
|
1846
|
+
}
|
|
1847
|
+
group.push(child);
|
|
1848
|
+
}
|
|
1849
|
+
const result = new Map;
|
|
1850
|
+
for (const [key, group] of groups) {
|
|
1851
|
+
const original = originalKeys.get(key);
|
|
1852
|
+
if (original !== undefined) {
|
|
1853
|
+
result.set(original, group);
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
return result;
|
|
1857
|
+
}
|
|
1858
|
+
function indexHasOneRelation(parents, children, relation) {
|
|
1859
|
+
const grouped = indexHasManyRelation(parents, children, relation);
|
|
1860
|
+
const result = new Map;
|
|
1861
|
+
for (const parent of parents) {
|
|
1862
|
+
const matches = getByRelationKey(grouped, parent[relation.localKey]) ?? [];
|
|
1863
|
+
result.set(parent[relation.localKey], matches[0]);
|
|
1864
|
+
}
|
|
1865
|
+
return result;
|
|
1866
|
+
}
|
|
1867
|
+
function indexBelongsToRelation(children, parents, relation) {
|
|
1868
|
+
const parentsById = new Map;
|
|
1869
|
+
for (const parent of parents) {
|
|
1870
|
+
parentsById.set(relationMatchKey(parent[relation.ownerKey]), parent);
|
|
1871
|
+
}
|
|
1872
|
+
const result = new Map;
|
|
1873
|
+
for (const child of children) {
|
|
1874
|
+
const foreignKey = child[relation.foreignKey];
|
|
1875
|
+
const parent = parentsById.get(relationMatchKey(foreignKey));
|
|
1876
|
+
if (parent) {
|
|
1877
|
+
result.set(foreignKey, parent);
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
return result;
|
|
1881
|
+
}
|
|
1882
|
+
function indexBelongsToManyRelation(parents, pivotRows, relatedRows, relation) {
|
|
1883
|
+
const relatedById = new Map;
|
|
1884
|
+
for (const related of relatedRows) {
|
|
1885
|
+
relatedById.set(relationMatchKey(related[relation.relatedKey]), related);
|
|
1886
|
+
}
|
|
1887
|
+
const groups = new Map;
|
|
1888
|
+
const originalKeys = new Map;
|
|
1889
|
+
for (const parent of parents) {
|
|
1890
|
+
const key = relationMatchKey(parent[relation.parentKey]);
|
|
1891
|
+
if (!groups.has(key)) {
|
|
1892
|
+
groups.set(key, []);
|
|
1893
|
+
originalKeys.set(key, parent[relation.parentKey]);
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
for (const pivot of pivotRows) {
|
|
1897
|
+
const group = groups.get(relationMatchKey(pivot[relation.foreignPivotKey]));
|
|
1898
|
+
const related = relatedById.get(relationMatchKey(pivot[relation.relatedPivotKey]));
|
|
1899
|
+
if (!group || !related) {
|
|
1900
|
+
continue;
|
|
1901
|
+
}
|
|
1902
|
+
group.push(related);
|
|
1903
|
+
}
|
|
1904
|
+
const result = new Map;
|
|
1905
|
+
for (const [key, group] of groups) {
|
|
1906
|
+
const original = originalKeys.get(key);
|
|
1907
|
+
if (original !== undefined) {
|
|
1908
|
+
result.set(original, group);
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
return result;
|
|
1912
|
+
}
|
|
1913
|
+
function morphMany(definition) {
|
|
1914
|
+
return {
|
|
1915
|
+
type: "morphMany",
|
|
1916
|
+
...definition
|
|
1917
|
+
};
|
|
1918
|
+
}
|
|
1919
|
+
function morphOne(definition) {
|
|
1920
|
+
return {
|
|
1921
|
+
type: "morphOne",
|
|
1922
|
+
...definition
|
|
1923
|
+
};
|
|
1924
|
+
}
|
|
1925
|
+
function morphTo(definition) {
|
|
1926
|
+
return {
|
|
1927
|
+
type: "morphTo",
|
|
1928
|
+
...definition
|
|
1929
|
+
};
|
|
1930
|
+
}
|
|
1931
|
+
function indexMorphManyRelation(parents, children, relation) {
|
|
1932
|
+
const groups = new Map;
|
|
1933
|
+
const originalKeys = new Map;
|
|
1934
|
+
for (const parent of parents) {
|
|
1935
|
+
const key = relationMatchKey(parent[relation.localKey]);
|
|
1936
|
+
if (!groups.has(key)) {
|
|
1937
|
+
groups.set(key, []);
|
|
1938
|
+
originalKeys.set(key, parent[relation.localKey]);
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
for (const child of children) {
|
|
1942
|
+
if (child[relation.morphTypeKey] !== relation.morphType) {
|
|
1943
|
+
continue;
|
|
1944
|
+
}
|
|
1945
|
+
const group = groups.get(relationMatchKey(child[relation.morphIdKey]));
|
|
1946
|
+
if (!group) {
|
|
1947
|
+
continue;
|
|
1948
|
+
}
|
|
1949
|
+
group.push(child);
|
|
1950
|
+
}
|
|
1951
|
+
const result = new Map;
|
|
1952
|
+
for (const [key, group] of groups) {
|
|
1953
|
+
const original = originalKeys.get(key);
|
|
1954
|
+
if (original !== undefined) {
|
|
1955
|
+
result.set(original, group);
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
return result;
|
|
1959
|
+
}
|
|
1960
|
+
function indexMorphOneRelation(parents, children, relation) {
|
|
1961
|
+
const grouped = indexMorphManyRelation(parents, children, relation);
|
|
1962
|
+
const result = new Map;
|
|
1963
|
+
for (const parent of parents) {
|
|
1964
|
+
const matches = getByRelationKey(grouped, parent[relation.localKey]) ?? [];
|
|
1965
|
+
result.set(parent[relation.localKey], matches[0]);
|
|
1966
|
+
}
|
|
1967
|
+
return result;
|
|
1968
|
+
}
|
|
1969
|
+
function indexHasManyThroughRelation(parents, children, relation) {
|
|
1970
|
+
const throughKey = relation.throughParentKey ?? "__through_parent_id";
|
|
1971
|
+
const grouped = new Map;
|
|
1972
|
+
for (const child of children) {
|
|
1973
|
+
const key = relationMatchKey(child[throughKey]);
|
|
1974
|
+
if (key === "") {
|
|
1975
|
+
continue;
|
|
1976
|
+
}
|
|
1977
|
+
const existing = grouped.get(key) ?? [];
|
|
1978
|
+
const { [throughKey]: _through, ...far } = child;
|
|
1979
|
+
existing.push(far);
|
|
1980
|
+
grouped.set(key, existing);
|
|
1981
|
+
}
|
|
1982
|
+
const result = new Map;
|
|
1983
|
+
for (const parent of parents) {
|
|
1984
|
+
const key = relationMatchKey(parent[relation.localKey]);
|
|
1985
|
+
result.set(parent[relation.localKey], grouped.get(key) ?? []);
|
|
1986
|
+
}
|
|
1987
|
+
return result;
|
|
1988
|
+
}
|
|
1989
|
+
function indexMorphToRelation(children, parentsByType, relation) {
|
|
1990
|
+
const result = new Map;
|
|
1991
|
+
for (const child of children) {
|
|
1992
|
+
const morphType = String(child[relation.morphTypeKey]);
|
|
1993
|
+
const parents = parentsByType.get(morphType);
|
|
1994
|
+
if (!parents) {
|
|
1995
|
+
continue;
|
|
1996
|
+
}
|
|
1997
|
+
const parent = getByRelationKey(parents, child[relation.morphIdKey]);
|
|
1998
|
+
if (parent) {
|
|
1999
|
+
result.set(child[relation.morphIdKey], parent);
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
return result;
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
// ../../src/core/database/baseRepository.ts
|
|
2006
|
+
init_repositoryConnection();
|
|
2007
|
+
|
|
2008
|
+
// ../../src/core/database/pluck.ts
|
|
2009
|
+
function uniqueColumnSelect(table, columns) {
|
|
2010
|
+
const seen = new Set;
|
|
2011
|
+
const select = [];
|
|
2012
|
+
for (const column of columns) {
|
|
2013
|
+
if (seen.has(column)) {
|
|
2014
|
+
continue;
|
|
2015
|
+
}
|
|
2016
|
+
seen.add(column);
|
|
2017
|
+
select.push({ kind: "column", table, column, as: column });
|
|
2018
|
+
}
|
|
2019
|
+
return select;
|
|
2020
|
+
}
|
|
2021
|
+
function projectPluck(rows, column, keyBy) {
|
|
2022
|
+
if (keyBy === undefined) {
|
|
2023
|
+
return rows.map((row) => row[column]);
|
|
2024
|
+
}
|
|
2025
|
+
const keyed = new Map;
|
|
2026
|
+
for (const row of rows) {
|
|
2027
|
+
keyed.set(row[keyBy], row[column]);
|
|
2028
|
+
}
|
|
2029
|
+
return keyed;
|
|
2030
|
+
}
|
|
2031
|
+
|
|
2032
|
+
// ../../src/core/database/whereBuilder.ts
|
|
2033
|
+
class WhereBuilder {
|
|
2034
|
+
nodes = [];
|
|
2035
|
+
where(where) {
|
|
2036
|
+
this.nodes.push({ kind: "and", where });
|
|
2037
|
+
return this;
|
|
2038
|
+
}
|
|
2039
|
+
orWhere(where) {
|
|
2040
|
+
this.nodes.push({ kind: "or", where });
|
|
2041
|
+
return this;
|
|
2042
|
+
}
|
|
2043
|
+
whereGroup(fn) {
|
|
2044
|
+
const nested = new WhereBuilder;
|
|
2045
|
+
fn(nested);
|
|
2046
|
+
if (nested.nodes.length > 0) {
|
|
2047
|
+
this.nodes.push({ kind: "and", group: nested.nodes });
|
|
2048
|
+
}
|
|
2049
|
+
return this;
|
|
2050
|
+
}
|
|
2051
|
+
orWhereGroup(fn) {
|
|
2052
|
+
const nested = new WhereBuilder;
|
|
2053
|
+
fn(nested);
|
|
2054
|
+
if (nested.nodes.length > 0) {
|
|
2055
|
+
this.nodes.push({ kind: "or", group: nested.nodes });
|
|
2056
|
+
}
|
|
2057
|
+
return this;
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
// ../../src/core/database/repositoryQuery.ts
|
|
2062
|
+
class RepositoryQuery {
|
|
2063
|
+
repository;
|
|
2064
|
+
whereClause;
|
|
2065
|
+
queryOptions;
|
|
2066
|
+
eagerLoads = [];
|
|
2067
|
+
whereNodes = [];
|
|
2068
|
+
protectedWhere = [];
|
|
2069
|
+
constructor(repository, whereClause = {}, queryOptions = {}) {
|
|
2070
|
+
this.repository = repository;
|
|
2071
|
+
this.whereClause = whereClause;
|
|
2072
|
+
this.queryOptions = queryOptions;
|
|
2073
|
+
}
|
|
2074
|
+
where(input) {
|
|
2075
|
+
if (typeof input === "function") {
|
|
2076
|
+
const builder = new WhereBuilder;
|
|
2077
|
+
input(builder);
|
|
2078
|
+
if (builder.nodes.length > 0)
|
|
2079
|
+
this.whereNodes.push({ kind: "and", group: builder.nodes });
|
|
2080
|
+
return this;
|
|
2081
|
+
}
|
|
2082
|
+
this.whereNodes.push({ kind: "and", where: { ...input } });
|
|
2083
|
+
return this;
|
|
2084
|
+
}
|
|
2085
|
+
protectWhere() {
|
|
2086
|
+
const nodes = [];
|
|
2087
|
+
if (Object.keys(this.whereClause).length > 0) {
|
|
2088
|
+
nodes.push({ kind: "and", where: this.whereClause });
|
|
2089
|
+
}
|
|
2090
|
+
nodes.push(...this.whereNodes);
|
|
2091
|
+
if (nodes.length > 0)
|
|
2092
|
+
this.protectedWhere.push({ kind: "and", group: nodes });
|
|
2093
|
+
this.whereClause = {};
|
|
2094
|
+
this.whereNodes.length = 0;
|
|
2095
|
+
return this;
|
|
2096
|
+
}
|
|
2097
|
+
orWhere(input) {
|
|
2098
|
+
if (typeof input === "function") {
|
|
2099
|
+
const builder = new WhereBuilder;
|
|
2100
|
+
input(builder);
|
|
2101
|
+
if (builder.nodes.length > 0) {
|
|
2102
|
+
this.whereNodes.push({ kind: "or", group: builder.nodes });
|
|
2103
|
+
}
|
|
2104
|
+
return this;
|
|
2105
|
+
}
|
|
2106
|
+
this.whereNodes.push({ kind: "or", where: input });
|
|
2107
|
+
return this;
|
|
2108
|
+
}
|
|
2109
|
+
lockForUpdate(options = {}) {
|
|
2110
|
+
this.queryOptions = {
|
|
2111
|
+
...this.queryOptions,
|
|
2112
|
+
lock: { ...options, of: options.of ? [...options.of] : undefined, mode: "update" }
|
|
2113
|
+
};
|
|
2114
|
+
return this;
|
|
2115
|
+
}
|
|
2116
|
+
sharedLock(options = {}) {
|
|
2117
|
+
this.queryOptions = {
|
|
2118
|
+
...this.queryOptions,
|
|
2119
|
+
lock: { ...options, of: options.of ? [...options.of] : undefined, mode: "share" }
|
|
2120
|
+
};
|
|
2121
|
+
return this;
|
|
2122
|
+
}
|
|
2123
|
+
orderBy(orderBy) {
|
|
2124
|
+
this.queryOptions = { ...this.queryOptions, orderBy };
|
|
2125
|
+
return this;
|
|
2126
|
+
}
|
|
2127
|
+
limit(limit) {
|
|
2128
|
+
this.queryOptions = { ...this.queryOptions, limit };
|
|
2129
|
+
return this;
|
|
2130
|
+
}
|
|
2131
|
+
whereNull(column) {
|
|
2132
|
+
return this.where({ [column]: null });
|
|
2133
|
+
}
|
|
2134
|
+
whereNotNull(column) {
|
|
2135
|
+
return this.where({ [column]: { isNull: false } });
|
|
2136
|
+
}
|
|
2137
|
+
whereNotIn(column, values) {
|
|
2138
|
+
return this.where({ [column]: { notIn: values } });
|
|
2139
|
+
}
|
|
2140
|
+
withSubqueryCount(alias, sql, params = []) {
|
|
2141
|
+
const table = this.repository.getTable();
|
|
2142
|
+
const existing = this.queryOptions.select ?? table.columns.map((column) => ({
|
|
2143
|
+
kind: "column",
|
|
2144
|
+
table: table.name,
|
|
2145
|
+
column
|
|
2146
|
+
}));
|
|
2147
|
+
this.queryOptions = {
|
|
2148
|
+
...this.queryOptions,
|
|
2149
|
+
select: [...existing, { kind: "subqueryCount", sql, params, as: alias }]
|
|
2150
|
+
};
|
|
2151
|
+
return this;
|
|
2152
|
+
}
|
|
2153
|
+
whereIn(column, values) {
|
|
2154
|
+
return this.where({ [column]: values });
|
|
2155
|
+
}
|
|
2156
|
+
whereExists(sql, params = []) {
|
|
2157
|
+
this.whereNodes.push({ kind: "and", exists: { sql, params } });
|
|
2158
|
+
return this;
|
|
2159
|
+
}
|
|
2160
|
+
whereNotExists(sql, params = []) {
|
|
2161
|
+
this.whereNodes.push({ kind: "and", exists: { sql, params, not: true } });
|
|
2162
|
+
return this;
|
|
2163
|
+
}
|
|
2164
|
+
offset(offset) {
|
|
2165
|
+
this.queryOptions = { ...this.queryOptions, offset };
|
|
2166
|
+
return this;
|
|
2167
|
+
}
|
|
2168
|
+
join(left, right) {
|
|
2169
|
+
return this.addJoin("inner", left, right);
|
|
2170
|
+
}
|
|
2171
|
+
leftJoin(left, right) {
|
|
2172
|
+
return this.addJoin("left", left, right);
|
|
2173
|
+
}
|
|
2174
|
+
groupBy(groupBy) {
|
|
2175
|
+
this.queryOptions = { ...this.queryOptions, groupBy };
|
|
2176
|
+
return this;
|
|
2177
|
+
}
|
|
2178
|
+
having(having) {
|
|
2179
|
+
this.queryOptions = { ...this.queryOptions, having };
|
|
2180
|
+
return this;
|
|
2181
|
+
}
|
|
2182
|
+
withHasMany(as, relation, childRepository, options = {}) {
|
|
2183
|
+
this.eagerLoads.push({
|
|
2184
|
+
kind: "hasMany",
|
|
2185
|
+
as,
|
|
2186
|
+
relation,
|
|
2187
|
+
repository: childRepository,
|
|
2188
|
+
options
|
|
2189
|
+
});
|
|
2190
|
+
return this;
|
|
2191
|
+
}
|
|
2192
|
+
withBelongsTo(as, relation, parentRepository, options = {}) {
|
|
2193
|
+
this.eagerLoads.push({
|
|
2194
|
+
kind: "belongsTo",
|
|
2195
|
+
as,
|
|
2196
|
+
relation,
|
|
2197
|
+
repository: parentRepository,
|
|
2198
|
+
options
|
|
2199
|
+
});
|
|
2200
|
+
return this;
|
|
2201
|
+
}
|
|
2202
|
+
withMorphMany(as, relation, childRepository, options = {}) {
|
|
2203
|
+
this.eagerLoads.push({
|
|
2204
|
+
kind: "morphMany",
|
|
2205
|
+
as,
|
|
2206
|
+
relation,
|
|
2207
|
+
repository: childRepository,
|
|
2208
|
+
options
|
|
2209
|
+
});
|
|
2210
|
+
return this;
|
|
2211
|
+
}
|
|
2212
|
+
withMorphOne(as, relation, childRepository, options = {}) {
|
|
2213
|
+
this.eagerLoads.push({
|
|
2214
|
+
kind: "morphOne",
|
|
2215
|
+
as,
|
|
2216
|
+
relation,
|
|
2217
|
+
repository: childRepository,
|
|
2218
|
+
options
|
|
2219
|
+
});
|
|
2220
|
+
return this;
|
|
2221
|
+
}
|
|
2222
|
+
withMorphTo(as, relation, repositoriesByType, options = {}, optionsByType) {
|
|
2223
|
+
this.eagerLoads.push({
|
|
2224
|
+
kind: "morphTo",
|
|
2225
|
+
morphOptions: optionsByType,
|
|
2226
|
+
as,
|
|
2227
|
+
relation,
|
|
2228
|
+
repository: this.repository,
|
|
2229
|
+
morphRepositories: repositoriesByType,
|
|
2230
|
+
options
|
|
2231
|
+
});
|
|
2232
|
+
return this;
|
|
2233
|
+
}
|
|
2234
|
+
withBelongsToMany(as, relation, relatedRepository, options = {}) {
|
|
2235
|
+
this.eagerLoads.push({
|
|
2236
|
+
kind: "belongsToMany",
|
|
2237
|
+
as,
|
|
2238
|
+
relation,
|
|
2239
|
+
repository: relatedRepository,
|
|
2240
|
+
options
|
|
2241
|
+
});
|
|
2242
|
+
return this;
|
|
2243
|
+
}
|
|
2244
|
+
withHasManyThrough(as, relation, farRepository, options = {}) {
|
|
2245
|
+
this.eagerLoads.push({
|
|
2246
|
+
kind: "hasManyThrough",
|
|
2247
|
+
as,
|
|
2248
|
+
relation,
|
|
2249
|
+
repository: farRepository,
|
|
2250
|
+
options
|
|
2251
|
+
});
|
|
2252
|
+
return this;
|
|
2253
|
+
}
|
|
2254
|
+
withTrashed() {
|
|
2255
|
+
this.queryOptions = { ...this.queryOptions, withTrashed: true };
|
|
2256
|
+
return this;
|
|
2257
|
+
}
|
|
2258
|
+
onlyTrashed() {
|
|
2259
|
+
this.queryOptions = { ...this.queryOptions, onlyTrashed: true };
|
|
2260
|
+
return this;
|
|
2261
|
+
}
|
|
2262
|
+
async update(changes) {
|
|
2263
|
+
this.assertNoMutationEagerLoads();
|
|
2264
|
+
return this.repository.updateWhere(changes, this.buildOptions());
|
|
2265
|
+
}
|
|
2266
|
+
async updateReturning(changes, ...columns) {
|
|
2267
|
+
this.assertNoMutationEagerLoads();
|
|
2268
|
+
return this.repository.updateWhereReturning(changes, columns, this.buildOptions());
|
|
2269
|
+
}
|
|
2270
|
+
async delete() {
|
|
2271
|
+
this.assertNoMutationEagerLoads();
|
|
2272
|
+
return this.repository.deleteWhere(this.buildOptions());
|
|
2273
|
+
}
|
|
2274
|
+
assertNoMutationEagerLoads() {
|
|
2275
|
+
if (this.eagerLoads.length)
|
|
2276
|
+
throw new Error("Conditional writes do not support eager loading.");
|
|
2277
|
+
}
|
|
2278
|
+
async get() {
|
|
2279
|
+
const rows = await this.repository.findAll(this.buildOptions());
|
|
2280
|
+
return await this.attach(rows);
|
|
2281
|
+
}
|
|
2282
|
+
async first() {
|
|
2283
|
+
const rows = await this.repository.findAll({ ...this.buildOptions(), limit: 1 });
|
|
2284
|
+
const attached = await this.attach(rows);
|
|
2285
|
+
return attached[0] ?? null;
|
|
2286
|
+
}
|
|
2287
|
+
async sumBy(groupColumn, column) {
|
|
2288
|
+
if (this.eagerLoads.length)
|
|
2289
|
+
throw new TypeError("Grouped sums do not support eager loading.");
|
|
2290
|
+
return this.repository.sumBy(groupColumn, column, this.buildOptions());
|
|
2291
|
+
}
|
|
2292
|
+
async count() {
|
|
2293
|
+
return await this.repository.count(this.buildOptions());
|
|
2294
|
+
}
|
|
2295
|
+
async pluck(column, keyBy) {
|
|
2296
|
+
const rows = await this.repository.findAll({
|
|
2297
|
+
...this.buildOptions(),
|
|
2298
|
+
select: uniqueColumnSelect(this.repository.getTable().name, keyBy === undefined || keyBy === column ? [column] : [column, keyBy])
|
|
2299
|
+
});
|
|
2300
|
+
if (keyBy === undefined) {
|
|
2301
|
+
return projectPluck(rows, column);
|
|
2302
|
+
}
|
|
2303
|
+
return projectPluck(rows, column, keyBy);
|
|
2304
|
+
}
|
|
2305
|
+
async value(column) {
|
|
2306
|
+
const rows = await this.repository.findAll({
|
|
2307
|
+
...this.buildOptions(),
|
|
2308
|
+
select: uniqueColumnSelect(this.repository.getTable().name, [column]),
|
|
2309
|
+
limit: 1
|
|
2310
|
+
});
|
|
2311
|
+
const row = rows[0];
|
|
2312
|
+
if (row === undefined) {
|
|
2313
|
+
return null;
|
|
2314
|
+
}
|
|
2315
|
+
return row[column];
|
|
2316
|
+
}
|
|
2317
|
+
async project(columns, limit) {
|
|
2318
|
+
if (columns.length === 0)
|
|
2319
|
+
throw new RangeError("A projection requires at least one column.");
|
|
2320
|
+
const table = this.repository.getTable();
|
|
2321
|
+
for (const column of columns)
|
|
2322
|
+
if (!table.columns.includes(column))
|
|
2323
|
+
throw new TypeError(`Unknown projection column: ${column}`);
|
|
2324
|
+
return await this.repository.findAll({
|
|
2325
|
+
...this.buildOptions(),
|
|
2326
|
+
...limit === undefined ? {} : { limit },
|
|
2327
|
+
select: uniqueColumnSelect(table.name, columns)
|
|
2328
|
+
});
|
|
2329
|
+
}
|
|
2330
|
+
async attachToRows(rows) {
|
|
2331
|
+
return await this.attach(rows);
|
|
2332
|
+
}
|
|
2333
|
+
async paginate(options) {
|
|
2334
|
+
return await this.repository.paginate({
|
|
2335
|
+
...this.buildOptions(),
|
|
2336
|
+
page: options.page,
|
|
2337
|
+
perPage: options.perPage
|
|
2338
|
+
});
|
|
2339
|
+
}
|
|
2340
|
+
async chunk(count, callback) {
|
|
2341
|
+
await this.repository.chunk(count, async (rows) => callback(await this.attach(rows)), this.buildOptions());
|
|
2342
|
+
}
|
|
2343
|
+
async cursorPaginate(options) {
|
|
2344
|
+
const page = await this.repository.cursorPaginate({ ...this.buildOptions(), ...options });
|
|
2345
|
+
return { ...page, data: await this.attach(page.data) };
|
|
2346
|
+
}
|
|
2347
|
+
async keysetPaginate(options) {
|
|
2348
|
+
const page = await this.repository.keysetPaginate({ ...this.buildOptions(), ...options });
|
|
2349
|
+
return { ...page, data: await this.attach(page.data) };
|
|
2350
|
+
}
|
|
2351
|
+
async projectKeyset(columns, options) {
|
|
2352
|
+
if (!columns.length || columns.some((column) => !this.repository.getTable().columns.includes(column)))
|
|
2353
|
+
throw new TypeError("Keyset projections require declared columns.");
|
|
2354
|
+
const page = await this.repository.keysetPaginate({
|
|
2355
|
+
...this.buildOptions(),
|
|
2356
|
+
...options,
|
|
2357
|
+
select: uniqueColumnSelect(this.repository.getTable().name, columns)
|
|
2358
|
+
});
|
|
2359
|
+
return { ...page, data: page.data };
|
|
2360
|
+
}
|
|
2361
|
+
getOptions() {
|
|
2362
|
+
return this.buildOptions();
|
|
2363
|
+
}
|
|
2364
|
+
buildOptions() {
|
|
2365
|
+
if (this.protectedWhere.length === 0) {
|
|
2366
|
+
return { ...this.queryOptions, where: this.whereClause, whereNodes: this.whereNodes };
|
|
2367
|
+
}
|
|
2368
|
+
return {
|
|
2369
|
+
...this.queryOptions,
|
|
2370
|
+
where: {},
|
|
2371
|
+
whereNodes: [...this.protectedWhere, { kind: "and", group: [...this.whereNodes] }]
|
|
2372
|
+
};
|
|
2373
|
+
}
|
|
2374
|
+
addJoin(type, left, right) {
|
|
2375
|
+
const leftRef = parseQualifiedColumn(left);
|
|
2376
|
+
const rightRef = parseQualifiedColumn(right);
|
|
2377
|
+
const table = type === "inner" ? rightRef.table : rightRef.table;
|
|
2378
|
+
const joins = this.queryOptions.joins ?? [];
|
|
2379
|
+
const existing = joins.find((join) => join.table === table && join.type === type);
|
|
2380
|
+
if (existing) {
|
|
2381
|
+
existing.on.push({ left: leftRef, right: rightRef });
|
|
2382
|
+
return this;
|
|
2383
|
+
}
|
|
2384
|
+
this.queryOptions = {
|
|
2385
|
+
...this.queryOptions,
|
|
2386
|
+
joins: [
|
|
2387
|
+
...joins,
|
|
2388
|
+
{
|
|
2389
|
+
type,
|
|
2390
|
+
table,
|
|
2391
|
+
on: [{ left: leftRef, right: rightRef }]
|
|
2392
|
+
}
|
|
2393
|
+
]
|
|
2394
|
+
};
|
|
2395
|
+
return this;
|
|
2396
|
+
}
|
|
2397
|
+
async attach(rows) {
|
|
2398
|
+
const result = rows.map((row) => ({ ...row }));
|
|
2399
|
+
if (result.length === 0 || this.eagerLoads.length === 0) {
|
|
2400
|
+
return result;
|
|
2401
|
+
}
|
|
2402
|
+
await Promise.all(this.eagerLoads.map((load) => this.hydrateEagerLoad(rows, result, load)));
|
|
2403
|
+
return result;
|
|
2404
|
+
}
|
|
2405
|
+
async hydrateEagerLoad(rows, result, load) {
|
|
2406
|
+
if (load.kind === "hasMany") {
|
|
2407
|
+
const relation = load.relation;
|
|
2408
|
+
const grouped = await load.repository.withConnection(this.repository.getConnection()).loadHasManyForParents(rows, relation, load.options);
|
|
2409
|
+
for (const row of result) {
|
|
2410
|
+
row[load.as] = getByRelationKey(grouped, row[relation.localKey]) ?? [];
|
|
2411
|
+
}
|
|
2412
|
+
return;
|
|
2413
|
+
}
|
|
2414
|
+
if (load.kind === "morphMany") {
|
|
2415
|
+
const relation = load.relation;
|
|
2416
|
+
const grouped = await load.repository.withConnection(this.repository.getConnection()).loadMorphManyForParents(rows, relation, load.options);
|
|
2417
|
+
for (const row of result) {
|
|
2418
|
+
row[load.as] = getByRelationKey(grouped, row[relation.localKey]) ?? [];
|
|
2419
|
+
}
|
|
2420
|
+
return;
|
|
2421
|
+
}
|
|
2422
|
+
if (load.kind === "morphOne") {
|
|
2423
|
+
const relation = load.relation;
|
|
2424
|
+
const grouped = await load.repository.withConnection(this.repository.getConnection()).loadMorphOneForParents(rows, relation, load.options);
|
|
2425
|
+
for (const row of result) {
|
|
2426
|
+
row[load.as] = getByRelationKey(grouped, row[relation.localKey]);
|
|
2427
|
+
}
|
|
2428
|
+
return;
|
|
2429
|
+
}
|
|
2430
|
+
if (load.kind === "hasManyThrough") {
|
|
2431
|
+
const relation = load.relation;
|
|
2432
|
+
const grouped = await load.repository.withConnection(this.repository.getConnection()).loadHasManyThroughForParents(rows, relation, load.options);
|
|
2433
|
+
for (const row of result) {
|
|
2434
|
+
row[load.as] = getByRelationKey(grouped, row[relation.localKey]) ?? [];
|
|
2435
|
+
}
|
|
2436
|
+
return;
|
|
2437
|
+
}
|
|
2438
|
+
if (load.kind === "belongsToMany") {
|
|
2439
|
+
const relation = load.relation;
|
|
2440
|
+
const grouped = await this.repository.loadBelongsToManyForParents(rows, relation, load.repository, load.options);
|
|
2441
|
+
for (const row of result) {
|
|
2442
|
+
row[load.as] = getByRelationKey(grouped, row[relation.parentKey]) ?? [];
|
|
2443
|
+
}
|
|
2444
|
+
return;
|
|
2445
|
+
}
|
|
2446
|
+
if (load.kind === "morphTo") {
|
|
2447
|
+
const relation = load.relation;
|
|
2448
|
+
const repositories = load.morphRepositories ?? new Map;
|
|
2449
|
+
const partitions = new Map;
|
|
2450
|
+
rows.forEach((row, index) => {
|
|
2451
|
+
const type = String(row[relation.morphTypeKey]);
|
|
2452
|
+
const target = result[index];
|
|
2453
|
+
if (!target || !repositories.has(type))
|
|
2454
|
+
return;
|
|
2455
|
+
const partition = partitions.get(type) ?? { rows: [], targets: [] };
|
|
2456
|
+
partition.rows.push(row);
|
|
2457
|
+
partition.targets.push(target);
|
|
2458
|
+
partitions.set(type, partition);
|
|
2459
|
+
});
|
|
2460
|
+
await Promise.all([...partitions.entries()].map(async ([type, partition]) => {
|
|
2461
|
+
const repository = repositories.get(type);
|
|
2462
|
+
if (!repository)
|
|
2463
|
+
return;
|
|
2464
|
+
const grouped = await this.repository.loadMorphToForChildren(partition.rows, relation, new Map([[type, repository]]), load.morphOptions?.get(type) ?? load.options);
|
|
2465
|
+
for (const row of partition.targets) {
|
|
2466
|
+
row[load.as] = getByRelationKey(grouped, row[relation.morphIdKey]);
|
|
2467
|
+
}
|
|
2468
|
+
}));
|
|
2469
|
+
return;
|
|
2470
|
+
}
|
|
2471
|
+
const relation = load.relation;
|
|
2472
|
+
const grouped = await this.repository.loadBelongsToForParents(rows, relation, load.repository, load.options);
|
|
2473
|
+
for (const row of result) {
|
|
2474
|
+
row[load.as] = getByRelationKey(grouped, row[relation.foreignKey]);
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2478
|
+
|
|
2479
|
+
// ../../src/core/database/baseRepository.ts
|
|
2480
|
+
init_transaction();
|
|
2481
|
+
init_transactionControl();
|
|
2482
|
+
function listOrders(orderBy) {
|
|
2483
|
+
if (!orderBy)
|
|
2484
|
+
return [];
|
|
2485
|
+
if (Array.isArray(orderBy))
|
|
2486
|
+
return orderBy.map(readOrder);
|
|
2487
|
+
if ("column" in orderBy)
|
|
2488
|
+
return [readOrder(orderBy)];
|
|
2489
|
+
return Object.entries(orderBy).flatMap(([column, direction]) => {
|
|
2490
|
+
if (direction === undefined)
|
|
2491
|
+
return [];
|
|
2492
|
+
return [
|
|
2493
|
+
{
|
|
2494
|
+
column,
|
|
2495
|
+
direction: String(direction).toUpperCase() === "DESC" ? "desc" : "asc"
|
|
2496
|
+
}
|
|
2497
|
+
];
|
|
2498
|
+
});
|
|
2499
|
+
}
|
|
2500
|
+
function readOrder(order) {
|
|
2501
|
+
return {
|
|
2502
|
+
column: String(order.column),
|
|
2503
|
+
direction: String(order.direction).toUpperCase() === "DESC" ? "desc" : "asc",
|
|
2504
|
+
...order.nulls === undefined ? {} : { nulls: order.nulls }
|
|
2505
|
+
};
|
|
2506
|
+
}
|
|
2507
|
+
|
|
2508
|
+
class BaseRepository {
|
|
2509
|
+
table;
|
|
2510
|
+
connection;
|
|
2511
|
+
constructor(table, connection = repositoryConnection) {
|
|
2512
|
+
this.table = table;
|
|
2513
|
+
this.connection = connection;
|
|
2514
|
+
}
|
|
2515
|
+
assertTransactionalEvent(action) {
|
|
2516
|
+
if (!eventBus.hasTransactionalListeners(modelEventName(this.table.name, action)))
|
|
2517
|
+
return;
|
|
2518
|
+
if (!hasActiveTransaction() || this.connection !== repositoryConnection && this.connection !== getActiveDatabaseConnection(repositoryConnection))
|
|
2519
|
+
throw new Error("Transactional model listeners require runInTransaction and the framework-bound repository connection before writing.");
|
|
2520
|
+
}
|
|
2521
|
+
async runModelMutation(action, operation) {
|
|
2522
|
+
if (!eventBus.hasTransactionalListeners(modelEventName(this.table.name, action)))
|
|
2523
|
+
return operation();
|
|
2524
|
+
return runInTransaction(operation);
|
|
2525
|
+
}
|
|
2526
|
+
async count(options = {}) {
|
|
2527
|
+
if (options.lock)
|
|
2528
|
+
throw new Error("Row locks require a row selection, not count().");
|
|
2529
|
+
const { whereNodes, where, ...rest } = options;
|
|
2530
|
+
return await this.countWhere(where ?? {}, rest, whereNodes ?? []);
|
|
2531
|
+
}
|
|
2532
|
+
async findAll(options = {}) {
|
|
2533
|
+
return await withDatabaseErrorHandling(async () => {
|
|
2534
|
+
if (options.lock && (!hasActiveTransaction() || this.connection !== repositoryConnection && this.connection !== getActiveDatabaseConnection(repositoryConnection))) {
|
|
2535
|
+
throw new Error("Row locks require the framework active transaction connection.");
|
|
2536
|
+
}
|
|
2537
|
+
const { whereNodes, ...queryOptions } = options;
|
|
2538
|
+
const { text, params } = buildSelectQuery(this.table, queryOptions, whereNodes ?? []);
|
|
2539
|
+
return await this.connection.unsafe(text, params);
|
|
2540
|
+
});
|
|
2541
|
+
}
|
|
2542
|
+
async paginate(options) {
|
|
2543
|
+
const { whereNodes, where = {}, page, perPage, ...queryOptions } = options;
|
|
2544
|
+
const total = await this.countWhere(where, {
|
|
2545
|
+
withTrashed: options.withTrashed,
|
|
2546
|
+
onlyTrashed: options.onlyTrashed,
|
|
2547
|
+
joins: options.joins,
|
|
2548
|
+
groupBy: options.groupBy
|
|
2549
|
+
}, whereNodes);
|
|
2550
|
+
const meta = buildPaginationMeta({ page, perPage, total });
|
|
2551
|
+
if (total === 0) {
|
|
2552
|
+
return { data: [], meta };
|
|
2553
|
+
}
|
|
2554
|
+
const offset = (page - 1) * perPage;
|
|
2555
|
+
const data = await this.findAll({
|
|
2556
|
+
...queryOptions,
|
|
2557
|
+
where,
|
|
2558
|
+
whereNodes,
|
|
2559
|
+
limit: perPage,
|
|
2560
|
+
offset
|
|
2561
|
+
});
|
|
2562
|
+
return {
|
|
2563
|
+
data,
|
|
2564
|
+
meta
|
|
2565
|
+
};
|
|
2566
|
+
}
|
|
2567
|
+
async chunk(count, callback, options = {}) {
|
|
2568
|
+
if (!Number.isInteger(count) || count <= 0) {
|
|
2569
|
+
throw new Error("Chunk size must be a positive integer.");
|
|
2570
|
+
}
|
|
2571
|
+
if (this.primaryKeySeekDirection(options) === "asc") {
|
|
2572
|
+
const seekOptions = { ...options };
|
|
2573
|
+
delete seekOptions.orderBy;
|
|
2574
|
+
await this.chunkById(count, async (rows) => await callback(rows) === false ? false : undefined, seekOptions);
|
|
2575
|
+
return;
|
|
2576
|
+
}
|
|
2577
|
+
let offset = 0;
|
|
2578
|
+
while (true) {
|
|
2579
|
+
const rows = await this.findAll({
|
|
2580
|
+
...options,
|
|
2581
|
+
limit: count,
|
|
2582
|
+
offset
|
|
2583
|
+
});
|
|
2584
|
+
if (rows.length === 0) {
|
|
2585
|
+
return;
|
|
2586
|
+
}
|
|
2587
|
+
const shouldContinue = await callback(rows);
|
|
2588
|
+
if (shouldContinue === false || rows.length < count) {
|
|
2589
|
+
return;
|
|
2590
|
+
}
|
|
2591
|
+
offset += count;
|
|
2592
|
+
}
|
|
2593
|
+
}
|
|
2594
|
+
async chunkById(count, callback, options = {}) {
|
|
2595
|
+
if (!Number.isInteger(count) || count <= 0) {
|
|
2596
|
+
throw new Error("Chunk size must be a positive integer.");
|
|
2597
|
+
}
|
|
2598
|
+
let cursor;
|
|
2599
|
+
while (true) {
|
|
2600
|
+
const { data, meta } = await this.cursorPaginate({
|
|
2601
|
+
...options,
|
|
2602
|
+
perPage: count,
|
|
2603
|
+
...cursor === undefined ? {} : { cursor }
|
|
2604
|
+
});
|
|
2605
|
+
if (data.length === 0) {
|
|
2606
|
+
return;
|
|
2607
|
+
}
|
|
2608
|
+
if (await callback(data) === false) {
|
|
2609
|
+
return;
|
|
2610
|
+
}
|
|
2611
|
+
if (!meta.has_more || meta.next_cursor === null) {
|
|
2612
|
+
return;
|
|
2613
|
+
}
|
|
2614
|
+
cursor = meta.next_cursor;
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
async cursorPaginate(options) {
|
|
2618
|
+
const {
|
|
2619
|
+
perPage,
|
|
2620
|
+
cursor,
|
|
2621
|
+
cursorColumn = this.table.primaryKey,
|
|
2622
|
+
direction = "asc",
|
|
2623
|
+
where = {},
|
|
2624
|
+
whereNodes,
|
|
2625
|
+
...queryOptions
|
|
2626
|
+
} = options;
|
|
2627
|
+
if (!Number.isInteger(perPage) || perPage <= 0) {
|
|
2628
|
+
throw new Error("Cursor page size must be a positive integer.");
|
|
2629
|
+
}
|
|
2630
|
+
const filters = [];
|
|
2631
|
+
if (Object.keys(where).length > 0)
|
|
2632
|
+
filters.push({ kind: "and", where });
|
|
2633
|
+
filters.push(...whereNodes ?? []);
|
|
2634
|
+
const cursorFilters = [{ kind: "and", group: filters }];
|
|
2635
|
+
if (cursor !== undefined) {
|
|
2636
|
+
const cursorWhere = {};
|
|
2637
|
+
cursorWhere[cursorColumn] = direction === "asc" ? { gt: cursor } : { lt: cursor };
|
|
2638
|
+
cursorFilters.push({ kind: "and", where: cursorWhere });
|
|
2639
|
+
}
|
|
2640
|
+
const rows = await this.findAll({
|
|
2641
|
+
...queryOptions,
|
|
2642
|
+
where: {},
|
|
2643
|
+
whereNodes: cursorFilters,
|
|
2644
|
+
orderBy: { [cursorColumn]: direction },
|
|
2645
|
+
limit: perPage + 1
|
|
2646
|
+
});
|
|
2647
|
+
const hasMore = rows.length > perPage;
|
|
2648
|
+
const data = hasMore ? rows.slice(0, perPage) : rows;
|
|
2649
|
+
const nextCursor = hasMore ? data[data.length - 1]?.[cursorColumn] ?? null : null;
|
|
2650
|
+
const prevCursor = cursor ?? null;
|
|
2651
|
+
return {
|
|
2652
|
+
data,
|
|
2653
|
+
meta: {
|
|
2654
|
+
per_page: perPage,
|
|
2655
|
+
next_cursor: nextCursor,
|
|
2656
|
+
prev_cursor: prevCursor,
|
|
2657
|
+
has_more: hasMore
|
|
2658
|
+
}
|
|
2659
|
+
};
|
|
2660
|
+
}
|
|
2661
|
+
async keysetPaginate(options) {
|
|
2662
|
+
const order = validateKeyset(this.table, options);
|
|
2663
|
+
if (options.joins?.length || options.groupBy || options.having || "offset" in options || "limit" in options)
|
|
2664
|
+
throw new TypeError("Keyset pagination does not accept joined/grouped/offset/limited queries.");
|
|
2665
|
+
if (options.select?.some((item) => item.as?.toLowerCase().startsWith(KEYSET_ALIAS_PREFIX)))
|
|
2666
|
+
throw new TypeError("Projection conflicts with reserved keyset aliases.");
|
|
2667
|
+
const filters = [...options.whereNodes ?? []];
|
|
2668
|
+
if (options.where)
|
|
2669
|
+
filters.push({ kind: "and", where: options.where });
|
|
2670
|
+
const rows = await this.findAll({
|
|
2671
|
+
...options,
|
|
2672
|
+
where: {},
|
|
2673
|
+
whereNodes: [
|
|
2674
|
+
{ kind: "and", group: filters },
|
|
2675
|
+
...keysetBoundary(order, options.cursor)
|
|
2676
|
+
],
|
|
2677
|
+
orderBy: order,
|
|
2678
|
+
limit: options.perPage + 1,
|
|
2679
|
+
select: [
|
|
2680
|
+
...options.select?.length ? options.select : this.table.columns.map((column) => ({
|
|
2681
|
+
kind: "column",
|
|
2682
|
+
table: this.table.name,
|
|
2683
|
+
column
|
|
2684
|
+
})),
|
|
2685
|
+
...order.map((item, i) => ({
|
|
2686
|
+
kind: "textColumn",
|
|
2687
|
+
table: this.table.name,
|
|
2688
|
+
column: item.column,
|
|
2689
|
+
as: `${KEYSET_ALIAS_PREFIX}${i}`
|
|
2690
|
+
}))
|
|
2691
|
+
]
|
|
2692
|
+
});
|
|
2693
|
+
const entries = rows.map((row) => {
|
|
2694
|
+
const record = { ...row };
|
|
2695
|
+
const values = order.map((_, i) => {
|
|
2696
|
+
const key = `${KEYSET_ALIAS_PREFIX}${i}`;
|
|
2697
|
+
const value = record[key];
|
|
2698
|
+
delete record[key];
|
|
2699
|
+
if (typeof value !== "string" || value.length > 4096)
|
|
2700
|
+
throw new TypeError("Keyset columns must contain non-null bounded scalar values.");
|
|
2701
|
+
return value;
|
|
2702
|
+
});
|
|
2703
|
+
return { record, values };
|
|
2704
|
+
});
|
|
2705
|
+
const hasMore = entries.length > options.perPage;
|
|
2706
|
+
const visible = entries.slice(0, options.perPage);
|
|
2707
|
+
const last = visible.at(-1);
|
|
2708
|
+
const nextCursor = hasMore && last ? { version: 1, order, values: last.values } : null;
|
|
2709
|
+
const data = visible.map(({ record }) => record);
|
|
2710
|
+
return {
|
|
2711
|
+
data,
|
|
2712
|
+
meta: {
|
|
2713
|
+
per_page: options.perPage,
|
|
2714
|
+
has_more: hasMore,
|
|
2715
|
+
next_cursor: nextCursor,
|
|
2716
|
+
prev_cursor: options.cursor ?? null
|
|
2717
|
+
}
|
|
2718
|
+
};
|
|
2719
|
+
}
|
|
2720
|
+
async findById(id) {
|
|
2721
|
+
return await this.firstOrNull({
|
|
2722
|
+
[this.table.primaryKey]: id
|
|
2723
|
+
});
|
|
2724
|
+
}
|
|
2725
|
+
async findByIdOrThrow(id, errorFactory) {
|
|
2726
|
+
const record = await this.findById(id);
|
|
2727
|
+
if (record) {
|
|
2728
|
+
return record;
|
|
2729
|
+
}
|
|
2730
|
+
throw errorFactory?.(id) ?? new Error(`${this.table.name} ${String(id)} was not found.`);
|
|
2731
|
+
}
|
|
2732
|
+
async findByIds(ids) {
|
|
2733
|
+
const uniqueIds = [...new Set(ids)];
|
|
2734
|
+
if (uniqueIds.length === 0) {
|
|
2735
|
+
return [];
|
|
2736
|
+
}
|
|
2737
|
+
return await this.findWhere({
|
|
2738
|
+
[this.table.primaryKey]: uniqueIds
|
|
2739
|
+
});
|
|
2740
|
+
}
|
|
2741
|
+
async firstOrNull(where, options = {}) {
|
|
2742
|
+
const [record] = await this.findAll({ ...options, where, limit: 1 });
|
|
2743
|
+
return record ?? null;
|
|
2744
|
+
}
|
|
2745
|
+
async upsert(values, conflictColumns, updateColumns) {
|
|
2746
|
+
return await withDatabaseErrorHandling(async () => {
|
|
2747
|
+
const { text, params } = buildUpsertQuery(this.table, values, conflictColumns, updateColumns);
|
|
2748
|
+
const [record] = await this.connection.unsafe(text, params);
|
|
2749
|
+
return record ?? null;
|
|
2750
|
+
});
|
|
2751
|
+
}
|
|
2752
|
+
async incrementById(id, column, amount = 1, extra = {}) {
|
|
2753
|
+
return await withDatabaseErrorHandling(async () => {
|
|
2754
|
+
const { text, params } = buildIncrementQuery(this.table, id, column, amount, extra);
|
|
2755
|
+
const [record] = await this.connection.unsafe(text, params);
|
|
2756
|
+
return record ?? null;
|
|
2757
|
+
});
|
|
2758
|
+
}
|
|
2759
|
+
async decrementById(id, column, amount = 1, extra = {}) {
|
|
2760
|
+
return await this.incrementById(id, column, -amount, extra);
|
|
2761
|
+
}
|
|
2762
|
+
async create(values) {
|
|
2763
|
+
this.assertTransactionalEvent("created");
|
|
2764
|
+
return await this.runModelMutation("created", () => withDatabaseErrorHandling(async () => {
|
|
2765
|
+
const { text, params } = buildInsertQuery(this.table, values);
|
|
2766
|
+
const [record] = await this.connection.unsafe(text, params);
|
|
2767
|
+
if (!record) {
|
|
2768
|
+
throw new Error(`Insert into ${this.table.name} did not return a record.`);
|
|
2769
|
+
}
|
|
2770
|
+
const entity = record;
|
|
2771
|
+
await dispatchModelEvent(modelEventName(this.table.name, "created"), entity);
|
|
2772
|
+
return entity;
|
|
2773
|
+
}));
|
|
2774
|
+
}
|
|
2775
|
+
async updateWhere(changes, options) {
|
|
2776
|
+
return this.executeConditionalWrite("update", changes, options);
|
|
2777
|
+
}
|
|
2778
|
+
async deleteWhere(options) {
|
|
2779
|
+
return this.executeConditionalWrite("delete", {}, options);
|
|
2780
|
+
}
|
|
2781
|
+
async updateWhereReturning(changes, columns, options) {
|
|
2782
|
+
const { text, params } = buildConditionalWriteQuery(this.table, "update", changes, options, columns);
|
|
2783
|
+
return withDatabaseErrorHandling(() => this.connection.unsafe(text, params));
|
|
2784
|
+
}
|
|
2785
|
+
async executeConditionalWrite(operation, changes, options) {
|
|
2786
|
+
const { text, params } = buildConditionalWriteQuery(this.table, operation, changes, options);
|
|
2787
|
+
return withDatabaseErrorHandling(async () => {
|
|
2788
|
+
const rows = await this.connection.unsafe(text, params);
|
|
2789
|
+
if (currentSqlDialect().driver !== "mysql")
|
|
2790
|
+
return rows.length;
|
|
2791
|
+
const affected = rows[0]?.affectedRows;
|
|
2792
|
+
if (!Number.isSafeInteger(affected) || affected === undefined || affected < 0)
|
|
2793
|
+
throw new Error("MySQL conditional writes require affectedRows metadata from the framework adapter.");
|
|
2794
|
+
return affected;
|
|
2795
|
+
});
|
|
2796
|
+
}
|
|
2797
|
+
async updateById(id, changes) {
|
|
2798
|
+
this.assertTransactionalEvent("updated");
|
|
2799
|
+
return await this.runModelMutation("updated", () => withDatabaseErrorHandling(async () => {
|
|
2800
|
+
const { text, params } = buildUpdateQuery(this.table, id, changes);
|
|
2801
|
+
const [record] = await this.connection.unsafe(text, params);
|
|
2802
|
+
const entity = record ?? null;
|
|
2803
|
+
if (entity) {
|
|
2804
|
+
await dispatchModelEvent(modelEventName(this.table.name, "updated"), entity);
|
|
2805
|
+
}
|
|
2806
|
+
return entity;
|
|
2807
|
+
}));
|
|
2808
|
+
}
|
|
2809
|
+
async updateByIdOrThrow(id, changes, errorFactory) {
|
|
2810
|
+
const record = await this.updateById(id, changes);
|
|
2811
|
+
if (record) {
|
|
2812
|
+
return record;
|
|
2813
|
+
}
|
|
2814
|
+
throw errorFactory?.(id) ?? new Error(`${this.table.name} ${String(id)} was not found.`);
|
|
2815
|
+
}
|
|
2816
|
+
async deleteById(id) {
|
|
2817
|
+
if (resolveSoftDeleteColumn(this.table)) {
|
|
2818
|
+
return await this.softDeleteById(id);
|
|
2819
|
+
}
|
|
2820
|
+
return await this.forceDeleteById(id);
|
|
2821
|
+
}
|
|
2822
|
+
async softDeleteById(id) {
|
|
2823
|
+
this.assertTransactionalEvent("deleted");
|
|
2824
|
+
return await this.runModelMutation("deleted", () => withDatabaseErrorHandling(async () => {
|
|
2825
|
+
const { text, params } = buildSoftDeleteByIdQuery(this.table, id, new Date);
|
|
2826
|
+
const [record] = await this.connection.unsafe(text, params);
|
|
2827
|
+
if (!record) {
|
|
2828
|
+
return false;
|
|
2829
|
+
}
|
|
2830
|
+
await dispatchModelEvent(modelEventName(this.table.name, "deleted"), record);
|
|
2831
|
+
return true;
|
|
2832
|
+
}));
|
|
2833
|
+
}
|
|
2834
|
+
async forceDeleteById(id) {
|
|
2835
|
+
this.assertTransactionalEvent("force-deleted");
|
|
2836
|
+
return await this.runModelMutation("force-deleted", () => withDatabaseErrorHandling(async () => {
|
|
2837
|
+
const { text, params } = buildDeleteByIdQuery(this.table, id);
|
|
2838
|
+
const [row] = await this.connection.unsafe(text, params);
|
|
2839
|
+
if (!row) {
|
|
2840
|
+
return false;
|
|
2841
|
+
}
|
|
2842
|
+
await dispatchModelEvent(modelEventName(this.table.name, "force-deleted"), {
|
|
2843
|
+
id
|
|
2844
|
+
});
|
|
2845
|
+
return true;
|
|
2846
|
+
}));
|
|
2847
|
+
}
|
|
2848
|
+
async restoreById(id) {
|
|
2849
|
+
this.assertTransactionalEvent("restored");
|
|
2850
|
+
return await this.runModelMutation("restored", () => withDatabaseErrorHandling(async () => {
|
|
2851
|
+
const { text, params } = buildRestoreByIdQuery(this.table, id);
|
|
2852
|
+
const [record] = await this.connection.unsafe(text, params);
|
|
2853
|
+
if (!record) {
|
|
2854
|
+
return null;
|
|
2855
|
+
}
|
|
2856
|
+
const entity = record;
|
|
2857
|
+
await dispatchModelEvent(modelEventName(this.table.name, "restored"), entity);
|
|
2858
|
+
return entity;
|
|
2859
|
+
}));
|
|
2860
|
+
}
|
|
2861
|
+
primaryKeySeekDirection(options) {
|
|
2862
|
+
if (options.joins?.length || options.groupBy || options.having || options.select?.length || options.offset !== undefined || options.limit !== undefined || options.lock) {
|
|
2863
|
+
return null;
|
|
2864
|
+
}
|
|
2865
|
+
const orders = listOrders(options.orderBy ?? this.table.defaultOrderBy);
|
|
2866
|
+
const order = orders[0];
|
|
2867
|
+
if (orders.length !== 1 || !order || order.nulls || order.column !== this.table.primaryKey) {
|
|
2868
|
+
return null;
|
|
2869
|
+
}
|
|
2870
|
+
return order.direction;
|
|
2871
|
+
}
|
|
2872
|
+
withConnection(connection) {
|
|
2873
|
+
const clone = Object.create(Object.getPrototypeOf(this));
|
|
2874
|
+
Object.assign(clone, this);
|
|
2875
|
+
clone.connection = connection;
|
|
2876
|
+
return clone;
|
|
2877
|
+
}
|
|
2878
|
+
getConnection() {
|
|
2879
|
+
return this.connection;
|
|
2880
|
+
}
|
|
2881
|
+
getTable() {
|
|
2882
|
+
return this.table;
|
|
2883
|
+
}
|
|
2884
|
+
query(where = {}) {
|
|
2885
|
+
return new RepositoryQuery(this, where);
|
|
2886
|
+
}
|
|
2887
|
+
async findWhere(where, options = {}) {
|
|
2888
|
+
return await this.findAll({ ...options, where });
|
|
2889
|
+
}
|
|
2890
|
+
async countWhere(where = {}, options = {}, whereNodes = []) {
|
|
2891
|
+
const { text, params } = buildCountQuery(this.table, where, options, whereNodes);
|
|
2892
|
+
const [row] = await this.connection.unsafe(text, params);
|
|
2893
|
+
return Number(row?.count ?? 0);
|
|
2894
|
+
}
|
|
2895
|
+
async averageColumn(column, where = {}) {
|
|
2896
|
+
const qualifiedColumn = qualifyColumn(this.table.name, column);
|
|
2897
|
+
return await this.averageExpression(`AVG(${qualifiedColumn})`, "value", where);
|
|
2898
|
+
}
|
|
2899
|
+
async averageExpression(expression, alias, where = {}) {
|
|
2900
|
+
const { text, params } = buildProjectionQuery(this.table, expression, alias, { where });
|
|
2901
|
+
const [row] = await this.connection.unsafe(text, params);
|
|
2902
|
+
return Number(row?.[alias] ?? 0);
|
|
2903
|
+
}
|
|
2904
|
+
async sumBy(groupColumn, column, options = {}) {
|
|
2905
|
+
const { whereNodes, ...queryOptions } = options;
|
|
2906
|
+
const { text, params } = buildGroupedSumQuery(this.table, groupColumn, column, queryOptions, whereNodes ?? []);
|
|
2907
|
+
return withDatabaseErrorHandling(() => this.connection.unsafe(text, params));
|
|
2908
|
+
}
|
|
2909
|
+
async sum(column, where = {}) {
|
|
2910
|
+
return await this.aggregateColumn("SUM", column, where);
|
|
2911
|
+
}
|
|
2912
|
+
async avg(column, where = {}) {
|
|
2913
|
+
return await this.aggregateColumn("AVG", column, where);
|
|
2914
|
+
}
|
|
2915
|
+
async min(column, where = {}) {
|
|
2916
|
+
return await this.aggregateColumn("MIN", column, where);
|
|
2917
|
+
}
|
|
2918
|
+
async max(column, where = {}) {
|
|
2919
|
+
return await this.aggregateColumn("MAX", column, where);
|
|
2920
|
+
}
|
|
2921
|
+
async aggregateColumn(fn, column, where) {
|
|
2922
|
+
const qualifiedColumn = qualifyColumn(this.table.name, column);
|
|
2923
|
+
return await this.averageExpression(`${fn}(${qualifiedColumn})`, "value", where);
|
|
2924
|
+
}
|
|
2925
|
+
async pluckNumberValues(expression, alias, options = {}) {
|
|
2926
|
+
const { text, params } = buildProjectionQuery(this.table, expression, alias, options);
|
|
2927
|
+
const rows = await this.connection.unsafe(text, params);
|
|
2928
|
+
return rows.flatMap((row) => {
|
|
2929
|
+
const value = row[alias];
|
|
2930
|
+
return value === null || value === undefined ? [] : [Number(value)];
|
|
2931
|
+
});
|
|
2932
|
+
}
|
|
2933
|
+
async countGroupedBy(column, where = {}) {
|
|
2934
|
+
const { text, params } = buildGroupedCountQuery(this.table, column, where);
|
|
2935
|
+
const rows = await this.connection.unsafe(text, params);
|
|
2936
|
+
return rows.map(({ value, count }) => ({
|
|
2937
|
+
value,
|
|
2938
|
+
count: Number(count)
|
|
2939
|
+
}));
|
|
2940
|
+
}
|
|
2941
|
+
async findByHasManyRelation(relation, parentId, options = {}) {
|
|
2942
|
+
return await this.findWhere({
|
|
2943
|
+
[relation.foreignKey]: parentId
|
|
2944
|
+
}, options);
|
|
2945
|
+
}
|
|
2946
|
+
async loadHasManyForParents(parents, relation, options = {}) {
|
|
2947
|
+
if (parents.length === 0) {
|
|
2948
|
+
return indexHasManyRelation(parents, [], relation);
|
|
2949
|
+
}
|
|
2950
|
+
const parentIds = [...new Set(parents.map((parent) => parent[relation.localKey]))];
|
|
2951
|
+
const children = await this.findWhere({
|
|
2952
|
+
[relation.foreignKey]: parentIds
|
|
2953
|
+
}, options);
|
|
2954
|
+
return indexHasManyRelation(parents, children, relation);
|
|
2955
|
+
}
|
|
2956
|
+
async findHasManyThrough(parentId, relation, options = {}) {
|
|
2957
|
+
const grouped = await this.loadHasManyThroughForParents([{ [relation.localKey]: parentId }], relation, options);
|
|
2958
|
+
return getByRelationKey(grouped, parentId) ?? [];
|
|
2959
|
+
}
|
|
2960
|
+
async loadHasManyThroughForParents(parents, relation, options = {}) {
|
|
2961
|
+
if (parents.length === 0) {
|
|
2962
|
+
return indexHasManyThroughRelation(parents, [], relation);
|
|
2963
|
+
}
|
|
2964
|
+
if (options.joins?.some((join) => join.table === relation.throughTable)) {
|
|
2965
|
+
throw new Error("A related model scope joins the through table twice; use an explicitly aliased correlated predicate instead.");
|
|
2966
|
+
}
|
|
2967
|
+
const parentIds = [...new Set(parents.map((parent) => parent[relation.localKey]))];
|
|
2968
|
+
const throughParentKey = relation.throughParentKey ?? "__through_parent_id";
|
|
2969
|
+
const farTable = this.table.name;
|
|
2970
|
+
const columns = this.table.columns.map((column) => `${qualifyColumn(farTable, column)}`).join(", ");
|
|
2971
|
+
const placeholders = parentIds.map((_, index) => currentSqlDialect().placeholder(index + 1)).join(", ");
|
|
2972
|
+
const { text: extraWhere, params: extraParams } = this.buildThroughWhere(options, parentIds);
|
|
2973
|
+
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}`;
|
|
2974
|
+
const children = await this.connection.unsafe(sql, [
|
|
2975
|
+
...parentIds,
|
|
2976
|
+
...extraParams
|
|
2977
|
+
]);
|
|
2978
|
+
return indexHasManyThroughRelation(parents, children, relation);
|
|
2979
|
+
}
|
|
2980
|
+
buildThroughWhere(options, parentIds) {
|
|
2981
|
+
const parameters = [...parentIds];
|
|
2982
|
+
const { clause } = buildQueryWhereClause(this.table, options, options.whereNodes, parameters);
|
|
2983
|
+
return {
|
|
2984
|
+
text: clause ? ` AND (${clause.replace(/^ WHERE /, "")})` : "",
|
|
2985
|
+
params: parameters.slice(parentIds.length)
|
|
2986
|
+
};
|
|
2987
|
+
}
|
|
2988
|
+
async loadBelongsToForParents(children, relation, parentRepository, options = {}) {
|
|
2989
|
+
if (children.length === 0) {
|
|
2990
|
+
return new Map;
|
|
2991
|
+
}
|
|
2992
|
+
const ownerIds = [...new Set(children.map((child) => child[relation.foreignKey]))];
|
|
2993
|
+
const parents = await parentRepository.withConnection(this.connection).findWhere({
|
|
2994
|
+
[relation.ownerKey]: ownerIds
|
|
2995
|
+
}, options);
|
|
2996
|
+
return indexBelongsToRelation(children, parents, relation);
|
|
2997
|
+
}
|
|
2998
|
+
async loadMorphManyForParents(parents, relation, options = {}) {
|
|
2999
|
+
if (parents.length === 0) {
|
|
3000
|
+
return indexMorphManyRelation(parents, [], relation);
|
|
3001
|
+
}
|
|
3002
|
+
const parentIds = [...new Set(parents.map((parent) => parent[relation.localKey]))];
|
|
3003
|
+
const children = await this.findWhere({
|
|
3004
|
+
[relation.morphTypeKey]: relation.morphType,
|
|
3005
|
+
[relation.morphIdKey]: parentIds
|
|
3006
|
+
}, options);
|
|
3007
|
+
return indexMorphManyRelation(parents, children, relation);
|
|
3008
|
+
}
|
|
3009
|
+
async loadMorphOneForParents(parents, relation, options = {}) {
|
|
3010
|
+
const grouped = await this.loadMorphManyForParents(parents, relation, options);
|
|
3011
|
+
const result = new Map;
|
|
3012
|
+
for (const parent of parents) {
|
|
3013
|
+
const matches = getByRelationKey(grouped, parent[relation.localKey]) ?? [];
|
|
3014
|
+
result.set(parent[relation.localKey], matches[0]);
|
|
3015
|
+
}
|
|
3016
|
+
return result;
|
|
3017
|
+
}
|
|
3018
|
+
async loadMorphToForChildren(children, relation, repositoriesByType, options = {}) {
|
|
3019
|
+
if (children.length === 0) {
|
|
3020
|
+
return new Map;
|
|
3021
|
+
}
|
|
3022
|
+
const idsByType = new Map;
|
|
3023
|
+
for (const child of children) {
|
|
3024
|
+
const morphType = String(child[relation.morphTypeKey]);
|
|
3025
|
+
const morphId = child[relation.morphIdKey];
|
|
3026
|
+
const ids = idsByType.get(morphType) ?? new Set;
|
|
3027
|
+
ids.add(morphId);
|
|
3028
|
+
idsByType.set(morphType, ids);
|
|
3029
|
+
}
|
|
3030
|
+
const parentsByType = new Map;
|
|
3031
|
+
await Promise.all([...idsByType.entries()].map(async ([morphType, ids]) => {
|
|
3032
|
+
const repository = repositoriesByType.get(morphType);
|
|
3033
|
+
if (!repository) {
|
|
3034
|
+
return;
|
|
3035
|
+
}
|
|
3036
|
+
const ownerKey = repository.getTable().primaryKey;
|
|
3037
|
+
const parents = await repository.withConnection(this.connection).findWhere({
|
|
3038
|
+
[ownerKey]: [...ids]
|
|
3039
|
+
}, options);
|
|
3040
|
+
const indexed = new Map;
|
|
3041
|
+
for (const parent of parents) {
|
|
3042
|
+
indexed.set(relationMatchKey(parent[ownerKey]), parent);
|
|
3043
|
+
}
|
|
3044
|
+
parentsByType.set(morphType, indexed);
|
|
3045
|
+
}));
|
|
3046
|
+
return indexMorphToRelation(children, parentsByType, relation);
|
|
3047
|
+
}
|
|
3048
|
+
async loadBelongsToManyForParents(parents, relation, relatedRepository, options = {}) {
|
|
3049
|
+
if (parents.length === 0) {
|
|
3050
|
+
return indexBelongsToManyRelation(parents, [], [], relation);
|
|
3051
|
+
}
|
|
3052
|
+
const parentIds = [...new Set(parents.map((parent) => parent[relation.parentKey]))];
|
|
3053
|
+
const placeholders = parentIds.map((_, index) => currentSqlDialect().placeholder(index + 1)).join(", ");
|
|
3054
|
+
const pivotRows = await this.connection.unsafe(`SELECT * FROM ${relation.pivotTable} WHERE ${String(relation.foreignPivotKey)} IN (${placeholders})`, parentIds);
|
|
3055
|
+
if (pivotRows.length === 0) {
|
|
3056
|
+
return indexBelongsToManyRelation(parents, [], [], relation);
|
|
3057
|
+
}
|
|
3058
|
+
const relatedIds = [
|
|
3059
|
+
...new Set(pivotRows.map((row) => row[relation.relatedPivotKey]))
|
|
3060
|
+
];
|
|
3061
|
+
const relatedRows = await relatedRepository.withConnection(this.connection).findWhere({
|
|
3062
|
+
[relation.relatedKey]: relatedIds
|
|
3063
|
+
}, options);
|
|
3064
|
+
return indexBelongsToManyRelation(parents, pivotRows, relatedRows, relation);
|
|
3065
|
+
}
|
|
3066
|
+
}
|
|
3067
|
+
|
|
3068
|
+
// ../../src/core/database/table.ts
|
|
3069
|
+
function defineTable(definition) {
|
|
3070
|
+
return definition;
|
|
3071
|
+
}
|
|
3072
|
+
|
|
935
3073
|
// ../../src/core/tenant/resolveTenant.ts
|
|
936
3074
|
init_tenancyConfig();
|
|
937
|
-
|
|
3075
|
+
var tenants = new BaseRepository(defineTable({
|
|
3076
|
+
name: "tenant",
|
|
3077
|
+
primaryKey: "id",
|
|
3078
|
+
columns: ["id", "slug"]
|
|
3079
|
+
}));
|
|
938
3080
|
async function resolveTenant(tenantId) {
|
|
939
3081
|
if (!isTenancyEnabled()) {
|
|
940
3082
|
return {
|
|
@@ -942,13 +3084,7 @@ async function resolveTenant(tenantId) {
|
|
|
942
3084
|
slug: "default"
|
|
943
3085
|
};
|
|
944
3086
|
}
|
|
945
|
-
const
|
|
946
|
-
SELECT id, slug
|
|
947
|
-
FROM tenant
|
|
948
|
-
WHERE id = ${tenantId}
|
|
949
|
-
LIMIT 1
|
|
950
|
-
`;
|
|
951
|
-
const row = rows[0];
|
|
3087
|
+
const row = await tenants.findById(tenantId);
|
|
952
3088
|
return row ? { id: row.id, slug: row.slug } : null;
|
|
953
3089
|
}
|
|
954
3090
|
|