@getstrata/core 0.5.101 → 0.7.4

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.
Files changed (61) hide show
  1. package/CHANGELOG.md +67 -32
  2. package/README.md +16 -35
  3. package/dist/core/auth/abilityCatalog.d.ts +2 -2
  4. package/dist/core/auth/basicAuthGuard.d.ts +9 -0
  5. package/dist/core/auth/guard.d.ts +6 -0
  6. package/dist/core/auth/jwt.d.ts +19 -0
  7. package/dist/core/auth/jwtGuard.d.ts +14 -0
  8. package/dist/core/auth/tokenAbilityChecker.d.ts +5 -0
  9. package/dist/core/cache/tags.d.ts +6 -0
  10. package/dist/core/contracts/authUserDirectory.d.ts +4 -0
  11. package/dist/core/database/dialect.d.ts +18 -0
  12. package/dist/core/database/factory.d.ts +1 -0
  13. package/dist/core/database/index.d.ts +8 -0
  14. package/dist/core/database/mysqlConnection.d.ts +12 -0
  15. package/dist/core/database/namedConnections.d.ts +15 -0
  16. package/dist/core/database/repositoryQuery.d.ts +1 -0
  17. package/dist/core/database/sqliteConnection.d.ts +7 -0
  18. package/dist/core/http/loginThrottleMiddleware.d.ts +5 -2
  19. package/dist/core/http/resources.d.ts +2 -2
  20. package/dist/core/http/response.d.ts +2 -1
  21. package/dist/core/http/statelessAuth.d.ts +8 -0
  22. package/dist/core/http/throttleResponse.d.ts +2 -0
  23. package/dist/core/runtime/frontendMode.d.ts +10 -2
  24. package/dist/entries/auth/basicAuthGuard.js +137 -0
  25. package/dist/entries/auth/jwt.js +135 -0
  26. package/dist/entries/auth/jwtGuard.js +203 -0
  27. package/dist/entries/auth/sessionGuard.js +3 -21
  28. package/dist/entries/auth/tokenAbilityChecker.js +24 -0
  29. package/dist/entries/cache/tags.js +7 -1
  30. package/dist/entries/database/connectionContext.js +1 -0
  31. package/dist/entries/database/dialect.js +1 -0
  32. package/dist/entries/database/factory.js +5 -4
  33. package/dist/entries/database/model.js +49 -32
  34. package/dist/entries/database/mysqlConnection.js +35 -0
  35. package/dist/entries/database/namedConnections.js +1 -0
  36. package/dist/entries/database/query.js +28 -15
  37. package/dist/entries/database/relationships.js +14 -6
  38. package/dist/entries/database/repositoryQuery.js +96 -81
  39. package/dist/entries/database/schema.js +28 -15
  40. package/dist/entries/database/sqliteConnection.js +34 -0
  41. package/dist/entries/facades.js +1 -1
  42. package/dist/entries/http/contentNegotiation.js +5 -2
  43. package/dist/entries/http/csrfMiddleware.js +45 -0
  44. package/dist/entries/http/loginThrottleMiddleware.js +246 -7
  45. package/dist/entries/http/memoryThrottleMiddleware.js +208 -6
  46. package/dist/entries/http/requireAbilityMiddleware.js +35 -11
  47. package/dist/entries/http/requirePasswordConfirmMiddleware.js +5 -2
  48. package/dist/entries/http/requireVerifiedMiddleware.js +5 -2
  49. package/dist/entries/http/requireWebAuthMiddleware.js +12 -3
  50. package/dist/entries/http/resources.js +4 -1
  51. package/dist/entries/http/response.js +46 -12
  52. package/dist/entries/http/statelessAuth.js +48 -0
  53. package/dist/entries/http/throttleMiddleware.js +208 -6
  54. package/dist/entries/http/webErrorResponse.js +35 -11
  55. package/dist/entries/http/webFormRequest.js +5 -2
  56. package/dist/entries/mail/mailer.js +1 -1
  57. package/dist/entries/openapi/generator.js +26 -3
  58. package/dist/entries/runtime/frontendMode.js +39 -10
  59. package/dist/framework/public-api.d.ts +11 -1
  60. package/dist/index.js +873 -270
  61. package/package.json +56 -5
@@ -23,11 +23,12 @@ function pivotTableName(leftTable, rightTable) {
23
23
  }
24
24
 
25
25
  // ../../src/core/database/query.ts
26
+ import { currentSqlDialect } from "@getstrata/core/database/dialect";
26
27
  function quoteIdentifier(identifier) {
27
- if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(identifier)) {
28
- throw new Error(`Invalid SQL identifier: ${identifier}`);
29
- }
30
- return `"${identifier}"`;
28
+ return currentSqlDialect().quoteIdentifier(identifier);
29
+ }
30
+ function returningSuffix(columns) {
31
+ return currentSqlDialect().returningClause(columns);
31
32
  }
32
33
  function qualifyColumn(tableName, column) {
33
34
  return `${quoteIdentifier(tableName)}.${quoteIdentifier(column)}`;
@@ -57,7 +58,7 @@ function isQueryOperator(value) {
57
58
  }
58
59
  function pushParam(values, value) {
59
60
  values.push(value);
60
- return `$${values.length}`;
61
+ return currentSqlDialect().placeholder(values.length);
61
62
  }
62
63
  function buildInClause(column, values, params) {
63
64
  if (values.length === 0) {
@@ -97,9 +98,12 @@ function buildOperatorClauses(column, operator, params) {
97
98
  clauses.push(`${column} <= ${pushParam(params, operator.lte)}`);
98
99
  }
99
100
  if (operator.ilike !== undefined) {
100
- clauses.push(`${column} ILIKE ${pushParam(params, operator.ilike)}`);
101
+ clauses.push(`${column} ${currentSqlDialect().ilikeOperator()} ${pushParam(params, operator.ilike)}`);
101
102
  }
102
103
  if (operator.tsMatch !== undefined) {
104
+ if (currentSqlDialect().driver !== "pgsql") {
105
+ throw new Error("Full-text search (tsMatch) is only available on PostgreSQL.");
106
+ }
103
107
  clauses.push(`${column} @@ plainto_tsquery('english', ${pushParam(params, operator.tsMatch)})`);
104
108
  }
105
109
  return clauses;
@@ -298,7 +302,7 @@ function buildSelectList(table, select, params = []) {
298
302
  return item.as ? `${column2} AS ${quoteIdentifier(item.as)}` : column2;
299
303
  }
300
304
  if (item.kind === "literalText") {
301
- return `${pushParam(params, item.value)}::text AS ${quoteIdentifier(item.as)}`;
305
+ return `${currentSqlDialect().castToText(pushParam(params, item.value))} AS ${quoteIdentifier(item.as)}`;
302
306
  }
303
307
  const column = qualifyColumn(item.table, item.column);
304
308
  const placeholder = pushParam(params, item.query);
@@ -390,7 +394,7 @@ function buildInsertQuery(table, values) {
390
394
  const placeholders = entries.map(([, value]) => pushParam(params, value)).join(", ");
391
395
  const returningColumns = buildReturningColumns(table);
392
396
  return {
393
- text: `INSERT INTO ${quoteIdentifier(table.name)} (${columns}) VALUES (${placeholders}) RETURNING ${returningColumns}`,
397
+ text: `INSERT INTO ${quoteIdentifier(table.name)} (${columns}) VALUES (${placeholders})${returningSuffix(returningColumns)}`,
394
398
  params
395
399
  };
396
400
  }
@@ -409,7 +413,7 @@ function buildUpdateQuery(table, id, changes) {
409
413
  appendSoftDeleteScope(table, {}, scopeClauses);
410
414
  const scopeSuffix = scopeClauses.length > 0 ? ` AND ${scopeClauses.join(" AND ")}` : "";
411
415
  return {
412
- text: `UPDATE ${quoteIdentifier(table.name)} SET ${setClause} WHERE ${quoteIdentifier(table.primaryKey)} = ${primaryKeyPlaceholder}${scopeSuffix} RETURNING ${returningColumns}`,
416
+ text: `UPDATE ${quoteIdentifier(table.name)} SET ${setClause} WHERE ${quoteIdentifier(table.primaryKey)} = ${primaryKeyPlaceholder}${scopeSuffix}${returningSuffix(returningColumns)}`,
413
417
  params
414
418
  };
415
419
  }
@@ -422,9 +426,12 @@ function buildSoftDeleteByIdQuery(table, id, deletedAt) {
422
426
  const scopeClauses = [];
423
427
  appendSoftDeleteScope(table, {}, scopeClauses);
424
428
  const scopeSuffix = scopeClauses.length > 0 ? ` AND ${scopeClauses.join(" AND ")}` : "";
429
+ const params = [];
430
+ const deletedAtPlaceholder = pushParam(params, deletedAt);
431
+ const idPlaceholder = pushParam(params, id);
425
432
  return {
426
- text: `UPDATE ${quoteIdentifier(table.name)} SET ${quoteIdentifier(deletedAtColumn)} = $1 WHERE ${quoteIdentifier(table.primaryKey)} = $2${scopeSuffix} RETURNING ${returningColumns}`,
427
- params: [deletedAt, id]
433
+ text: `UPDATE ${quoteIdentifier(table.name)} SET ${quoteIdentifier(deletedAtColumn)} = ${deletedAtPlaceholder} WHERE ${quoteIdentifier(table.primaryKey)} = ${idPlaceholder}${scopeSuffix}${returningSuffix(returningColumns)}`,
434
+ params
428
435
  };
429
436
  }
430
437
  function buildRestoreByIdQuery(table, id) {
@@ -433,15 +440,21 @@ function buildRestoreByIdQuery(table, id) {
433
440
  throw new Error(`Table ${table.name} does not support soft deletes.`);
434
441
  }
435
442
  const returningColumns = buildReturningColumns(table);
443
+ const params = [];
444
+ const deletedAtPlaceholder = pushParam(params, null);
445
+ const idPlaceholder = pushParam(params, id);
436
446
  return {
437
- text: `UPDATE ${quoteIdentifier(table.name)} SET ${quoteIdentifier(deletedAtColumn)} = $1 WHERE ${quoteIdentifier(table.primaryKey)} = $2 AND ${qualifyColumn(table.name, deletedAtColumn)} IS NOT NULL RETURNING ${returningColumns}`,
438
- params: [null, id]
447
+ text: `UPDATE ${quoteIdentifier(table.name)} SET ${quoteIdentifier(deletedAtColumn)} = ${deletedAtPlaceholder} WHERE ${quoteIdentifier(table.primaryKey)} = ${idPlaceholder} AND ${qualifyColumn(table.name, deletedAtColumn)} IS NOT NULL${returningSuffix(returningColumns)}`,
448
+ params
439
449
  };
440
450
  }
441
451
  function buildDeleteByIdQuery(table, id) {
452
+ const params = [];
453
+ const idPlaceholder = pushParam(params, id);
454
+ const returning = returningSuffix(`${quoteIdentifier(table.primaryKey)} AS ${quoteIdentifier("deleted_id")}`);
442
455
  return {
443
- text: `DELETE FROM ${quoteIdentifier(table.name)} WHERE ${quoteIdentifier(table.primaryKey)} = $1 RETURNING ${quoteIdentifier(table.primaryKey)} AS ${quoteIdentifier("deleted_id")}`,
444
- params: [id]
456
+ text: `DELETE FROM ${quoteIdentifier(table.name)} WHERE ${quoteIdentifier(table.primaryKey)} = ${idPlaceholder}${returning}`,
457
+ params
445
458
  };
446
459
  }
447
460
 
@@ -547,11 +560,7 @@ class HasManyRelationQuery {
547
560
  return this.create(related);
548
561
  }
549
562
  async createMany(records) {
550
- const created = [];
551
- for (const attributes of records) {
552
- created.push(await this.create(attributes));
553
- }
554
- return created;
563
+ return Promise.all(records.map((attributes) => this.create(attributes)));
555
564
  }
556
565
  }
557
566
 
@@ -1064,6 +1073,19 @@ function relationMatchKey(value) {
1064
1073
  }
1065
1074
  return String(value);
1066
1075
  }
1076
+ var relationLookupCache = new WeakMap;
1077
+ function indexedRelationLookup(map) {
1078
+ const cached = relationLookupCache.get(map);
1079
+ if (cached) {
1080
+ return cached;
1081
+ }
1082
+ const indexed = new Map;
1083
+ for (const [existing, value] of map) {
1084
+ indexed.set(relationMatchKey(existing), value);
1085
+ }
1086
+ relationLookupCache.set(map, indexed);
1087
+ return indexed;
1088
+ }
1067
1089
  function getByRelationKey(map, key) {
1068
1090
  if (map.has(key)) {
1069
1091
  return map.get(key);
@@ -1072,12 +1094,7 @@ function getByRelationKey(map, key) {
1072
1094
  if (want === "") {
1073
1095
  return;
1074
1096
  }
1075
- for (const [existing, value] of map) {
1076
- if (relationMatchKey(existing) === want) {
1077
- return value;
1078
- }
1079
- }
1080
- return;
1097
+ return indexedRelationLookup(map).get(want);
1081
1098
  }
1082
1099
  function indexHasManyRelation(parents, children, relation) {
1083
1100
  const groups = new Map;
@@ -1292,12 +1309,12 @@ async function eagerLoadOnModels(models, paths) {
1292
1309
  }
1293
1310
  grouped.set(head, existing);
1294
1311
  }
1295
- for (const [head, nested] of grouped) {
1312
+ await Promise.all([...grouped.entries()].map(async ([head, nested]) => {
1296
1313
  const unloaded = models.filter((model) => model.loaded(head) === undefined);
1297
1314
  if (unloaded.length > 0) {
1298
1315
  const first = unloaded[0];
1299
1316
  if (!first) {
1300
- continue;
1317
+ return;
1301
1318
  }
1302
1319
  const method = first[head];
1303
1320
  if (typeof method !== "function") {
@@ -1313,14 +1330,14 @@ async function eagerLoadOnModels(models, paths) {
1313
1330
  }
1314
1331
  }
1315
1332
  if (nested.length === 0) {
1316
- continue;
1333
+ return;
1317
1334
  }
1318
1335
  const children = models.flatMap((model) => {
1319
1336
  const loaded = model.loaded(head);
1320
1337
  return Array.isArray(loaded) ? loaded : loaded ? [loaded] : [];
1321
1338
  });
1322
1339
  await eagerLoadOnModels(children.filter(isLoadableModel), nested);
1323
- }
1340
+ }));
1324
1341
  }
1325
1342
  async function loadNested(model, path) {
1326
1343
  await eagerLoadOnModels([model], [path]);
@@ -1598,12 +1615,12 @@ class ModelQuery {
1598
1615
  const models = [];
1599
1616
  for (const row of rows) {
1600
1617
  const model = statics.newFromRecord(row, true);
1601
- await runObservers(model, "retrieved");
1602
1618
  for (const { name, relationQuery } of this.eager) {
1603
1619
  model.setLoaded(name, relationQuery.hydrateEager(row, name));
1604
1620
  }
1605
1621
  models.push(model);
1606
1622
  }
1623
+ await Promise.all(models.map((model) => runObservers(model, "retrieved")));
1607
1624
  const nested = this.eager.filter((item) => item.path.includes(".")).map((item) => item.path);
1608
1625
  await eagerLoadOnModels(models.filter(isLoadableModel), nested);
1609
1626
  return models;
@@ -2060,7 +2077,7 @@ class Model {
2060
2077
  morphTo(relatedByType, morphName, typeKey, idKey) {
2061
2078
  const resolvedName = morphName ?? inferRelationMethodName("morphTo");
2062
2079
  if (!resolvedName) {
2063
- throw new Error(`${this.constructor.name}.morphTo() needs an explicit morph name (Laravel infers it from the relation method).`);
2080
+ throw new Error(`${this.constructor.name}.morphTo() needs an explicit morph name (the calling method name is not available at runtime).`);
2064
2081
  }
2065
2082
  const resolvedMap = Object.fromEntries(Object.entries(relatedByType).map(([type, related]) => [type, resolveRelated(related)]));
2066
2083
  return new MorphToRelationQuery(this, resolvedMap, morphTo({
@@ -0,0 +1,35 @@
1
+ // @bun
2
+ // ../../src/core/database/mysqlConnection.ts
3
+ import mysql from "mysql2/promise";
4
+ function rowsFromResult(result) {
5
+ if (Array.isArray(result)) {
6
+ return result;
7
+ }
8
+ if (result && typeof result === "object") {
9
+ return [result];
10
+ }
11
+ return [];
12
+ }
13
+ function createMysqlConnectionFromPool(pool) {
14
+ return {
15
+ async unsafe(query, params = []) {
16
+ const [result] = await pool.execute(query, [...params]);
17
+ return rowsFromResult(result);
18
+ },
19
+ async close() {
20
+ if (typeof pool.end === "function") {
21
+ await pool.end();
22
+ }
23
+ }
24
+ };
25
+ }
26
+ function createMysqlConnection(url) {
27
+ if (!url.trim()) {
28
+ throw new Error("MYSQL_URL is not configured. Set url before creating a MySQL pool.");
29
+ }
30
+ return createMysqlConnectionFromPool(mysql.createPool(url));
31
+ }
32
+ export {
33
+ createMysqlConnection,
34
+ createMysqlConnectionFromPool
35
+ };
@@ -0,0 +1 @@
1
+ export * from "../../index.js";
@@ -1,10 +1,11 @@
1
1
  // @bun
2
2
  // ../../src/core/database/query.ts
3
+ import { currentSqlDialect } from "@getstrata/core/database/dialect";
3
4
  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}"`;
5
+ return currentSqlDialect().quoteIdentifier(identifier);
6
+ }
7
+ function returningSuffix(columns) {
8
+ return currentSqlDialect().returningClause(columns);
8
9
  }
9
10
  function qualifyColumn(tableName, column) {
10
11
  return `${quoteIdentifier(tableName)}.${quoteIdentifier(column)}`;
@@ -34,7 +35,7 @@ function isQueryOperator(value) {
34
35
  }
35
36
  function pushParam(values, value) {
36
37
  values.push(value);
37
- return `$${values.length}`;
38
+ return currentSqlDialect().placeholder(values.length);
38
39
  }
39
40
  function buildInClause(column, values, params) {
40
41
  if (values.length === 0) {
@@ -74,9 +75,12 @@ function buildOperatorClauses(column, operator, params) {
74
75
  clauses.push(`${column} <= ${pushParam(params, operator.lte)}`);
75
76
  }
76
77
  if (operator.ilike !== undefined) {
77
- clauses.push(`${column} ILIKE ${pushParam(params, operator.ilike)}`);
78
+ clauses.push(`${column} ${currentSqlDialect().ilikeOperator()} ${pushParam(params, operator.ilike)}`);
78
79
  }
79
80
  if (operator.tsMatch !== undefined) {
81
+ if (currentSqlDialect().driver !== "pgsql") {
82
+ throw new Error("Full-text search (tsMatch) is only available on PostgreSQL.");
83
+ }
80
84
  clauses.push(`${column} @@ plainto_tsquery('english', ${pushParam(params, operator.tsMatch)})`);
81
85
  }
82
86
  return clauses;
@@ -275,7 +279,7 @@ function buildSelectList(table, select, params = []) {
275
279
  return item.as ? `${column2} AS ${quoteIdentifier(item.as)}` : column2;
276
280
  }
277
281
  if (item.kind === "literalText") {
278
- return `${pushParam(params, item.value)}::text AS ${quoteIdentifier(item.as)}`;
282
+ return `${currentSqlDialect().castToText(pushParam(params, item.value))} AS ${quoteIdentifier(item.as)}`;
279
283
  }
280
284
  const column = qualifyColumn(item.table, item.column);
281
285
  const placeholder = pushParam(params, item.query);
@@ -367,7 +371,7 @@ function buildInsertQuery(table, values) {
367
371
  const placeholders = entries.map(([, value]) => pushParam(params, value)).join(", ");
368
372
  const returningColumns = buildReturningColumns(table);
369
373
  return {
370
- text: `INSERT INTO ${quoteIdentifier(table.name)} (${columns}) VALUES (${placeholders}) RETURNING ${returningColumns}`,
374
+ text: `INSERT INTO ${quoteIdentifier(table.name)} (${columns}) VALUES (${placeholders})${returningSuffix(returningColumns)}`,
371
375
  params
372
376
  };
373
377
  }
@@ -386,7 +390,7 @@ function buildUpdateQuery(table, id, changes) {
386
390
  appendSoftDeleteScope(table, {}, scopeClauses);
387
391
  const scopeSuffix = scopeClauses.length > 0 ? ` AND ${scopeClauses.join(" AND ")}` : "";
388
392
  return {
389
- text: `UPDATE ${quoteIdentifier(table.name)} SET ${setClause} WHERE ${quoteIdentifier(table.primaryKey)} = ${primaryKeyPlaceholder}${scopeSuffix} RETURNING ${returningColumns}`,
393
+ text: `UPDATE ${quoteIdentifier(table.name)} SET ${setClause} WHERE ${quoteIdentifier(table.primaryKey)} = ${primaryKeyPlaceholder}${scopeSuffix}${returningSuffix(returningColumns)}`,
390
394
  params
391
395
  };
392
396
  }
@@ -399,9 +403,12 @@ function buildSoftDeleteByIdQuery(table, id, deletedAt) {
399
403
  const scopeClauses = [];
400
404
  appendSoftDeleteScope(table, {}, scopeClauses);
401
405
  const scopeSuffix = scopeClauses.length > 0 ? ` AND ${scopeClauses.join(" AND ")}` : "";
406
+ const params = [];
407
+ const deletedAtPlaceholder = pushParam(params, deletedAt);
408
+ const idPlaceholder = pushParam(params, id);
402
409
  return {
403
- text: `UPDATE ${quoteIdentifier(table.name)} SET ${quoteIdentifier(deletedAtColumn)} = $1 WHERE ${quoteIdentifier(table.primaryKey)} = $2${scopeSuffix} RETURNING ${returningColumns}`,
404
- params: [deletedAt, id]
410
+ text: `UPDATE ${quoteIdentifier(table.name)} SET ${quoteIdentifier(deletedAtColumn)} = ${deletedAtPlaceholder} WHERE ${quoteIdentifier(table.primaryKey)} = ${idPlaceholder}${scopeSuffix}${returningSuffix(returningColumns)}`,
411
+ params
405
412
  };
406
413
  }
407
414
  function buildRestoreByIdQuery(table, id) {
@@ -410,15 +417,21 @@ function buildRestoreByIdQuery(table, id) {
410
417
  throw new Error(`Table ${table.name} does not support soft deletes.`);
411
418
  }
412
419
  const returningColumns = buildReturningColumns(table);
420
+ const params = [];
421
+ const deletedAtPlaceholder = pushParam(params, null);
422
+ const idPlaceholder = pushParam(params, id);
413
423
  return {
414
- text: `UPDATE ${quoteIdentifier(table.name)} SET ${quoteIdentifier(deletedAtColumn)} = $1 WHERE ${quoteIdentifier(table.primaryKey)} = $2 AND ${qualifyColumn(table.name, deletedAtColumn)} IS NOT NULL RETURNING ${returningColumns}`,
415
- params: [null, id]
424
+ text: `UPDATE ${quoteIdentifier(table.name)} SET ${quoteIdentifier(deletedAtColumn)} = ${deletedAtPlaceholder} WHERE ${quoteIdentifier(table.primaryKey)} = ${idPlaceholder} AND ${qualifyColumn(table.name, deletedAtColumn)} IS NOT NULL${returningSuffix(returningColumns)}`,
425
+ params
416
426
  };
417
427
  }
418
428
  function buildDeleteByIdQuery(table, id) {
429
+ const params = [];
430
+ const idPlaceholder = pushParam(params, id);
431
+ const returning = returningSuffix(`${quoteIdentifier(table.primaryKey)} AS ${quoteIdentifier("deleted_id")}`);
419
432
  return {
420
- text: `DELETE FROM ${quoteIdentifier(table.name)} WHERE ${quoteIdentifier(table.primaryKey)} = $1 RETURNING ${quoteIdentifier(table.primaryKey)} AS ${quoteIdentifier("deleted_id")}`,
421
- params: [id]
433
+ text: `DELETE FROM ${quoteIdentifier(table.name)} WHERE ${quoteIdentifier(table.primaryKey)} = ${idPlaceholder}${returning}`,
434
+ params
422
435
  };
423
436
  }
424
437
  export {
@@ -46,6 +46,19 @@ function relationMatchKey(value) {
46
46
  }
47
47
  return String(value);
48
48
  }
49
+ var relationLookupCache = new WeakMap;
50
+ function indexedRelationLookup(map) {
51
+ const cached = relationLookupCache.get(map);
52
+ if (cached) {
53
+ return cached;
54
+ }
55
+ const indexed = new Map;
56
+ for (const [existing, value] of map) {
57
+ indexed.set(relationMatchKey(existing), value);
58
+ }
59
+ relationLookupCache.set(map, indexed);
60
+ return indexed;
61
+ }
49
62
  function getByRelationKey(map, key) {
50
63
  if (map.has(key)) {
51
64
  return map.get(key);
@@ -54,12 +67,7 @@ function getByRelationKey(map, key) {
54
67
  if (want === "") {
55
68
  return;
56
69
  }
57
- for (const [existing, value] of map) {
58
- if (relationMatchKey(existing) === want) {
59
- return value;
60
- }
61
- }
62
- return;
70
+ return indexedRelationLookup(map).get(want);
63
71
  }
64
72
  function indexHasManyRelation(parents, children, relation) {
65
73
  const groups = new Map;
@@ -1,10 +1,11 @@
1
1
  // @bun
2
2
  // ../../src/core/database/query.ts
3
+ import { currentSqlDialect } from "@getstrata/core/database/dialect";
3
4
  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}"`;
5
+ return currentSqlDialect().quoteIdentifier(identifier);
6
+ }
7
+ function returningSuffix(columns) {
8
+ return currentSqlDialect().returningClause(columns);
8
9
  }
9
10
  function qualifyColumn(tableName, column) {
10
11
  return `${quoteIdentifier(tableName)}.${quoteIdentifier(column)}`;
@@ -34,7 +35,7 @@ function isQueryOperator(value) {
34
35
  }
35
36
  function pushParam(values, value) {
36
37
  values.push(value);
37
- return `$${values.length}`;
38
+ return currentSqlDialect().placeholder(values.length);
38
39
  }
39
40
  function buildInClause(column, values, params) {
40
41
  if (values.length === 0) {
@@ -74,9 +75,12 @@ function buildOperatorClauses(column, operator, params) {
74
75
  clauses.push(`${column} <= ${pushParam(params, operator.lte)}`);
75
76
  }
76
77
  if (operator.ilike !== undefined) {
77
- clauses.push(`${column} ILIKE ${pushParam(params, operator.ilike)}`);
78
+ clauses.push(`${column} ${currentSqlDialect().ilikeOperator()} ${pushParam(params, operator.ilike)}`);
78
79
  }
79
80
  if (operator.tsMatch !== undefined) {
81
+ if (currentSqlDialect().driver !== "pgsql") {
82
+ throw new Error("Full-text search (tsMatch) is only available on PostgreSQL.");
83
+ }
80
84
  clauses.push(`${column} @@ plainto_tsquery('english', ${pushParam(params, operator.tsMatch)})`);
81
85
  }
82
86
  return clauses;
@@ -275,7 +279,7 @@ function buildSelectList(table, select, params = []) {
275
279
  return item.as ? `${column2} AS ${quoteIdentifier(item.as)}` : column2;
276
280
  }
277
281
  if (item.kind === "literalText") {
278
- return `${pushParam(params, item.value)}::text AS ${quoteIdentifier(item.as)}`;
282
+ return `${currentSqlDialect().castToText(pushParam(params, item.value))} AS ${quoteIdentifier(item.as)}`;
279
283
  }
280
284
  const column = qualifyColumn(item.table, item.column);
281
285
  const placeholder = pushParam(params, item.query);
@@ -367,7 +371,7 @@ function buildInsertQuery(table, values) {
367
371
  const placeholders = entries.map(([, value]) => pushParam(params, value)).join(", ");
368
372
  const returningColumns = buildReturningColumns(table);
369
373
  return {
370
- text: `INSERT INTO ${quoteIdentifier(table.name)} (${columns}) VALUES (${placeholders}) RETURNING ${returningColumns}`,
374
+ text: `INSERT INTO ${quoteIdentifier(table.name)} (${columns}) VALUES (${placeholders})${returningSuffix(returningColumns)}`,
371
375
  params
372
376
  };
373
377
  }
@@ -386,7 +390,7 @@ function buildUpdateQuery(table, id, changes) {
386
390
  appendSoftDeleteScope(table, {}, scopeClauses);
387
391
  const scopeSuffix = scopeClauses.length > 0 ? ` AND ${scopeClauses.join(" AND ")}` : "";
388
392
  return {
389
- text: `UPDATE ${quoteIdentifier(table.name)} SET ${setClause} WHERE ${quoteIdentifier(table.primaryKey)} = ${primaryKeyPlaceholder}${scopeSuffix} RETURNING ${returningColumns}`,
393
+ text: `UPDATE ${quoteIdentifier(table.name)} SET ${setClause} WHERE ${quoteIdentifier(table.primaryKey)} = ${primaryKeyPlaceholder}${scopeSuffix}${returningSuffix(returningColumns)}`,
390
394
  params
391
395
  };
392
396
  }
@@ -399,9 +403,12 @@ function buildSoftDeleteByIdQuery(table, id, deletedAt) {
399
403
  const scopeClauses = [];
400
404
  appendSoftDeleteScope(table, {}, scopeClauses);
401
405
  const scopeSuffix = scopeClauses.length > 0 ? ` AND ${scopeClauses.join(" AND ")}` : "";
406
+ const params = [];
407
+ const deletedAtPlaceholder = pushParam(params, deletedAt);
408
+ const idPlaceholder = pushParam(params, id);
402
409
  return {
403
- text: `UPDATE ${quoteIdentifier(table.name)} SET ${quoteIdentifier(deletedAtColumn)} = $1 WHERE ${quoteIdentifier(table.primaryKey)} = $2${scopeSuffix} RETURNING ${returningColumns}`,
404
- params: [deletedAt, id]
410
+ text: `UPDATE ${quoteIdentifier(table.name)} SET ${quoteIdentifier(deletedAtColumn)} = ${deletedAtPlaceholder} WHERE ${quoteIdentifier(table.primaryKey)} = ${idPlaceholder}${scopeSuffix}${returningSuffix(returningColumns)}`,
411
+ params
405
412
  };
406
413
  }
407
414
  function buildRestoreByIdQuery(table, id) {
@@ -410,15 +417,21 @@ function buildRestoreByIdQuery(table, id) {
410
417
  throw new Error(`Table ${table.name} does not support soft deletes.`);
411
418
  }
412
419
  const returningColumns = buildReturningColumns(table);
420
+ const params = [];
421
+ const deletedAtPlaceholder = pushParam(params, null);
422
+ const idPlaceholder = pushParam(params, id);
413
423
  return {
414
- text: `UPDATE ${quoteIdentifier(table.name)} SET ${quoteIdentifier(deletedAtColumn)} = $1 WHERE ${quoteIdentifier(table.primaryKey)} = $2 AND ${qualifyColumn(table.name, deletedAtColumn)} IS NOT NULL RETURNING ${returningColumns}`,
415
- params: [null, id]
424
+ text: `UPDATE ${quoteIdentifier(table.name)} SET ${quoteIdentifier(deletedAtColumn)} = ${deletedAtPlaceholder} WHERE ${quoteIdentifier(table.primaryKey)} = ${idPlaceholder} AND ${qualifyColumn(table.name, deletedAtColumn)} IS NOT NULL${returningSuffix(returningColumns)}`,
425
+ params
416
426
  };
417
427
  }
418
428
  function buildDeleteByIdQuery(table, id) {
429
+ const params = [];
430
+ const idPlaceholder = pushParam(params, id);
431
+ const returning = returningSuffix(`${quoteIdentifier(table.primaryKey)} AS ${quoteIdentifier("deleted_id")}`);
419
432
  return {
420
- text: `DELETE FROM ${quoteIdentifier(table.name)} WHERE ${quoteIdentifier(table.primaryKey)} = $1 RETURNING ${quoteIdentifier(table.primaryKey)} AS ${quoteIdentifier("deleted_id")}`,
421
- params: [id]
433
+ text: `DELETE FROM ${quoteIdentifier(table.name)} WHERE ${quoteIdentifier(table.primaryKey)} = ${idPlaceholder}${returning}`,
434
+ params
422
435
  };
423
436
  }
424
437
 
@@ -469,6 +482,19 @@ function relationMatchKey(value) {
469
482
  }
470
483
  return String(value);
471
484
  }
485
+ var relationLookupCache = new WeakMap;
486
+ function indexedRelationLookup(map) {
487
+ const cached = relationLookupCache.get(map);
488
+ if (cached) {
489
+ return cached;
490
+ }
491
+ const indexed = new Map;
492
+ for (const [existing, value] of map) {
493
+ indexed.set(relationMatchKey(existing), value);
494
+ }
495
+ relationLookupCache.set(map, indexed);
496
+ return indexed;
497
+ }
472
498
  function getByRelationKey(map, key) {
473
499
  if (map.has(key)) {
474
500
  return map.get(key);
@@ -477,12 +503,7 @@ function getByRelationKey(map, key) {
477
503
  if (want === "") {
478
504
  return;
479
505
  }
480
- for (const [existing, value] of map) {
481
- if (relationMatchKey(existing) === want) {
482
- return value;
483
- }
484
- }
485
- return;
506
+ return indexedRelationLookup(map).get(want);
486
507
  }
487
508
  function indexHasManyRelation(parents, children, relation) {
488
509
  const groups = new Map;
@@ -895,73 +916,67 @@ class RepositoryQuery {
895
916
  return this;
896
917
  }
897
918
  async attach(rows) {
898
- if (rows.length === 0 || this.eagerLoads.length === 0) {
899
- return rows.map((row) => ({ ...row }));
900
- }
901
- let result = rows.map((row) => ({ ...row }));
902
- for (const load of this.eagerLoads) {
903
- if (load.kind === "hasMany") {
904
- const relation2 = load.relation;
905
- const grouped2 = await load.repository.withConnection(this.repository.getConnection()).loadHasManyForParents(rows, relation2, load.options);
906
- result = result.map((row) => ({
907
- ...row,
908
- [load.as]: getByRelationKey(grouped2, row[relation2.localKey]) ?? []
909
- }));
910
- continue;
919
+ const result = rows.map((row) => ({ ...row }));
920
+ if (result.length === 0 || this.eagerLoads.length === 0) {
921
+ return result;
922
+ }
923
+ await Promise.all(this.eagerLoads.map((load) => this.hydrateEagerLoad(rows, result, load)));
924
+ return result;
925
+ }
926
+ async hydrateEagerLoad(rows, result, load) {
927
+ if (load.kind === "hasMany") {
928
+ const relation2 = load.relation;
929
+ const grouped2 = await load.repository.withConnection(this.repository.getConnection()).loadHasManyForParents(rows, relation2, load.options);
930
+ for (const row of result) {
931
+ row[load.as] = getByRelationKey(grouped2, row[relation2.localKey]) ?? [];
911
932
  }
912
- if (load.kind === "morphMany") {
913
- const relation2 = load.relation;
914
- const grouped2 = await load.repository.withConnection(this.repository.getConnection()).loadMorphManyForParents(rows, relation2, load.options);
915
- result = result.map((row) => ({
916
- ...row,
917
- [load.as]: getByRelationKey(grouped2, row[relation2.localKey]) ?? []
918
- }));
919
- continue;
933
+ return;
934
+ }
935
+ if (load.kind === "morphMany") {
936
+ const relation2 = load.relation;
937
+ const grouped2 = await load.repository.withConnection(this.repository.getConnection()).loadMorphManyForParents(rows, relation2, load.options);
938
+ for (const row of result) {
939
+ row[load.as] = getByRelationKey(grouped2, row[relation2.localKey]) ?? [];
920
940
  }
921
- if (load.kind === "morphOne") {
922
- const relation2 = load.relation;
923
- const grouped2 = await load.repository.withConnection(this.repository.getConnection()).loadMorphOneForParents(rows, relation2, load.options);
924
- result = result.map((row) => ({
925
- ...row,
926
- [load.as]: getByRelationKey(grouped2, row[relation2.localKey])
927
- }));
928
- continue;
941
+ return;
942
+ }
943
+ if (load.kind === "morphOne") {
944
+ const relation2 = load.relation;
945
+ const grouped2 = await load.repository.withConnection(this.repository.getConnection()).loadMorphOneForParents(rows, relation2, load.options);
946
+ for (const row of result) {
947
+ row[load.as] = getByRelationKey(grouped2, row[relation2.localKey]);
929
948
  }
930
- if (load.kind === "hasManyThrough") {
931
- const relation2 = load.relation;
932
- const grouped2 = await load.repository.withConnection(this.repository.getConnection()).loadHasManyThroughForParents(rows, relation2, load.options);
933
- result = result.map((row) => ({
934
- ...row,
935
- [load.as]: getByRelationKey(grouped2, row[relation2.localKey]) ?? []
936
- }));
937
- continue;
949
+ return;
950
+ }
951
+ if (load.kind === "hasManyThrough") {
952
+ const relation2 = load.relation;
953
+ const grouped2 = await load.repository.withConnection(this.repository.getConnection()).loadHasManyThroughForParents(rows, relation2, load.options);
954
+ for (const row of result) {
955
+ row[load.as] = getByRelationKey(grouped2, row[relation2.localKey]) ?? [];
938
956
  }
939
- if (load.kind === "belongsToMany") {
940
- const relation2 = load.relation;
941
- const grouped2 = await this.repository.loadBelongsToManyForParents(rows, relation2, load.repository, load.options);
942
- result = result.map((row) => ({
943
- ...row,
944
- [load.as]: getByRelationKey(grouped2, row[relation2.parentKey]) ?? []
945
- }));
946
- continue;
957
+ return;
958
+ }
959
+ if (load.kind === "belongsToMany") {
960
+ const relation2 = load.relation;
961
+ const grouped2 = await this.repository.loadBelongsToManyForParents(rows, relation2, load.repository, load.options);
962
+ for (const row of result) {
963
+ row[load.as] = getByRelationKey(grouped2, row[relation2.parentKey]) ?? [];
947
964
  }
948
- if (load.kind === "morphTo") {
949
- const relation2 = load.relation;
950
- const grouped2 = await this.repository.loadMorphToForChildren(rows, relation2, load.morphRepositories ?? new Map, load.options);
951
- result = result.map((row) => ({
952
- ...row,
953
- [load.as]: getByRelationKey(grouped2, row[relation2.morphIdKey])
954
- }));
955
- continue;
965
+ return;
966
+ }
967
+ if (load.kind === "morphTo") {
968
+ const relation2 = load.relation;
969
+ const grouped2 = await this.repository.loadMorphToForChildren(rows, relation2, load.morphRepositories ?? new Map, load.options);
970
+ for (const row of result) {
971
+ row[load.as] = getByRelationKey(grouped2, row[relation2.morphIdKey]);
956
972
  }
957
- const relation = load.relation;
958
- const grouped = await this.repository.loadBelongsToForParents(rows, relation, load.repository, load.options);
959
- result = result.map((row) => ({
960
- ...row,
961
- [load.as]: getByRelationKey(grouped, row[relation.foreignKey])
962
- }));
973
+ return;
974
+ }
975
+ const relation = load.relation;
976
+ const grouped = await this.repository.loadBelongsToForParents(rows, relation, load.repository, load.options);
977
+ for (const row of result) {
978
+ row[load.as] = getByRelationKey(grouped, row[relation.foreignKey]);
963
979
  }
964
- return result;
965
980
  }
966
981
  }
967
982
  export {