@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
@@ -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
 
@@ -441,6 +454,13 @@ function belongsTo(definition) {
441
454
  ...definition
442
455
  };
443
456
  }
457
+ function hasManyThrough(definition) {
458
+ return {
459
+ type: "hasManyThrough",
460
+ throughParentKey: "__through_parent_id",
461
+ ...definition
462
+ };
463
+ }
444
464
  function belongsToMany(definition) {
445
465
  return {
446
466
  type: "belongsToMany",
@@ -462,6 +482,19 @@ function relationMatchKey(value) {
462
482
  }
463
483
  return String(value);
464
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
+ }
465
498
  function getByRelationKey(map, key) {
466
499
  if (map.has(key)) {
467
500
  return map.get(key);
@@ -470,12 +503,7 @@ function getByRelationKey(map, key) {
470
503
  if (want === "") {
471
504
  return;
472
505
  }
473
- for (const [existing, value] of map) {
474
- if (relationMatchKey(existing) === want) {
475
- return value;
476
- }
477
- }
478
- return;
506
+ return indexedRelationLookup(map).get(want);
479
507
  }
480
508
  function indexHasManyRelation(parents, children, relation) {
481
509
  const groups = new Map;
@@ -614,6 +642,26 @@ function indexMorphOneRelation(parents, children, relation) {
614
642
  }
615
643
  return result;
616
644
  }
645
+ function indexHasManyThroughRelation(parents, children, relation) {
646
+ const throughKey = relation.throughParentKey ?? "__through_parent_id";
647
+ const grouped = new Map;
648
+ for (const child of children) {
649
+ const key = relationMatchKey(child[throughKey]);
650
+ if (key === "") {
651
+ continue;
652
+ }
653
+ const existing = grouped.get(key) ?? [];
654
+ const { [throughKey]: _through, ...far } = child;
655
+ existing.push(far);
656
+ grouped.set(key, existing);
657
+ }
658
+ const result = new Map;
659
+ for (const parent of parents) {
660
+ const key = relationMatchKey(parent[relation.localKey]);
661
+ result.set(parent[relation.localKey], grouped.get(key) ?? []);
662
+ }
663
+ return result;
664
+ }
617
665
  function indexMorphToRelation(children, parentsByType, relation) {
618
666
  const result = new Map;
619
667
  for (const child of children) {
@@ -797,6 +845,24 @@ class RepositoryQuery {
797
845
  });
798
846
  return this;
799
847
  }
848
+ withHasManyThrough(as, relation, farRepository, options = {}) {
849
+ this.eagerLoads.push({
850
+ kind: "hasManyThrough",
851
+ as,
852
+ relation,
853
+ repository: farRepository,
854
+ options
855
+ });
856
+ return this;
857
+ }
858
+ withTrashed() {
859
+ this.queryOptions = { ...this.queryOptions, withTrashed: true };
860
+ return this;
861
+ }
862
+ onlyTrashed() {
863
+ this.queryOptions = { ...this.queryOptions, onlyTrashed: true };
864
+ return this;
865
+ }
800
866
  async get() {
801
867
  const rows = await this.repository.findAll(this.buildOptions());
802
868
  return await this.attach(rows);
@@ -850,64 +916,67 @@ class RepositoryQuery {
850
916
  return this;
851
917
  }
852
918
  async attach(rows) {
853
- if (rows.length === 0 || this.eagerLoads.length === 0) {
854
- return rows.map((row) => ({ ...row }));
855
- }
856
- let result = rows.map((row) => ({ ...row }));
857
- for (const load of this.eagerLoads) {
858
- if (load.kind === "hasMany") {
859
- const relation2 = load.relation;
860
- const grouped2 = await load.repository.withConnection(this.repository.getConnection()).loadHasManyForParents(rows, relation2, load.options);
861
- result = result.map((row) => ({
862
- ...row,
863
- [load.as]: getByRelationKey(grouped2, row[relation2.localKey]) ?? []
864
- }));
865
- 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]) ?? [];
866
932
  }
867
- if (load.kind === "morphMany") {
868
- const relation2 = load.relation;
869
- const grouped2 = await load.repository.withConnection(this.repository.getConnection()).loadMorphManyForParents(rows, relation2, load.options);
870
- result = result.map((row) => ({
871
- ...row,
872
- [load.as]: getByRelationKey(grouped2, row[relation2.localKey]) ?? []
873
- }));
874
- 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]) ?? [];
875
940
  }
876
- if (load.kind === "morphOne") {
877
- const relation2 = load.relation;
878
- const grouped2 = await load.repository.withConnection(this.repository.getConnection()).loadMorphOneForParents(rows, relation2, load.options);
879
- result = result.map((row) => ({
880
- ...row,
881
- [load.as]: getByRelationKey(grouped2, row[relation2.localKey])
882
- }));
883
- 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]);
884
948
  }
885
- if (load.kind === "belongsToMany") {
886
- const relation2 = load.relation;
887
- const grouped2 = await this.repository.loadBelongsToManyForParents(rows, relation2, load.repository, load.options);
888
- result = result.map((row) => ({
889
- ...row,
890
- [load.as]: getByRelationKey(grouped2, row[relation2.parentKey]) ?? []
891
- }));
892
- 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]) ?? [];
893
956
  }
894
- if (load.kind === "morphTo") {
895
- const relation2 = load.relation;
896
- const grouped2 = await this.repository.loadMorphToForChildren(rows, relation2, load.morphRepositories ?? new Map, load.options);
897
- result = result.map((row) => ({
898
- ...row,
899
- [load.as]: getByRelationKey(grouped2, row[relation2.morphIdKey])
900
- }));
901
- 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]) ?? [];
902
964
  }
903
- const relation = load.relation;
904
- const grouped = await this.repository.loadBelongsToForParents(rows, relation, load.repository, load.options);
905
- result = result.map((row) => ({
906
- ...row,
907
- [load.as]: getByRelationKey(grouped, row[relation.foreignKey])
908
- }));
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]);
972
+ }
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]);
909
979
  }
910
- return result;
911
980
  }
912
981
  }
913
982
  export {
@@ -267,11 +267,12 @@ class UnsupportedSchemaFeatureError extends Error {
267
267
  }
268
268
  }
269
269
  // ../../src/core/database/query.ts
270
+ import { currentSqlDialect } from "@getstrata/core/database/dialect";
270
271
  function quoteIdentifier(identifier) {
271
- if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(identifier)) {
272
- throw new Error(`Invalid SQL identifier: ${identifier}`);
273
- }
274
- return `"${identifier}"`;
272
+ return currentSqlDialect().quoteIdentifier(identifier);
273
+ }
274
+ function returningSuffix(columns) {
275
+ return currentSqlDialect().returningClause(columns);
275
276
  }
276
277
  function qualifyColumn(tableName, column) {
277
278
  return `${quoteIdentifier(tableName)}.${quoteIdentifier(column)}`;
@@ -301,7 +302,7 @@ function isQueryOperator(value) {
301
302
  }
302
303
  function pushParam(values, value) {
303
304
  values.push(value);
304
- return `$${values.length}`;
305
+ return currentSqlDialect().placeholder(values.length);
305
306
  }
306
307
  function buildInClause(column, values, params) {
307
308
  if (values.length === 0) {
@@ -341,9 +342,12 @@ function buildOperatorClauses(column, operator, params) {
341
342
  clauses.push(`${column} <= ${pushParam(params, operator.lte)}`);
342
343
  }
343
344
  if (operator.ilike !== undefined) {
344
- clauses.push(`${column} ILIKE ${pushParam(params, operator.ilike)}`);
345
+ clauses.push(`${column} ${currentSqlDialect().ilikeOperator()} ${pushParam(params, operator.ilike)}`);
345
346
  }
346
347
  if (operator.tsMatch !== undefined) {
348
+ if (currentSqlDialect().driver !== "pgsql") {
349
+ throw new Error("Full-text search (tsMatch) is only available on PostgreSQL.");
350
+ }
347
351
  clauses.push(`${column} @@ plainto_tsquery('english', ${pushParam(params, operator.tsMatch)})`);
348
352
  }
349
353
  return clauses;
@@ -542,7 +546,7 @@ function buildSelectList(table, select, params = []) {
542
546
  return item.as ? `${column2} AS ${quoteIdentifier(item.as)}` : column2;
543
547
  }
544
548
  if (item.kind === "literalText") {
545
- return `${pushParam(params, item.value)}::text AS ${quoteIdentifier(item.as)}`;
549
+ return `${currentSqlDialect().castToText(pushParam(params, item.value))} AS ${quoteIdentifier(item.as)}`;
546
550
  }
547
551
  const column = qualifyColumn(item.table, item.column);
548
552
  const placeholder = pushParam(params, item.query);
@@ -634,7 +638,7 @@ function buildInsertQuery(table, values) {
634
638
  const placeholders = entries.map(([, value]) => pushParam(params, value)).join(", ");
635
639
  const returningColumns = buildReturningColumns(table);
636
640
  return {
637
- text: `INSERT INTO ${quoteIdentifier(table.name)} (${columns}) VALUES (${placeholders}) RETURNING ${returningColumns}`,
641
+ text: `INSERT INTO ${quoteIdentifier(table.name)} (${columns}) VALUES (${placeholders})${returningSuffix(returningColumns)}`,
638
642
  params
639
643
  };
640
644
  }
@@ -653,7 +657,7 @@ function buildUpdateQuery(table, id, changes) {
653
657
  appendSoftDeleteScope(table, {}, scopeClauses);
654
658
  const scopeSuffix = scopeClauses.length > 0 ? ` AND ${scopeClauses.join(" AND ")}` : "";
655
659
  return {
656
- text: `UPDATE ${quoteIdentifier(table.name)} SET ${setClause} WHERE ${quoteIdentifier(table.primaryKey)} = ${primaryKeyPlaceholder}${scopeSuffix} RETURNING ${returningColumns}`,
660
+ text: `UPDATE ${quoteIdentifier(table.name)} SET ${setClause} WHERE ${quoteIdentifier(table.primaryKey)} = ${primaryKeyPlaceholder}${scopeSuffix}${returningSuffix(returningColumns)}`,
657
661
  params
658
662
  };
659
663
  }
@@ -666,9 +670,12 @@ function buildSoftDeleteByIdQuery(table, id, deletedAt) {
666
670
  const scopeClauses = [];
667
671
  appendSoftDeleteScope(table, {}, scopeClauses);
668
672
  const scopeSuffix = scopeClauses.length > 0 ? ` AND ${scopeClauses.join(" AND ")}` : "";
673
+ const params = [];
674
+ const deletedAtPlaceholder = pushParam(params, deletedAt);
675
+ const idPlaceholder = pushParam(params, id);
669
676
  return {
670
- text: `UPDATE ${quoteIdentifier(table.name)} SET ${quoteIdentifier(deletedAtColumn)} = $1 WHERE ${quoteIdentifier(table.primaryKey)} = $2${scopeSuffix} RETURNING ${returningColumns}`,
671
- params: [deletedAt, id]
677
+ text: `UPDATE ${quoteIdentifier(table.name)} SET ${quoteIdentifier(deletedAtColumn)} = ${deletedAtPlaceholder} WHERE ${quoteIdentifier(table.primaryKey)} = ${idPlaceholder}${scopeSuffix}${returningSuffix(returningColumns)}`,
678
+ params
672
679
  };
673
680
  }
674
681
  function buildRestoreByIdQuery(table, id) {
@@ -677,15 +684,21 @@ function buildRestoreByIdQuery(table, id) {
677
684
  throw new Error(`Table ${table.name} does not support soft deletes.`);
678
685
  }
679
686
  const returningColumns = buildReturningColumns(table);
687
+ const params = [];
688
+ const deletedAtPlaceholder = pushParam(params, null);
689
+ const idPlaceholder = pushParam(params, id);
680
690
  return {
681
- text: `UPDATE ${quoteIdentifier(table.name)} SET ${quoteIdentifier(deletedAtColumn)} = $1 WHERE ${quoteIdentifier(table.primaryKey)} = $2 AND ${qualifyColumn(table.name, deletedAtColumn)} IS NOT NULL RETURNING ${returningColumns}`,
682
- params: [null, id]
691
+ text: `UPDATE ${quoteIdentifier(table.name)} SET ${quoteIdentifier(deletedAtColumn)} = ${deletedAtPlaceholder} WHERE ${quoteIdentifier(table.primaryKey)} = ${idPlaceholder} AND ${qualifyColumn(table.name, deletedAtColumn)} IS NOT NULL${returningSuffix(returningColumns)}`,
692
+ params
683
693
  };
684
694
  }
685
695
  function buildDeleteByIdQuery(table, id) {
696
+ const params = [];
697
+ const idPlaceholder = pushParam(params, id);
698
+ const returning = returningSuffix(`${quoteIdentifier(table.primaryKey)} AS ${quoteIdentifier("deleted_id")}`);
686
699
  return {
687
- text: `DELETE FROM ${quoteIdentifier(table.name)} WHERE ${quoteIdentifier(table.primaryKey)} = $1 RETURNING ${quoteIdentifier(table.primaryKey)} AS ${quoteIdentifier("deleted_id")}`,
688
- params: [id]
700
+ text: `DELETE FROM ${quoteIdentifier(table.name)} WHERE ${quoteIdentifier(table.primaryKey)} = ${idPlaceholder}${returning}`,
701
+ params
689
702
  };
690
703
  }
691
704
 
@@ -0,0 +1,34 @@
1
+ // @bun
2
+ // ../../src/core/database/sqliteConnection.ts
3
+ import { Database } from "bun:sqlite";
4
+ function isRowReturning(sql) {
5
+ const upper = sql.replace(/\s+/g, " ").trim().toUpperCase();
6
+ if (upper.includes(" RETURNING ")) {
7
+ return true;
8
+ }
9
+ return upper.startsWith("SELECT") || upper.startsWith("WITH") || upper.startsWith("PRAGMA") || upper.startsWith("EXPLAIN");
10
+ }
11
+ function createSqliteConnection(filename) {
12
+ if (!filename.trim()) {
13
+ throw new Error("SQLite path is not configured. Pass a filename or :memory:.");
14
+ }
15
+ const db = new Database(filename, { create: true });
16
+ db.exec("PRAGMA foreign_keys = ON");
17
+ return {
18
+ async unsafe(query, params = []) {
19
+ const statement = db.query(query);
20
+ const args = [...params];
21
+ if (isRowReturning(query)) {
22
+ return statement.all(...args);
23
+ }
24
+ statement.run(...args);
25
+ return [];
26
+ },
27
+ close() {
28
+ db.close();
29
+ }
30
+ };
31
+ }
32
+ export {
33
+ createSqliteConnection
34
+ };
@@ -226,7 +226,7 @@ class LogMailDriver {
226
226
  to: message.to,
227
227
  subject: message.subject,
228
228
  body: message.body,
229
- ...message.html ? { html: message.html } : {}
229
+ ...message.html ? { htmlBytes: Buffer.byteLength(message.html, "utf8") } : {}
230
230
  }));
231
231
  }
232
232
  }
@@ -7,6 +7,10 @@ function requestPrefersJson(request) {
7
7
  if (request.headers.get("HX-Request") === "true") {
8
8
  return false;
9
9
  }
10
+ const pathname = new URL(request.url).pathname;
11
+ if (pathname.startsWith("/api/")) {
12
+ return true;
13
+ }
10
14
  const accept = request.headers.get("accept")?.toLowerCase() ?? "";
11
15
  if (accept.includes("text/html")) {
12
16
  return false;
@@ -18,8 +22,7 @@ function requestPrefersJson(request) {
18
22
  if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
19
23
  return false;
20
24
  }
21
- const pathname = new URL(request.url).pathname;
22
- return pathname.startsWith("/api/");
25
+ return false;
23
26
  }
24
27
  export {
25
28
  requestPrefersJson
@@ -195,6 +195,48 @@ function resolveCsrfTokenForRequest(request) {
195
195
  return resolveCsrfToken(request).token;
196
196
  }
197
197
 
198
+ // ../../src/core/http/statelessAuth.ts
199
+ function authorizationScheme(request) {
200
+ const header = request.headers.get("authorization")?.trim() ?? "";
201
+ const scheme = header.split(/\s+/, 1)[0];
202
+ return scheme ? scheme.toLowerCase() : "";
203
+ }
204
+ function requestUsesHeaderCredentials(request) {
205
+ const scheme = authorizationScheme(request);
206
+ return scheme === "bearer" || scheme === "basic";
207
+ }
208
+ function readBearerToken(request) {
209
+ const header = request.headers.get("authorization")?.trim() ?? "";
210
+ if (!header.toLowerCase().startsWith("bearer ")) {
211
+ return null;
212
+ }
213
+ const token = header.slice("Bearer ".length).trim();
214
+ return token.length > 0 ? token : null;
215
+ }
216
+ function readBasicCredentials(request) {
217
+ const header = request.headers.get("authorization")?.trim() ?? "";
218
+ if (!header.toLowerCase().startsWith("basic ")) {
219
+ return null;
220
+ }
221
+ const encoded = header.slice("Basic ".length).trim();
222
+ if (!encoded) {
223
+ return null;
224
+ }
225
+ try {
226
+ const decoded = Buffer.from(encoded, "base64").toString("utf8");
227
+ const separator = decoded.indexOf(":");
228
+ if (separator < 0) {
229
+ return null;
230
+ }
231
+ return {
232
+ username: decoded.slice(0, separator),
233
+ password: decoded.slice(separator + 1)
234
+ };
235
+ } catch {
236
+ return null;
237
+ }
238
+ }
239
+
198
240
  // ../../src/core/http/csrfMiddleware.ts
199
241
  var MUTATING_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]);
200
242
  function appendSetCookie(response, cookie) {
@@ -208,6 +250,9 @@ function appendSetCookie(response, cookie) {
208
250
  }
209
251
  function createCsrfMiddleware() {
210
252
  return async (request, next) => {
253
+ if (requestUsesHeaderCredentials(request)) {
254
+ return await next();
255
+ }
211
256
  const method = request.method.toUpperCase();
212
257
  if (!MUTATING_METHODS.has(method)) {
213
258
  const csrf = resolveCsrfToken(request);