@getstrata/core 0.5.52 → 0.5.53

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.
@@ -0,0 +1,655 @@
1
+ // @bun
2
+ // ../../src/core/database/query.ts
3
+ function quoteIdentifier(identifier) {
4
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(identifier)) {
5
+ throw new Error(`Invalid SQL identifier: ${identifier}`);
6
+ }
7
+ return `"${identifier}"`;
8
+ }
9
+ function qualifyColumn(tableName, column) {
10
+ return `${quoteIdentifier(tableName)}.${quoteIdentifier(column)}`;
11
+ }
12
+ function resolveQualifiedColumn(defaultTable, columnName) {
13
+ if (columnName.includes(".")) {
14
+ const [table, column] = columnName.split(".", 2);
15
+ if (!table || !column) {
16
+ throw new Error(`Invalid qualified column: ${columnName}`);
17
+ }
18
+ return qualifyColumn(table, column);
19
+ }
20
+ return qualifyColumn(defaultTable, columnName);
21
+ }
22
+ function parseQualifiedColumn(reference) {
23
+ const [table, column] = reference.split(".", 2);
24
+ if (!table || !column) {
25
+ throw new Error(`Join columns must be qualified as table.column: ${reference}`);
26
+ }
27
+ return { table, column };
28
+ }
29
+ function normalizeDirection(direction = "ASC") {
30
+ return direction.toUpperCase() === "DESC" ? "DESC" : "ASC";
31
+ }
32
+ function isQueryOperator(value) {
33
+ return value !== null && !Array.isArray(value) && !(value instanceof Date) && typeof value === "object";
34
+ }
35
+ function pushParam(values, value) {
36
+ values.push(value);
37
+ return `$${values.length}`;
38
+ }
39
+ function buildInClause(column, values, params) {
40
+ if (values.length === 0) {
41
+ return "1 = 0";
42
+ }
43
+ const placeholders = values.map((value) => pushParam(params, value)).join(", ");
44
+ return `${column} IN (${placeholders})`;
45
+ }
46
+ function buildOperatorClauses(column, operator, params) {
47
+ const clauses = [];
48
+ if (operator.isNull === true) {
49
+ clauses.push(`${column} IS NULL`);
50
+ }
51
+ if (operator.isNull === false) {
52
+ clauses.push(`${column} IS NOT NULL`);
53
+ }
54
+ if (operator.eq !== undefined) {
55
+ if (operator.eq === null) {
56
+ clauses.push(`${column} IS NULL`);
57
+ } else {
58
+ clauses.push(`${column} = ${pushParam(params, operator.eq)}`);
59
+ }
60
+ }
61
+ if (operator.in !== undefined) {
62
+ clauses.push(buildInClause(column, operator.in, params));
63
+ }
64
+ if (operator.gt !== undefined) {
65
+ clauses.push(`${column} > ${pushParam(params, operator.gt)}`);
66
+ }
67
+ if (operator.gte !== undefined) {
68
+ clauses.push(`${column} >= ${pushParam(params, operator.gte)}`);
69
+ }
70
+ if (operator.lt !== undefined) {
71
+ clauses.push(`${column} < ${pushParam(params, operator.lt)}`);
72
+ }
73
+ if (operator.lte !== undefined) {
74
+ clauses.push(`${column} <= ${pushParam(params, operator.lte)}`);
75
+ }
76
+ if (operator.ilike !== undefined) {
77
+ clauses.push(`${column} ILIKE ${pushParam(params, `%${operator.ilike}%`)}`);
78
+ }
79
+ if (operator.tsMatch !== undefined) {
80
+ clauses.push(`${column} @@ plainto_tsquery('english', ${pushParam(params, operator.tsMatch)})`);
81
+ }
82
+ return clauses;
83
+ }
84
+ function appendWhereParts(tableName, where, params) {
85
+ const clauses = [];
86
+ for (const [columnName, filterValue] of Object.entries(where)) {
87
+ if (filterValue === undefined) {
88
+ continue;
89
+ }
90
+ const column = resolveQualifiedColumn(tableName, columnName);
91
+ if (Array.isArray(filterValue)) {
92
+ clauses.push(buildInClause(column, filterValue, params));
93
+ continue;
94
+ }
95
+ if (isQueryOperator(filterValue)) {
96
+ clauses.push(...buildOperatorClauses(column, filterValue, params));
97
+ continue;
98
+ }
99
+ if (filterValue === null) {
100
+ clauses.push(`${column} IS NULL`);
101
+ continue;
102
+ }
103
+ clauses.push(`${column} = ${pushParam(params, filterValue)}`);
104
+ }
105
+ return clauses.join(" AND ");
106
+ }
107
+ function buildWhereClause(tableName, where = {}) {
108
+ const params = [];
109
+ const body = appendWhereParts(tableName, where, params);
110
+ return {
111
+ clause: body.length > 0 ? ` WHERE ${body}` : "",
112
+ params
113
+ };
114
+ }
115
+ function buildWhereNodeClause(tableName, node, params) {
116
+ if ("where" in node) {
117
+ return appendWhereParts(tableName, node.where, params);
118
+ }
119
+ const grouped = buildWhereGroupClause(tableName, node.group, params);
120
+ if (!grouped) {
121
+ return "";
122
+ }
123
+ return grouped.includes(" OR ") ? `(${grouped})` : grouped;
124
+ }
125
+ function buildWhereGroupClause(tableName, nodes, params) {
126
+ let result = "";
127
+ for (const node of nodes) {
128
+ const part = buildWhereNodeClause(tableName, node, params);
129
+ if (!part) {
130
+ continue;
131
+ }
132
+ if (!result) {
133
+ result = part;
134
+ continue;
135
+ }
136
+ result = node.kind === "or" ? `${result} OR ${part}` : `${result} AND ${part}`;
137
+ }
138
+ if (!result) {
139
+ return "";
140
+ }
141
+ return result;
142
+ }
143
+ function buildAdvancedWhereClause(tableName, where = {}, whereNodes = [], params = []) {
144
+ const nodes = [];
145
+ if (Object.keys(where).length > 0) {
146
+ nodes.push({ kind: "and", where });
147
+ }
148
+ nodes.push(...whereNodes);
149
+ const combined = buildWhereGroupClause(tableName, nodes, params);
150
+ return {
151
+ clause: combined ? ` WHERE ${combined}` : "",
152
+ params
153
+ };
154
+ }
155
+ function resolveSoftDeleteColumn(table) {
156
+ if (!table.softDeletes) {
157
+ return null;
158
+ }
159
+ if (table.softDeletes === true) {
160
+ return "deleted_at";
161
+ }
162
+ return table.softDeletes.column ?? "deleted_at";
163
+ }
164
+ function appendSoftDeleteScope(table, options, clauses) {
165
+ const column = resolveSoftDeleteColumn(table);
166
+ if (!column) {
167
+ return;
168
+ }
169
+ const qualifiedColumn = qualifyColumn(table.name, column);
170
+ if (options.onlyTrashed) {
171
+ clauses.push(`${qualifiedColumn} IS NOT NULL`);
172
+ return;
173
+ }
174
+ if (!options.withTrashed) {
175
+ clauses.push(`${qualifiedColumn} IS NULL`);
176
+ }
177
+ }
178
+ function buildQueryWhereClause(table, options = {}, whereNodes = [], params = []) {
179
+ const { clause, params: whereParams } = buildAdvancedWhereClause(table.name, options.where ?? {}, whereNodes, params);
180
+ const softDeleteClauses = [];
181
+ appendSoftDeleteScope(table, options, softDeleteClauses);
182
+ if (softDeleteClauses.length === 0) {
183
+ return { clause, params: whereParams };
184
+ }
185
+ const base = clause.replace(/^ WHERE /, "");
186
+ const scope = softDeleteClauses.join(" AND ");
187
+ return {
188
+ clause: base ? ` WHERE (${base}) AND ${scope}` : ` WHERE ${scope}`,
189
+ params: whereParams
190
+ };
191
+ }
192
+ function isQueryOrder(value) {
193
+ return "column" in value;
194
+ }
195
+ function normalizeOrderBy(orderBy) {
196
+ if (!orderBy) {
197
+ return [];
198
+ }
199
+ if (Array.isArray(orderBy)) {
200
+ return orderBy;
201
+ }
202
+ if (isQueryOrder(orderBy)) {
203
+ return [orderBy];
204
+ }
205
+ return Object.entries(orderBy).map(([column, direction]) => ({
206
+ column,
207
+ direction
208
+ }));
209
+ }
210
+ function buildOrderByClause(tableName, orderBy) {
211
+ const parts = normalizeOrderBy(orderBy).map(({ column, direction }) => {
212
+ return `${resolveQualifiedColumn(tableName, column)} ${normalizeDirection(direction)}`;
213
+ });
214
+ return parts.length > 0 ? ` ORDER BY ${parts.join(", ")}` : "";
215
+ }
216
+ function buildGroupByClause(tableName, groupBy) {
217
+ if (!groupBy) {
218
+ return "";
219
+ }
220
+ const columns = (Array.isArray(groupBy) ? groupBy : [groupBy]).map((column) => String(column));
221
+ const parts = columns.map((column) => resolveQualifiedColumn(tableName, column));
222
+ return parts.length > 0 ? ` GROUP BY ${parts.join(", ")}` : "";
223
+ }
224
+ function buildHavingClause(tableName, having, params) {
225
+ if (!having) {
226
+ return "";
227
+ }
228
+ const body = appendWhereParts(tableName, having, params);
229
+ return body.length > 0 ? ` HAVING ${body}` : "";
230
+ }
231
+ function buildJoinClause(joins = []) {
232
+ return joins.map((join) => {
233
+ const joinType = join.type === "left" ? "LEFT JOIN" : "INNER JOIN";
234
+ const onClause = join.on.map(({ left, right }) => `${qualifyColumn(left.table, left.column)} = ${qualifyColumn(right.table, right.column)}`).join(" AND ");
235
+ return ` ${joinType} ${quoteIdentifier(join.table)} ON ${onClause}`;
236
+ }).join("");
237
+ }
238
+ function buildLimitClause(limit) {
239
+ if (limit === undefined) {
240
+ return "";
241
+ }
242
+ if (!Number.isInteger(limit) || limit <= 0) {
243
+ throw new Error("Query limit must be a positive integer.");
244
+ }
245
+ return ` LIMIT ${limit}`;
246
+ }
247
+ function buildOffsetClause(offset) {
248
+ if (offset === undefined) {
249
+ return "";
250
+ }
251
+ if (!Number.isInteger(offset) || offset < 0) {
252
+ throw new Error("Query offset must be a non-negative integer.");
253
+ }
254
+ return ` OFFSET ${offset}`;
255
+ }
256
+ function buildReturningColumns(table) {
257
+ return table.columns.map((column) => qualifyColumn(table.name, column)).join(", ");
258
+ }
259
+ function buildSelectList(table, select, params = []) {
260
+ if (!select || select.length === 0) {
261
+ return buildReturningColumns(table);
262
+ }
263
+ return select.map((item) => {
264
+ if (item.kind === "column") {
265
+ const column2 = qualifyColumn(item.table, item.column);
266
+ return item.as ? `${column2} AS ${quoteIdentifier(item.as)}` : column2;
267
+ }
268
+ if (item.kind === "literalText") {
269
+ return `${pushParam(params, item.value)}::text AS ${quoteIdentifier(item.as)}`;
270
+ }
271
+ const column = qualifyColumn(item.table, item.column);
272
+ const placeholder = pushParam(params, item.query);
273
+ return `ts_rank(${column}, plainto_tsquery('english', ${placeholder})) AS ${quoteIdentifier(item.as)}`;
274
+ }).join(", ");
275
+ }
276
+ function getDefinedColumnEntries(table, values, options = {}) {
277
+ const record = values;
278
+ const excluded = new Set(options.exclude ?? []);
279
+ return table.columns.flatMap((column) => {
280
+ if (excluded.has(column) || !Object.hasOwn(record, column)) {
281
+ return [];
282
+ }
283
+ const value = record[column];
284
+ if (value === undefined) {
285
+ return [];
286
+ }
287
+ return [[column, value]];
288
+ });
289
+ }
290
+ function buildSelectQuery(table, options = {}, whereNodes = []) {
291
+ const params = [];
292
+ const columns = buildSelectList(table, options.select, params);
293
+ const { clause } = buildQueryWhereClause(table, options, whereNodes, params);
294
+ const joins = buildJoinClause(options.joins);
295
+ const groupBy = buildGroupByClause(table.name, options.groupBy);
296
+ const havingClause = buildHavingClause(table.name, options.having, params);
297
+ const orderBy = buildOrderByClause(table.name, options.orderBy ?? table.defaultOrderBy);
298
+ const limit = buildLimitClause(options.limit);
299
+ const offset = buildOffsetClause(options.offset);
300
+ return {
301
+ text: `SELECT ${columns} FROM ${quoteIdentifier(table.name)}${joins}${clause}${groupBy}${havingClause}${orderBy}${limit}${offset}`,
302
+ params
303
+ };
304
+ }
305
+ function buildCountQuery(table, where = {}, options = {}, whereNodes = []) {
306
+ const params = [];
307
+ const { clause, params: whereParams } = buildQueryWhereClause(table, {
308
+ where,
309
+ withTrashed: options.withTrashed,
310
+ onlyTrashed: options.onlyTrashed
311
+ }, whereNodes);
312
+ params.push(...whereParams);
313
+ const joins = buildJoinClause(options.joins);
314
+ const groupBy = buildGroupByClause(table.name, options.groupBy);
315
+ return {
316
+ text: `SELECT COUNT(*) AS count FROM ${quoteIdentifier(table.name)}${joins}${clause}${groupBy}`,
317
+ params
318
+ };
319
+ }
320
+ function buildProjectionQuery(table, expression, alias, options = {}, whereNodes = []) {
321
+ assertSafeProjectionExpression(expression);
322
+ const params = [];
323
+ const { clause, params: whereParams } = buildQueryWhereClause(table, options, whereNodes);
324
+ params.push(...whereParams);
325
+ const joins = buildJoinClause(options.joins);
326
+ const groupBy = buildGroupByClause(table.name, options.groupBy);
327
+ const orderBy = buildOrderByClause(table.name, options.orderBy);
328
+ const limit = buildLimitClause(options.limit);
329
+ return {
330
+ text: `SELECT ${expression} AS ${quoteIdentifier(alias)} FROM ${quoteIdentifier(table.name)}${joins}${clause}${groupBy}${orderBy}${limit}`,
331
+ params
332
+ };
333
+ }
334
+ var SAFE_PROJECTION_EXPRESSION = /^(COUNT\(\*\)|(?:"[\w_]+"(?:\."[\w_]+")?)|(?:AVG|SUM|MIN|MAX)\((?:"[\w_]+"(?:\."[\w_]+")?)\))$/;
335
+ function assertSafeProjectionExpression(expression) {
336
+ if (!SAFE_PROJECTION_EXPRESSION.test(expression.trim())) {
337
+ throw new Error(`Unsafe projection expression: ${expression}`);
338
+ }
339
+ }
340
+ function buildGroupedCountQuery(table, column, where = {}, options = {}) {
341
+ const qualifiedColumn = qualifyColumn(table.name, column);
342
+ const { clause, params } = buildQueryWhereClause(table, {
343
+ where,
344
+ ...options
345
+ });
346
+ return {
347
+ text: `SELECT ${qualifiedColumn} AS ${quoteIdentifier("value")}, COUNT(*) AS ${quoteIdentifier("count")} FROM ${quoteIdentifier(table.name)}${clause} GROUP BY ${qualifiedColumn} ORDER BY ${qualifiedColumn} ASC`,
348
+ params
349
+ };
350
+ }
351
+ function buildInsertQuery(table, values) {
352
+ const entries = getDefinedColumnEntries(table, values);
353
+ if (entries.length === 0) {
354
+ throw new Error(`Cannot insert into ${table.name} without any column values.`);
355
+ }
356
+ const params = [];
357
+ const columns = entries.map(([column]) => quoteIdentifier(column)).join(", ");
358
+ const placeholders = entries.map(([, value]) => pushParam(params, value)).join(", ");
359
+ const returningColumns = buildReturningColumns(table);
360
+ return {
361
+ text: `INSERT INTO ${quoteIdentifier(table.name)} (${columns}) VALUES (${placeholders}) RETURNING ${returningColumns}`,
362
+ params
363
+ };
364
+ }
365
+ function buildUpdateQuery(table, id, changes) {
366
+ const entries = getDefinedColumnEntries(table, changes, {
367
+ exclude: [table.primaryKey]
368
+ });
369
+ if (entries.length === 0) {
370
+ throw new Error(`Cannot update ${table.name} without any changed column values.`);
371
+ }
372
+ const params = [];
373
+ const setClause = entries.map(([column, value]) => `${quoteIdentifier(column)} = ${pushParam(params, value)}`).join(", ");
374
+ const primaryKeyPlaceholder = pushParam(params, id);
375
+ const returningColumns = buildReturningColumns(table);
376
+ const scopeClauses = [];
377
+ appendSoftDeleteScope(table, {}, scopeClauses);
378
+ const scopeSuffix = scopeClauses.length > 0 ? ` AND ${scopeClauses.join(" AND ")}` : "";
379
+ return {
380
+ text: `UPDATE ${quoteIdentifier(table.name)} SET ${setClause} WHERE ${quoteIdentifier(table.primaryKey)} = ${primaryKeyPlaceholder}${scopeSuffix} RETURNING ${returningColumns}`,
381
+ params
382
+ };
383
+ }
384
+ function buildSoftDeleteByIdQuery(table, id, deletedAt) {
385
+ const deletedAtColumn = resolveSoftDeleteColumn(table);
386
+ if (!deletedAtColumn) {
387
+ throw new Error(`Table ${table.name} does not support soft deletes.`);
388
+ }
389
+ const returningColumns = buildReturningColumns(table);
390
+ const scopeClauses = [];
391
+ appendSoftDeleteScope(table, {}, scopeClauses);
392
+ const scopeSuffix = scopeClauses.length > 0 ? ` AND ${scopeClauses.join(" AND ")}` : "";
393
+ return {
394
+ text: `UPDATE ${quoteIdentifier(table.name)} SET ${quoteIdentifier(deletedAtColumn)} = $1 WHERE ${quoteIdentifier(table.primaryKey)} = $2${scopeSuffix} RETURNING ${returningColumns}`,
395
+ params: [deletedAt, id]
396
+ };
397
+ }
398
+ function buildRestoreByIdQuery(table, id) {
399
+ const deletedAtColumn = resolveSoftDeleteColumn(table);
400
+ if (!deletedAtColumn) {
401
+ throw new Error(`Table ${table.name} does not support soft deletes.`);
402
+ }
403
+ const returningColumns = buildReturningColumns(table);
404
+ return {
405
+ text: `UPDATE ${quoteIdentifier(table.name)} SET ${quoteIdentifier(deletedAtColumn)} = $1 WHERE ${quoteIdentifier(table.primaryKey)} = $2 AND ${qualifyColumn(table.name, deletedAtColumn)} IS NOT NULL RETURNING ${returningColumns}`,
406
+ params: [null, id]
407
+ };
408
+ }
409
+ function buildDeleteByIdQuery(table, id) {
410
+ return {
411
+ text: `DELETE FROM ${quoteIdentifier(table.name)} WHERE ${quoteIdentifier(table.primaryKey)} = $1 RETURNING ${quoteIdentifier(table.primaryKey)} AS ${quoteIdentifier("deleted_id")}`,
412
+ params: [id]
413
+ };
414
+ }
415
+
416
+ // ../../src/core/database/whereBuilder.ts
417
+ class WhereBuilder {
418
+ nodes = [];
419
+ where(where) {
420
+ this.nodes.push({ kind: "and", where });
421
+ return this;
422
+ }
423
+ orWhere(where) {
424
+ this.nodes.push({ kind: "or", where });
425
+ return this;
426
+ }
427
+ whereGroup(fn) {
428
+ const nested = new WhereBuilder;
429
+ fn(nested);
430
+ if (nested.nodes.length > 0) {
431
+ this.nodes.push({ kind: "and", group: nested.nodes });
432
+ }
433
+ return this;
434
+ }
435
+ orWhereGroup(fn) {
436
+ const nested = new WhereBuilder;
437
+ fn(nested);
438
+ if (nested.nodes.length > 0) {
439
+ this.nodes.push({ kind: "or", group: nested.nodes });
440
+ }
441
+ return this;
442
+ }
443
+ }
444
+
445
+ // ../../src/core/database/repositoryQuery.ts
446
+ class RepositoryQuery {
447
+ repository;
448
+ whereClause;
449
+ queryOptions;
450
+ eagerLoads = [];
451
+ whereNodes = [];
452
+ constructor(repository, whereClause = {}, queryOptions = {}) {
453
+ this.repository = repository;
454
+ this.whereClause = whereClause;
455
+ this.queryOptions = queryOptions;
456
+ }
457
+ where(input) {
458
+ if (typeof input === "function") {
459
+ const builder = new WhereBuilder;
460
+ input(builder);
461
+ this.whereNodes.push(...builder.nodes);
462
+ return this;
463
+ }
464
+ this.whereClause = { ...this.whereClause, ...input };
465
+ return this;
466
+ }
467
+ orWhere(input) {
468
+ if (typeof input === "function") {
469
+ const builder = new WhereBuilder;
470
+ input(builder);
471
+ if (builder.nodes.length > 0) {
472
+ this.whereNodes.push({ kind: "or", group: builder.nodes });
473
+ }
474
+ return this;
475
+ }
476
+ this.whereNodes.push({ kind: "or", where: input });
477
+ return this;
478
+ }
479
+ orderBy(orderBy) {
480
+ this.queryOptions = { ...this.queryOptions, orderBy };
481
+ return this;
482
+ }
483
+ limit(limit) {
484
+ this.queryOptions = { ...this.queryOptions, limit };
485
+ return this;
486
+ }
487
+ offset(offset) {
488
+ this.queryOptions = { ...this.queryOptions, offset };
489
+ return this;
490
+ }
491
+ join(left, right) {
492
+ return this.addJoin("inner", left, right);
493
+ }
494
+ leftJoin(left, right) {
495
+ return this.addJoin("left", left, right);
496
+ }
497
+ groupBy(groupBy) {
498
+ this.queryOptions = { ...this.queryOptions, groupBy };
499
+ return this;
500
+ }
501
+ having(having) {
502
+ this.queryOptions = { ...this.queryOptions, having };
503
+ return this;
504
+ }
505
+ withHasMany(as, relation, childRepository, options = {}) {
506
+ this.eagerLoads.push({
507
+ kind: "hasMany",
508
+ as,
509
+ relation,
510
+ repository: childRepository,
511
+ options
512
+ });
513
+ return this;
514
+ }
515
+ withBelongsTo(as, relation, parentRepository, options = {}) {
516
+ this.eagerLoads.push({
517
+ kind: "belongsTo",
518
+ as,
519
+ relation,
520
+ repository: parentRepository,
521
+ options
522
+ });
523
+ return this;
524
+ }
525
+ withMorphMany(as, relation, childRepository, options = {}) {
526
+ this.eagerLoads.push({
527
+ kind: "morphMany",
528
+ as,
529
+ relation,
530
+ repository: childRepository,
531
+ options
532
+ });
533
+ return this;
534
+ }
535
+ withMorphOne(as, relation, childRepository, options = {}) {
536
+ this.eagerLoads.push({
537
+ kind: "morphOne",
538
+ as,
539
+ relation,
540
+ repository: childRepository,
541
+ options
542
+ });
543
+ return this;
544
+ }
545
+ withMorphTo(as, relation, repositoriesByType, options = {}) {
546
+ this.eagerLoads.push({
547
+ kind: "morphTo",
548
+ as,
549
+ relation,
550
+ repository: this.repository,
551
+ morphRepositories: repositoriesByType,
552
+ options
553
+ });
554
+ return this;
555
+ }
556
+ async get() {
557
+ const rows = await this.repository.findAll(this.buildOptions());
558
+ return await this.attach(rows);
559
+ }
560
+ async first() {
561
+ const rows = await this.get();
562
+ return rows[0] ?? null;
563
+ }
564
+ async paginate(options) {
565
+ return await this.repository.paginate({
566
+ ...this.buildOptions(),
567
+ page: options.page,
568
+ perPage: options.perPage
569
+ });
570
+ }
571
+ buildOptions() {
572
+ return {
573
+ ...this.queryOptions,
574
+ where: this.whereClause,
575
+ whereNodes: this.whereNodes
576
+ };
577
+ }
578
+ addJoin(type, left, right) {
579
+ const leftRef = parseQualifiedColumn(left);
580
+ const rightRef = parseQualifiedColumn(right);
581
+ const table = type === "inner" ? rightRef.table : rightRef.table;
582
+ const joins = this.queryOptions.joins ?? [];
583
+ const existing = joins.find((join) => join.table === table && join.type === type);
584
+ if (existing) {
585
+ existing.on.push({ left: leftRef, right: rightRef });
586
+ return this;
587
+ }
588
+ this.queryOptions = {
589
+ ...this.queryOptions,
590
+ joins: [
591
+ ...joins,
592
+ {
593
+ type,
594
+ table,
595
+ on: [{ left: leftRef, right: rightRef }]
596
+ }
597
+ ]
598
+ };
599
+ return this;
600
+ }
601
+ async attach(rows) {
602
+ if (rows.length === 0 || this.eagerLoads.length === 0) {
603
+ return rows.map((row) => ({ ...row }));
604
+ }
605
+ let result = rows.map((row) => ({ ...row }));
606
+ for (const load of this.eagerLoads) {
607
+ if (load.kind === "hasMany") {
608
+ const relation2 = load.relation;
609
+ const grouped2 = await load.repository.withConnection(this.repository.getConnection()).loadHasManyForParents(rows, relation2, load.options);
610
+ result = result.map((row) => ({
611
+ ...row,
612
+ [load.as]: grouped2.get(row[relation2.localKey]) ?? []
613
+ }));
614
+ continue;
615
+ }
616
+ if (load.kind === "morphMany") {
617
+ const relation2 = load.relation;
618
+ const grouped2 = await load.repository.withConnection(this.repository.getConnection()).loadMorphManyForParents(rows, relation2, load.options);
619
+ result = result.map((row) => ({
620
+ ...row,
621
+ [load.as]: grouped2.get(row[relation2.localKey]) ?? []
622
+ }));
623
+ continue;
624
+ }
625
+ if (load.kind === "morphOne") {
626
+ const relation2 = load.relation;
627
+ const grouped2 = await load.repository.withConnection(this.repository.getConnection()).loadMorphOneForParents(rows, relation2, load.options);
628
+ result = result.map((row) => ({
629
+ ...row,
630
+ [load.as]: grouped2.get(row[relation2.localKey])
631
+ }));
632
+ continue;
633
+ }
634
+ if (load.kind === "morphTo") {
635
+ const relation2 = load.relation;
636
+ const grouped2 = await this.repository.loadMorphToForChildren(rows, relation2, load.morphRepositories ?? new Map, load.options);
637
+ result = result.map((row) => ({
638
+ ...row,
639
+ [load.as]: grouped2.get(row[relation2.morphIdKey])
640
+ }));
641
+ continue;
642
+ }
643
+ const relation = load.relation;
644
+ const grouped = await this.repository.loadBelongsToForParents(rows, relation, load.repository, load.options);
645
+ result = result.map((row) => ({
646
+ ...row,
647
+ [load.as]: grouped.get(row[relation.foreignKey])
648
+ }));
649
+ }
650
+ return result;
651
+ }
652
+ }
653
+ export {
654
+ RepositoryQuery
655
+ };
@@ -0,0 +1,32 @@
1
+ // @bun
2
+ // ../../src/core/database/whereBuilder.ts
3
+ class WhereBuilder {
4
+ nodes = [];
5
+ where(where) {
6
+ this.nodes.push({ kind: "and", where });
7
+ return this;
8
+ }
9
+ orWhere(where) {
10
+ this.nodes.push({ kind: "or", where });
11
+ return this;
12
+ }
13
+ whereGroup(fn) {
14
+ const nested = new WhereBuilder;
15
+ fn(nested);
16
+ if (nested.nodes.length > 0) {
17
+ this.nodes.push({ kind: "and", group: nested.nodes });
18
+ }
19
+ return this;
20
+ }
21
+ orWhereGroup(fn) {
22
+ const nested = new WhereBuilder;
23
+ fn(nested);
24
+ if (nested.nodes.length > 0) {
25
+ this.nodes.push({ kind: "or", group: nested.nodes });
26
+ }
27
+ return this;
28
+ }
29
+ }
30
+ export {
31
+ WhereBuilder
32
+ };