@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.
@@ -42,26 +42,81 @@ var init_connectionContext = __esm(() => {
42
42
  activeConnection = createAsyncContextStore("@getstrata/databaseConnectionContext");
43
43
  });
44
44
 
45
- // ../../src/core/tenant/tenancyConfig.ts
46
- function readTenancyDriver(env = process.env) {
47
- const raw = env.TENANCY_DRIVER?.trim();
48
- if (raw === undefined || raw === "") {
49
- return "rls";
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
- if (TENANCY_DRIVERS.includes(raw)) {
52
- return raw;
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 isTenancyEnabled(env = process.env) {
57
- return readTenancyDriver(env) !== "none";
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
- function isRlsTenancy(env = process.env) {
60
- return readTenancyDriver(env) === "rls";
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
- var TENANCY_DRIVERS;
63
- var init_tenancyConfig = __esm(() => {
64
- TENANCY_DRIVERS = ["rls", "column", "none"];
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/databaseTenantContext.ts
759
- import { getDefaultDatabasePool as getDefaultDatabasePool2 } from "@getstrata/core/database/defaultConnection";
760
- import { AsyncLocalStorage as AsyncLocalStorage3 } from "async_hooks";
761
- async function applyBypassToTransaction(transaction, bypass) {
762
- await transaction.unsafe(`SELECT set_config('app.bypass_rls', $1, true)`, [
763
- bypass ? "true" : "false"
764
- ]);
765
- }
766
- function stringifyBypassIdentifier(identifier) {
767
- if (typeof identifier === "number") {
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
- import { repositoryConnection as db } from "@getstrata/core/database/repositoryConnection";
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 rows = await db`
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