@getstrata/core 0.5.100 → 0.7.3

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 (65) 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/baseRepository.d.ts +5 -1
  12. package/dist/core/database/dialect.d.ts +18 -0
  13. package/dist/core/database/factory.d.ts +1 -0
  14. package/dist/core/database/index.d.ts +11 -3
  15. package/dist/core/database/model.d.ts +21 -2
  16. package/dist/core/database/mysqlConnection.d.ts +12 -0
  17. package/dist/core/database/namedConnections.d.ts +15 -0
  18. package/dist/core/database/relationQuery.d.ts +22 -3
  19. package/dist/core/database/relationships.d.ts +22 -2
  20. package/dist/core/database/repositoryQuery.d.ts +5 -1
  21. package/dist/core/database/sqliteConnection.d.ts +7 -0
  22. package/dist/core/http/loginThrottleMiddleware.d.ts +5 -2
  23. package/dist/core/http/resources.d.ts +2 -2
  24. package/dist/core/http/response.d.ts +2 -1
  25. package/dist/core/http/statelessAuth.d.ts +8 -0
  26. package/dist/core/http/throttleResponse.d.ts +2 -0
  27. package/dist/core/runtime/frontendMode.d.ts +10 -2
  28. package/dist/entries/auth/basicAuthGuard.js +137 -0
  29. package/dist/entries/auth/jwt.js +135 -0
  30. package/dist/entries/auth/jwtGuard.js +203 -0
  31. package/dist/entries/auth/sessionGuard.js +3 -21
  32. package/dist/entries/auth/tokenAbilityChecker.js +24 -0
  33. package/dist/entries/cache/tags.js +7 -1
  34. package/dist/entries/database/connectionContext.js +1 -0
  35. package/dist/entries/database/dialect.js +1 -0
  36. package/dist/entries/database/factory.js +5 -4
  37. package/dist/entries/database/model.js +189 -33
  38. package/dist/entries/database/mysqlConnection.js +35 -0
  39. package/dist/entries/database/namedConnections.js +1 -0
  40. package/dist/entries/database/query.js +28 -15
  41. package/dist/entries/database/relationships.js +43 -6
  42. package/dist/entries/database/repositoryQuery.js +142 -73
  43. package/dist/entries/database/schema.js +28 -15
  44. package/dist/entries/database/sqliteConnection.js +34 -0
  45. package/dist/entries/facades.js +1 -1
  46. package/dist/entries/http/contentNegotiation.js +5 -2
  47. package/dist/entries/http/csrfMiddleware.js +45 -0
  48. package/dist/entries/http/loginThrottleMiddleware.js +246 -7
  49. package/dist/entries/http/memoryThrottleMiddleware.js +208 -6
  50. package/dist/entries/http/requireAbilityMiddleware.js +35 -11
  51. package/dist/entries/http/requirePasswordConfirmMiddleware.js +5 -2
  52. package/dist/entries/http/requireVerifiedMiddleware.js +5 -2
  53. package/dist/entries/http/requireWebAuthMiddleware.js +12 -3
  54. package/dist/entries/http/resources.js +4 -1
  55. package/dist/entries/http/response.js +46 -12
  56. package/dist/entries/http/statelessAuth.js +48 -0
  57. package/dist/entries/http/throttleMiddleware.js +208 -6
  58. package/dist/entries/http/webErrorResponse.js +35 -11
  59. package/dist/entries/http/webFormRequest.js +5 -2
  60. package/dist/entries/mail/mailer.js +1 -1
  61. package/dist/entries/openapi/generator.js +48 -5
  62. package/dist/entries/runtime/frontendMode.js +39 -10
  63. package/dist/framework/public-api.d.ts +11 -1
  64. package/dist/index.js +1068 -250
  65. 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
 
@@ -775,7 +784,8 @@ class BelongsToManyRelationQuery {
775
784
  return;
776
785
  }
777
786
  const list = Array.isArray(ids) ? ids : [ids];
778
- await this.connection().unsafe(`DELETE FROM ${this.relation.pivotTable} WHERE ${String(this.relation.foreignPivotKey)} = $1 AND ${String(this.relation.relatedPivotKey)} = ANY($2)`, [parentId, list]);
787
+ const placeholders = list.map((_, index) => `$${index + 2}`).join(", ");
788
+ await this.connection().unsafe(`DELETE FROM ${this.relation.pivotTable} WHERE ${String(this.relation.foreignPivotKey)} = $1 AND ${String(this.relation.relatedPivotKey)} IN (${placeholders})`, [parentId, ...list]);
779
789
  }
780
790
  async sync(ids) {
781
791
  await this.detach();
@@ -957,6 +967,65 @@ class MorphToRelationQuery {
957
967
  }
958
968
  }
959
969
 
970
+ class HasManyThroughRelationQuery {
971
+ parent;
972
+ related;
973
+ relation;
974
+ kind = "hasManyThrough";
975
+ extraWhere = {};
976
+ extraOptions = {};
977
+ constructor(parent, related, relation) {
978
+ this.parent = parent;
979
+ this.related = related;
980
+ this.relation = relation;
981
+ }
982
+ where(where) {
983
+ this.extraWhere = { ...this.extraWhere, ...where };
984
+ return this;
985
+ }
986
+ orderBy(orderBy) {
987
+ this.extraOptions = { ...this.extraOptions, orderBy };
988
+ return this;
989
+ }
990
+ limit(limit) {
991
+ this.extraOptions = { ...this.extraOptions, limit };
992
+ return this;
993
+ }
994
+ applyEagerLoad(query, alias) {
995
+ query.withHasManyThrough(alias, this.relation, this.related.repository(), this.extraOptions);
996
+ }
997
+ hydrateEager(row, alias) {
998
+ const value = row[alias] ?? [];
999
+ const rows = Array.isArray(value) ? value : [];
1000
+ return rows.map((item) => this.related.newFromRecord(item));
1001
+ }
1002
+ toExistsClause(parentTable) {
1003
+ const farTable = this.related.repository().getTable().name;
1004
+ const extra = buildAdvancedWhereClause(farTable, this.extraWhere, [], []);
1005
+ const extraSql = extra.clause.replace(/^ WHERE /, "");
1006
+ const sql = `SELECT 1 FROM ${quoteIdentifier(farTable)} INNER JOIN ${quoteIdentifier(this.relation.throughTable)} ON ${qualifyColumn(this.relation.throughTable, this.relation.secondLocalKey)} = ${qualifyColumn(farTable, this.relation.secondKey)} WHERE ${qualifyColumn(this.relation.throughTable, this.relation.firstKey)} = ${qualifyColumn(parentTable, this.relation.localKey)}${extraSql ? ` AND ${extraSql}` : ""}`;
1007
+ return { sql, params: extra.params };
1008
+ }
1009
+ async get() {
1010
+ const rows = await this.related.repository().withConnection(this.parent.getRepository().getConnection()).findHasManyThrough(this.parent.get(this.relation.localKey), this.relation, {
1011
+ ...this.extraOptions,
1012
+ where: this.extraWhere
1013
+ });
1014
+ return rows.map((row) => this.related.newFromRecord(row));
1015
+ }
1016
+ async first() {
1017
+ const rows = await this.limit(1).get();
1018
+ return rows[0] ?? null;
1019
+ }
1020
+ async count() {
1021
+ const rows = await this.get();
1022
+ return rows.length;
1023
+ }
1024
+ then(onfulfilled, onrejected) {
1025
+ return thenGet(() => this.get(), onfulfilled, onrejected);
1026
+ }
1027
+ }
1028
+
960
1029
  // ../../src/core/database/relationships.ts
961
1030
  function hasMany(definition) {
962
1031
  return {
@@ -976,6 +1045,13 @@ function belongsTo(definition) {
976
1045
  ...definition
977
1046
  };
978
1047
  }
1048
+ function hasManyThrough(definition) {
1049
+ return {
1050
+ type: "hasManyThrough",
1051
+ throughParentKey: "__through_parent_id",
1052
+ ...definition
1053
+ };
1054
+ }
979
1055
  function belongsToMany(definition) {
980
1056
  return {
981
1057
  type: "belongsToMany",
@@ -997,6 +1073,19 @@ function relationMatchKey(value) {
997
1073
  }
998
1074
  return String(value);
999
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
+ }
1000
1089
  function getByRelationKey(map, key) {
1001
1090
  if (map.has(key)) {
1002
1091
  return map.get(key);
@@ -1005,12 +1094,7 @@ function getByRelationKey(map, key) {
1005
1094
  if (want === "") {
1006
1095
  return;
1007
1096
  }
1008
- for (const [existing, value] of map) {
1009
- if (relationMatchKey(existing) === want) {
1010
- return value;
1011
- }
1012
- }
1013
- return;
1097
+ return indexedRelationLookup(map).get(want);
1014
1098
  }
1015
1099
  function indexHasManyRelation(parents, children, relation) {
1016
1100
  const groups = new Map;
@@ -1149,6 +1233,26 @@ function indexMorphOneRelation(parents, children, relation) {
1149
1233
  }
1150
1234
  return result;
1151
1235
  }
1236
+ function indexHasManyThroughRelation(parents, children, relation) {
1237
+ const throughKey = relation.throughParentKey ?? "__through_parent_id";
1238
+ const grouped = new Map;
1239
+ for (const child of children) {
1240
+ const key = relationMatchKey(child[throughKey]);
1241
+ if (key === "") {
1242
+ continue;
1243
+ }
1244
+ const existing = grouped.get(key) ?? [];
1245
+ const { [throughKey]: _through, ...far } = child;
1246
+ existing.push(far);
1247
+ grouped.set(key, existing);
1248
+ }
1249
+ const result = new Map;
1250
+ for (const parent of parents) {
1251
+ const key = relationMatchKey(parent[relation.localKey]);
1252
+ result.set(parent[relation.localKey], grouped.get(key) ?? []);
1253
+ }
1254
+ return result;
1255
+ }
1152
1256
  function indexMorphToRelation(children, parentsByType, relation) {
1153
1257
  const result = new Map;
1154
1258
  for (const child of children) {
@@ -1205,12 +1309,12 @@ async function eagerLoadOnModels(models, paths) {
1205
1309
  }
1206
1310
  grouped.set(head, existing);
1207
1311
  }
1208
- for (const [head, nested] of grouped) {
1312
+ await Promise.all([...grouped.entries()].map(async ([head, nested]) => {
1209
1313
  const unloaded = models.filter((model) => model.loaded(head) === undefined);
1210
1314
  if (unloaded.length > 0) {
1211
1315
  const first = unloaded[0];
1212
1316
  if (!first) {
1213
- continue;
1317
+ return;
1214
1318
  }
1215
1319
  const method = first[head];
1216
1320
  if (typeof method !== "function") {
@@ -1226,14 +1330,14 @@ async function eagerLoadOnModels(models, paths) {
1226
1330
  }
1227
1331
  }
1228
1332
  if (nested.length === 0) {
1229
- continue;
1333
+ return;
1230
1334
  }
1231
1335
  const children = models.flatMap((model) => {
1232
1336
  const loaded = model.loaded(head);
1233
1337
  return Array.isArray(loaded) ? loaded : loaded ? [loaded] : [];
1234
1338
  });
1235
1339
  await eagerLoadOnModels(children.filter(isLoadableModel), nested);
1236
- }
1340
+ }));
1237
1341
  }
1238
1342
  async function loadNested(model, path) {
1239
1343
  await eagerLoadOnModels([model], [path]);
@@ -1493,18 +1597,30 @@ class ModelQuery {
1493
1597
  this.query.withMorphTo(...args);
1494
1598
  return this;
1495
1599
  }
1600
+ withHasManyThrough(...args) {
1601
+ this.query.withHasManyThrough(...args);
1602
+ return this;
1603
+ }
1604
+ withTrashed() {
1605
+ this.query.withTrashed();
1606
+ return this;
1607
+ }
1608
+ onlyTrashed() {
1609
+ this.query.onlyTrashed();
1610
+ return this;
1611
+ }
1496
1612
  async get() {
1497
1613
  const statics = modelStatics(this.modelClass);
1498
1614
  const rows = await this.query.get();
1499
1615
  const models = [];
1500
1616
  for (const row of rows) {
1501
1617
  const model = statics.newFromRecord(row, true);
1502
- await runObservers(model, "retrieved");
1503
1618
  for (const { name, relationQuery } of this.eager) {
1504
1619
  model.setLoaded(name, relationQuery.hydrateEager(row, name));
1505
1620
  }
1506
1621
  models.push(model);
1507
1622
  }
1623
+ await Promise.all(models.map((model) => runObservers(model, "retrieved")));
1508
1624
  const nested = this.eager.filter((item) => item.path.includes(".")).map((item) => item.path);
1509
1625
  await eagerLoadOnModels(models.filter(isLoadableModel), nested);
1510
1626
  return models;
@@ -1700,6 +1816,31 @@ class Model {
1700
1816
  static with(...relations) {
1701
1817
  return Model.query.call(this).with(...relations);
1702
1818
  }
1819
+ static withTrashed() {
1820
+ return Model.query.call(this).withTrashed();
1821
+ }
1822
+ static onlyTrashed() {
1823
+ return Model.query.call(this).onlyTrashed();
1824
+ }
1825
+ static async chunk(count, callback) {
1826
+ const statics = modelStatics(this);
1827
+ ensureBooted(this);
1828
+ await resolveModelRepository(this).chunk(count, async (rows) => {
1829
+ return await callback(rows.map((row) => statics.newFromRecord(row, true)));
1830
+ });
1831
+ }
1832
+ static async cursorPaginate(options) {
1833
+ const statics = modelStatics(this);
1834
+ ensureBooted(this);
1835
+ const page = await resolveModelRepository(this).cursorPaginate({
1836
+ perPage: options.perPage,
1837
+ cursor: options.cursor
1838
+ });
1839
+ return {
1840
+ data: page.data.map((row) => statics.newFromRecord(row, true)),
1841
+ meta: page.meta
1842
+ };
1843
+ }
1703
1844
  static whereHas(name, constrain) {
1704
1845
  return Model.query.call(this).whereHas(name, constrain);
1705
1846
  }
@@ -1884,6 +2025,20 @@ class Model {
1884
2025
  ownerKey: ownerKey ?? relatedTable.primaryKey
1885
2026
  }));
1886
2027
  }
2028
+ hasManyThrough(related, through, firstKey, secondKey, localKey, secondLocalKey) {
2029
+ const table = this.repository.getTable();
2030
+ const relatedClass = resolveRelated(related);
2031
+ const throughClass = resolveRelated(through);
2032
+ const throughTable = throughClass.repository().getTable();
2033
+ return new HasManyThroughRelationQuery(this, relatedClass, hasManyThrough({
2034
+ name: relatedClass.repository().getTable().name,
2035
+ throughTable: throughTable.name,
2036
+ localKey: localKey ?? table.primaryKey,
2037
+ firstKey: firstKey ?? foreignKeyFromTable(table.name),
2038
+ secondLocalKey: secondLocalKey ?? throughTable.primaryKey,
2039
+ secondKey: secondKey ?? foreignKeyFromTable(throughTable.name)
2040
+ }));
2041
+ }
1887
2042
  belongsToMany(related, pivotTable, foreignPivotKey, relatedPivotKey) {
1888
2043
  const table = this.repository.getTable();
1889
2044
  const relatedClass = resolveRelated(related);
@@ -1922,7 +2077,7 @@ class Model {
1922
2077
  morphTo(relatedByType, morphName, typeKey, idKey) {
1923
2078
  const resolvedName = morphName ?? inferRelationMethodName("morphTo");
1924
2079
  if (!resolvedName) {
1925
- 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).`);
1926
2081
  }
1927
2082
  const resolvedMap = Object.fromEntries(Object.entries(relatedByType).map(([type, related]) => [type, resolveRelated(related)]));
1928
2083
  return new MorphToRelationQuery(this, resolvedMap, morphTo({
@@ -1975,6 +2130,7 @@ export {
1975
2130
  BelongsToManyRelationQuery,
1976
2131
  BelongsToRelationQuery,
1977
2132
  HasManyRelationQuery,
2133
+ HasManyThroughRelationQuery,
1978
2134
  HasOneRelationQuery,
1979
2135
  Model,
1980
2136
  ModelQuery,
@@ -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 {
@@ -18,6 +18,13 @@ function belongsTo(definition) {
18
18
  ...definition
19
19
  };
20
20
  }
21
+ function hasManyThrough(definition) {
22
+ return {
23
+ type: "hasManyThrough",
24
+ throughParentKey: "__through_parent_id",
25
+ ...definition
26
+ };
27
+ }
21
28
  function belongsToMany(definition) {
22
29
  return {
23
30
  type: "belongsToMany",
@@ -39,6 +46,19 @@ function relationMatchKey(value) {
39
46
  }
40
47
  return String(value);
41
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
+ }
42
62
  function getByRelationKey(map, key) {
43
63
  if (map.has(key)) {
44
64
  return map.get(key);
@@ -47,12 +67,7 @@ function getByRelationKey(map, key) {
47
67
  if (want === "") {
48
68
  return;
49
69
  }
50
- for (const [existing, value] of map) {
51
- if (relationMatchKey(existing) === want) {
52
- return value;
53
- }
54
- }
55
- return;
70
+ return indexedRelationLookup(map).get(want);
56
71
  }
57
72
  function indexHasManyRelation(parents, children, relation) {
58
73
  const groups = new Map;
@@ -191,6 +206,26 @@ function indexMorphOneRelation(parents, children, relation) {
191
206
  }
192
207
  return result;
193
208
  }
209
+ function indexHasManyThroughRelation(parents, children, relation) {
210
+ const throughKey = relation.throughParentKey ?? "__through_parent_id";
211
+ const grouped = new Map;
212
+ for (const child of children) {
213
+ const key = relationMatchKey(child[throughKey]);
214
+ if (key === "") {
215
+ continue;
216
+ }
217
+ const existing = grouped.get(key) ?? [];
218
+ const { [throughKey]: _through, ...far } = child;
219
+ existing.push(far);
220
+ grouped.set(key, existing);
221
+ }
222
+ const result = new Map;
223
+ for (const parent of parents) {
224
+ const key = relationMatchKey(parent[relation.localKey]);
225
+ result.set(parent[relation.localKey], grouped.get(key) ?? []);
226
+ }
227
+ return result;
228
+ }
194
229
  function indexMorphToRelation(children, parentsByType, relation) {
195
230
  const result = new Map;
196
231
  for (const child of children) {
@@ -211,10 +246,12 @@ export {
211
246
  belongsToMany,
212
247
  getByRelationKey,
213
248
  hasMany,
249
+ hasManyThrough,
214
250
  hasOne,
215
251
  indexBelongsToManyRelation,
216
252
  indexBelongsToRelation,
217
253
  indexHasManyRelation,
254
+ indexHasManyThroughRelation,
218
255
  indexHasOneRelation,
219
256
  indexMorphManyRelation,
220
257
  indexMorphOneRelation,