@aotter/mantle-runtime 0.1.0-alpha.9 → 0.1.2-alpha.2

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 (155) hide show
  1. package/README.md +74 -4
  2. package/dist/MantleRuntime.js +14 -6
  3. package/dist/MantleRuntime.js.map +1 -1
  4. package/dist/domain/model/EntryRow.d.ts +16 -16
  5. package/dist/domain/model/EntryRow.d.ts.map +1 -1
  6. package/dist/domain/model/EntryRow.js +34 -0
  7. package/dist/domain/model/EntryRow.js.map +1 -1
  8. package/dist/domain/port/DatabaseDriver.d.ts +2 -2
  9. package/dist/domain/port/DatabaseDriver.d.ts.map +1 -1
  10. package/dist/domain/port/EntryReader.d.ts +38 -3
  11. package/dist/domain/port/EntryReader.d.ts.map +1 -1
  12. package/dist/domain/port/EntryRepository.d.ts +13 -6
  13. package/dist/domain/port/EntryRepository.d.ts.map +1 -1
  14. package/dist/domain/port/index.d.ts +2 -2
  15. package/dist/domain/port/index.d.ts.map +1 -1
  16. package/dist/domain/port/index.js.map +1 -1
  17. package/dist/domain/service/CallableCapabilityProjector.d.ts +28 -0
  18. package/dist/domain/service/CallableCapabilityProjector.d.ts.map +1 -0
  19. package/dist/domain/service/CallableCapabilityProjector.js +67 -0
  20. package/dist/domain/service/CallableCapabilityProjector.js.map +1 -0
  21. package/dist/domain/service/EntryMutationDiagnostics.d.ts.map +1 -1
  22. package/dist/domain/service/EntryMutationDiagnostics.js +11 -1
  23. package/dist/domain/service/EntryMutationDiagnostics.js.map +1 -1
  24. package/dist/domain/service/PathMatcher.d.ts +7 -0
  25. package/dist/domain/service/PathMatcher.d.ts.map +1 -1
  26. package/dist/domain/service/PathMatcher.js +56 -0
  27. package/dist/domain/service/PathMatcher.js.map +1 -1
  28. package/dist/domain/service/RuntimePlanCompiler.d.ts +7 -2
  29. package/dist/domain/service/RuntimePlanCompiler.d.ts.map +1 -1
  30. package/dist/domain/service/RuntimePlanCompiler.js +1 -0
  31. package/dist/domain/service/RuntimePlanCompiler.js.map +1 -1
  32. package/dist/domain/service/index.d.ts +1 -0
  33. package/dist/domain/service/index.d.ts.map +1 -1
  34. package/dist/domain/service/index.js +1 -0
  35. package/dist/domain/service/index.js.map +1 -1
  36. package/dist/domain/service/io/JoinedEntryReader.d.ts.map +1 -1
  37. package/dist/domain/service/io/JoinedEntryReader.js +2 -3
  38. package/dist/domain/service/io/JoinedEntryReader.js.map +1 -1
  39. package/dist/index.d.ts +12 -4
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +10 -2
  42. package/dist/index.js.map +1 -1
  43. package/dist/infrastructure/boot/SqliteMigrationRunner.d.ts +1 -0
  44. package/dist/infrastructure/boot/SqliteMigrationRunner.d.ts.map +1 -1
  45. package/dist/infrastructure/boot/SqliteMigrationRunner.js +71 -12
  46. package/dist/infrastructure/boot/SqliteMigrationRunner.js.map +1 -1
  47. package/dist/infrastructure/boot/bootState.d.ts +2 -0
  48. package/dist/infrastructure/boot/bootState.d.ts.map +1 -1
  49. package/dist/infrastructure/boot/bootState.js +3 -1
  50. package/dist/infrastructure/boot/bootState.js.map +1 -1
  51. package/dist/infrastructure/boot/canonicalMigrations.d.ts +4 -15
  52. package/dist/infrastructure/boot/canonicalMigrations.d.ts.map +1 -1
  53. package/dist/infrastructure/boot/canonicalMigrations.js +14 -200
  54. package/dist/infrastructure/boot/canonicalMigrations.js.map +1 -1
  55. package/dist/infrastructure/boot/index.d.ts +2 -2
  56. package/dist/infrastructure/boot/index.d.ts.map +1 -1
  57. package/dist/infrastructure/boot/index.js +2 -2
  58. package/dist/infrastructure/boot/index.js.map +1 -1
  59. package/dist/infrastructure/http/createMantleRequestHandler.d.ts +3 -0
  60. package/dist/infrastructure/http/createMantleRequestHandler.d.ts.map +1 -1
  61. package/dist/infrastructure/http/createMantleRequestHandler.js +36 -15
  62. package/dist/infrastructure/http/createMantleRequestHandler.js.map +1 -1
  63. package/dist/infrastructure/http/readJsonBody.d.ts +8 -0
  64. package/dist/infrastructure/http/readJsonBody.d.ts.map +1 -0
  65. package/dist/infrastructure/http/readJsonBody.js +36 -0
  66. package/dist/infrastructure/http/readJsonBody.js.map +1 -0
  67. package/dist/infrastructure/mcp/McpJsonRpcDispatcher.d.ts +22 -17
  68. package/dist/infrastructure/mcp/McpJsonRpcDispatcher.d.ts.map +1 -1
  69. package/dist/infrastructure/mcp/McpJsonRpcDispatcher.js +89 -86
  70. package/dist/infrastructure/mcp/McpJsonRpcDispatcher.js.map +1 -1
  71. package/dist/infrastructure/mcp/McpToolCatalog.d.ts +11 -9
  72. package/dist/infrastructure/mcp/McpToolCatalog.d.ts.map +1 -1
  73. package/dist/infrastructure/mcp/McpToolCatalog.js +70 -80
  74. package/dist/infrastructure/mcp/McpToolCatalog.js.map +1 -1
  75. package/dist/infrastructure/persistence/DatabaseEntryRepository.d.ts +15 -20
  76. package/dist/infrastructure/persistence/DatabaseEntryRepository.d.ts.map +1 -1
  77. package/dist/infrastructure/persistence/DatabaseEntryRepository.js +291 -331
  78. package/dist/infrastructure/persistence/DatabaseEntryRepository.js.map +1 -1
  79. package/dist/infrastructure/persistence/DatabaseSiteConfigRepository.d.ts +2 -0
  80. package/dist/infrastructure/persistence/DatabaseSiteConfigRepository.d.ts.map +1 -1
  81. package/dist/infrastructure/persistence/DatabaseSiteConfigRepository.js +5 -1
  82. package/dist/infrastructure/persistence/DatabaseSiteConfigRepository.js.map +1 -1
  83. package/dist/infrastructure/persistence/LifecycleHookingEntryRepository.d.ts +2 -2
  84. package/dist/infrastructure/persistence/LifecycleHookingEntryRepository.d.ts.map +1 -1
  85. package/dist/infrastructure/persistence/LifecycleHookingEntryRepository.js +5 -5
  86. package/dist/infrastructure/persistence/LifecycleHookingEntryRepository.js.map +1 -1
  87. package/dist/infrastructure/persistence/Pagination.d.ts +6 -0
  88. package/dist/infrastructure/persistence/Pagination.d.ts.map +1 -1
  89. package/dist/infrastructure/persistence/Pagination.js +25 -0
  90. package/dist/infrastructure/persistence/Pagination.js.map +1 -1
  91. package/dist/infrastructure/storage/SqliteMantleStorageAdapter.d.ts +15 -1
  92. package/dist/infrastructure/storage/SqliteMantleStorageAdapter.d.ts.map +1 -1
  93. package/dist/infrastructure/storage/SqliteMantleStorageAdapter.js +64 -9
  94. package/dist/infrastructure/storage/SqliteMantleStorageAdapter.js.map +1 -1
  95. package/dist/infrastructure/storage/SqliteMigrationArtifact.d.ts +19 -0
  96. package/dist/infrastructure/storage/SqliteMigrationArtifact.d.ts.map +1 -0
  97. package/dist/infrastructure/storage/SqliteMigrationArtifact.js +41 -0
  98. package/dist/infrastructure/storage/SqliteMigrationArtifact.js.map +1 -0
  99. package/dist/infrastructure/storage/SqliteSchemaTables.d.ts +23 -0
  100. package/dist/infrastructure/storage/SqliteSchemaTables.d.ts.map +1 -0
  101. package/dist/infrastructure/storage/SqliteSchemaTables.js +244 -0
  102. package/dist/infrastructure/storage/SqliteSchemaTables.js.map +1 -0
  103. package/dist/infrastructure/storage/SqliteViewCompiler.d.ts +3 -5
  104. package/dist/infrastructure/storage/SqliteViewCompiler.d.ts.map +1 -1
  105. package/dist/infrastructure/storage/SqliteViewCompiler.js +20 -51
  106. package/dist/infrastructure/storage/SqliteViewCompiler.js.map +1 -1
  107. package/dist/infrastructure/testing/HttpBenchmarkHarness.d.ts +26 -1
  108. package/dist/infrastructure/testing/HttpBenchmarkHarness.d.ts.map +1 -1
  109. package/dist/infrastructure/testing/HttpBenchmarkHarness.js +27 -12
  110. package/dist/infrastructure/testing/HttpBenchmarkHarness.js.map +1 -1
  111. package/dist/infrastructure/testing/IndexCoverageHarness.d.ts.map +1 -1
  112. package/dist/infrastructure/testing/IndexCoverageHarness.js +19 -31
  113. package/dist/infrastructure/testing/IndexCoverageHarness.js.map +1 -1
  114. package/dist/infrastructure/testing/StorageConformance.d.ts +32 -0
  115. package/dist/infrastructure/testing/StorageConformance.d.ts.map +1 -0
  116. package/dist/infrastructure/testing/StorageConformance.js +306 -0
  117. package/dist/infrastructure/testing/StorageConformance.js.map +1 -0
  118. package/dist/testing-storage.d.ts +3 -0
  119. package/dist/testing-storage.d.ts.map +1 -0
  120. package/dist/testing-storage.js +3 -0
  121. package/dist/testing-storage.js.map +1 -0
  122. package/dist/usecase/content/ArchiveUseCase.js +4 -4
  123. package/dist/usecase/content/ArchiveUseCase.js.map +1 -1
  124. package/dist/usecase/content/DeleteEntryUseCase.js +4 -4
  125. package/dist/usecase/content/DeleteEntryUseCase.js.map +1 -1
  126. package/dist/usecase/content/GetEntryUseCase.d.ts +1 -2
  127. package/dist/usecase/content/GetEntryUseCase.d.ts.map +1 -1
  128. package/dist/usecase/content/GetEntryUseCase.js +2 -6
  129. package/dist/usecase/content/GetEntryUseCase.js.map +1 -1
  130. package/dist/usecase/content/ListEntriesUseCase.d.ts +1 -1
  131. package/dist/usecase/content/ListEntriesUseCase.d.ts.map +1 -1
  132. package/dist/usecase/content/ListEntriesUseCase.js +18 -2
  133. package/dist/usecase/content/ListEntriesUseCase.js.map +1 -1
  134. package/dist/usecase/content/RequestPublishUseCase.js +4 -4
  135. package/dist/usecase/content/RequestPublishUseCase.js.map +1 -1
  136. package/dist/usecase/content/UnpublishUseCase.js +4 -4
  137. package/dist/usecase/content/UnpublishUseCase.js.map +1 -1
  138. package/dist/usecase/content/UpdateDraftUseCase.js +4 -4
  139. package/dist/usecase/content/UpdateDraftUseCase.js.map +1 -1
  140. package/dist/usecase/dto/content/index.d.ts +8 -5
  141. package/dist/usecase/dto/content/index.d.ts.map +1 -1
  142. package/dist/usecase/media/DeleteMediaAssetUseCase.d.ts +3 -3
  143. package/dist/usecase/media/DeleteMediaAssetUseCase.d.ts.map +1 -1
  144. package/dist/usecase/media/DeleteMediaAssetUseCase.js +35 -19
  145. package/dist/usecase/media/DeleteMediaAssetUseCase.js.map +1 -1
  146. package/dist/usecase/procedure/InvokeBuiltinUseCase.d.ts +8 -2
  147. package/dist/usecase/procedure/InvokeBuiltinUseCase.d.ts.map +1 -1
  148. package/dist/usecase/procedure/InvokeBuiltinUseCase.js +98 -16
  149. package/dist/usecase/procedure/InvokeBuiltinUseCase.js.map +1 -1
  150. package/dist/usecase/procedure/InvokeProcedureUseCase.js +2 -2
  151. package/dist/usecase/procedure/InvokeProcedureUseCase.js.map +1 -1
  152. package/dist/usecase/site/UpdateSiteSettingsUseCase.d.ts.map +1 -1
  153. package/dist/usecase/site/UpdateSiteSettingsUseCase.js +8 -1
  154. package/dist/usecase/site/UpdateSiteSettingsUseCase.js.map +1 -1
  155. package/package.json +9 -5
@@ -1,18 +1,9 @@
1
- import { schemaIndexedFieldSql, } from "@aotter/mantle-spec";
1
+ import { checkSchemaAdminUi, } from "@aotter/mantle-spec";
2
+ import { EntryStatusConflict, EntryUniqueConflict, EntryVersionConflict, liftLocale, projectPublicEntry, } from "../../domain/model/EntryRow.js";
2
3
  import { clampLimit } from "../../domain/service/Pagination.js";
3
- import { EntryStatusConflict, EntryVersionConflict, liftLocale, projectPublicEntry, } from "../../domain/model/EntryRow.js";
4
- import { decodeEntrySortCursor, encodeEntrySortCursor, escapeLikeTerm, } from "./Pagination.js";
5
- /**
6
- * `EntryRepository` impl backed by `DatabaseDriver`. Adapters that
7
- * implement the SQLite-shaped `DatabaseDriver` contract get this
8
- * repository for free.
9
- *
10
- * `UPDATE … RETURNING` collapses the post-write SELECT to one round
11
- * trip on SQLite ≥ 3.35 / Postgres.
12
- *
13
- * Lifts `data.locale` to `EntryRow.locale` at the rowFromDb boundary
14
- * — see ADR-0010 + `domain/model/EntryRow.ts`.
15
- */
4
+ import { decodeEntrySortCursor, encodeEntrySortCursor, escapeLikeTerm, PUBLISHED_PAGE_DATA_BUDGET, publishedPageLimit, } from "./Pagination.js";
5
+ import { decodeField, encodeField, fieldColumn, fieldSql, isNullableJsonSchema, sqliteSchemaTable, } from "../storage/SqliteSchemaTables.js";
6
+ /** SQLite/D1 repository where each Schema is one physical table. */
16
7
  export class DatabaseEntryRepository {
17
8
  db;
18
9
  schemasByName;
@@ -20,12 +11,41 @@ export class DatabaseEntryRepository {
20
11
  this.db = db;
21
12
  this.schemasByName = schemasByName;
22
13
  }
14
+ async readCreationStatistics(args) {
15
+ const { from, to, bucketMs } = args;
16
+ if (![from, to, bucketMs].every(Number.isSafeInteger) || from < 0 || to <= from ||
17
+ bucketMs <= 0 || to - from > 20 * 86_400_000 || Math.ceil((to - from) / bucketMs) > 480) {
18
+ throw new RangeError("Statistics require a positive window <= 20 days and <= 480 buckets.");
19
+ }
20
+ const table = this.table(args.collection);
21
+ const filter = checkSchemaAdminUi(table.schema).filter;
22
+ const subtypeField = filter ? requiredFieldSql(table.schema, filter.field) : null;
23
+ const subtype = filter && subtypeField
24
+ ? `CASE WHEN ${subtypeField} IN (${filter.values.map(() => "?").join(", ")}) THEN ${subtypeField} ELSE NULL END`
25
+ : "NULL";
26
+ const total = await this.db.prepare(`SELECT COUNT(*) AS count FROM ${table.table}`)
27
+ .first();
28
+ const rows = await this.db.prepare(`SELECT CAST(("_mantle_created_at" - ?) / ? AS INTEGER) AS bucket,
29
+ ${subtype} AS subtype, COUNT(*) AS count FROM ${table.table}
30
+ WHERE "_mantle_created_at" >= ? AND "_mantle_created_at" < ? GROUP BY bucket, subtype`)
31
+ .bind(from, bucketMs, ...(filter ? filter.values : []), from, to)
32
+ .all();
33
+ return { total: total?.count ?? 0, buckets: rows };
34
+ }
23
35
  async create(args) {
24
- await this.db
25
- .prepare(`INSERT INTO entries (id, collection, status, version, data, author_id, created_at, updated_at)
26
- VALUES (?, ?, ?, 1, ?, ?, ?, ?)`)
27
- .bind(args.id, args.collection, args.status, JSON.stringify(args.data), args.authorId, args.now, args.now)
28
- .run();
36
+ const table = this.table(args.collection);
37
+ const columns = ["_mantle_id", "_mantle_status", "_mantle_version", "_mantle_author_id", "_mantle_created_at", "_mantle_updated_at", ...table.fields];
38
+ const values = [args.id, args.status, 1, args.authorId, args.now, args.now, ...this.encodedData(table, args.data)];
39
+ try {
40
+ await this.db.prepare(`INSERT INTO ${table.table} (${columns.map(quote).join(", ")}) VALUES (${columns.map(() => "?").join(", ")})`)
41
+ .bind(...values).run();
42
+ }
43
+ catch (error) {
44
+ if (isDriverUniqueConstraintError(error)) {
45
+ throw new EntryUniqueConflict(args.collection, args.data, error.message);
46
+ }
47
+ throw error;
48
+ }
29
49
  return {
30
50
  id: args.id,
31
51
  collection: args.collection,
@@ -38,408 +58,348 @@ export class DatabaseEntryRepository {
38
58
  updatedAt: args.now,
39
59
  };
40
60
  }
41
- async get(id) {
42
- const row = await this.db
43
- .prepare(`SELECT id, collection, status, version, data, author_id, created_at, updated_at
44
- FROM entries WHERE id = ?`)
45
- .bind(id)
46
- .first();
47
- return row ? rowFromDb(row) : null;
61
+ async get(args) {
62
+ const table = this.table(args.collection);
63
+ const row = await this.db.prepare(`SELECT ${table.selectColumns} FROM ${table.table} WHERE "_mantle_id" = ?`)
64
+ .bind(args.id).first();
65
+ return row ? rowFromDb(table, row) : null;
48
66
  }
49
67
  async update(args) {
50
- const newVersion = args.expectedVersion + 1;
51
- const row = await this.db
52
- .prepare(`UPDATE entries SET data = ?, version = ?, updated_at = ?
53
- WHERE id = ? AND version = ?
54
- RETURNING id, collection, status, version, data, author_id, created_at, updated_at`)
55
- .bind(JSON.stringify(args.data), newVersion, args.now, args.id, args.expectedVersion)
56
- .first();
68
+ const table = this.table(args.collection);
69
+ const version = args.expectedVersion + 1;
70
+ const assignments = [...table.fields.map((field) => `${quote(field)} = ?`), '"_mantle_version" = ?', '"_mantle_updated_at" = ?'];
71
+ let row = null;
72
+ try {
73
+ row = await this.db.prepare(`UPDATE ${table.table} SET ${assignments.join(", ")}
74
+ WHERE "_mantle_id" = ? AND "_mantle_version" = ? RETURNING ${table.selectColumns}`)
75
+ .bind(...this.encodedData(table, args.data), version, args.now, args.id, args.expectedVersion)
76
+ .first();
77
+ }
78
+ catch (error) {
79
+ if (isDriverUniqueConstraintError(error)) {
80
+ throw new EntryUniqueConflict(args.collection, args.data, error.message);
81
+ }
82
+ throw error;
83
+ }
57
84
  if (!row)
58
- throw await this.versionConflict(args.id, args.expectedVersion);
59
- return rowFromDb(row);
85
+ throw await this.versionConflict(table, args.id, args.expectedVersion);
86
+ return rowFromDb(table, row);
60
87
  }
61
88
  async delete(args) {
62
- const parentMatches = `id = ? AND collection = ? AND status = ? AND version = ?`;
63
- const parentSnapshot = [
64
- args.id,
65
- args.collection,
66
- args.expectedStatus,
67
- args.expectedVersion,
68
- ];
69
- const result = await this.db
70
- .prepare(`DELETE FROM entries WHERE ${parentMatches}`)
71
- .bind(...parentSnapshot)
72
- .run();
89
+ const table = this.table(args.collection);
90
+ const result = await this.db.prepare(`DELETE FROM ${table.table} WHERE "_mantle_id" = ? AND "_mantle_status" = ? AND "_mantle_version" = ?`)
91
+ .bind(args.id, args.expectedStatus, args.expectedVersion).run();
73
92
  if (result.meta.changes > 0)
74
93
  return { removed: true };
75
- const after = await this.db
76
- .prepare(`SELECT collection, status, version FROM entries WHERE id = ?`)
77
- .bind(args.id)
78
- .first();
79
- if (!after || after.collection !== args.collection)
94
+ const after = await this.db.prepare(`SELECT "_mantle_status" AS "status", "_mantle_version" AS "version" FROM ${table.table} WHERE "_mantle_id" = ?`)
95
+ .bind(args.id).first();
96
+ if (!after)
80
97
  return { removed: false };
81
- if (after.version !== args.expectedVersion) {
98
+ if (after.version !== args.expectedVersion)
82
99
  throw new EntryVersionConflict(args.id, args.expectedVersion, after.version);
83
- }
84
100
  throw new EntryStatusConflict(args.id, args.expectedStatus, after.status);
85
101
  }
86
102
  async transitionStatus(args) {
87
- const { expectedStatus, expectedVersion } = args;
88
- const guards = [];
103
+ const table = this.table(args.collection);
104
+ const conditions = ['"_mantle_id" = ?'];
89
105
  const binds = [args.to, args.now, args.id];
90
- if (expectedStatus !== undefined) {
91
- guards.push(" AND status = ?");
92
- binds.push(expectedStatus);
106
+ if (args.expectedStatus !== undefined) {
107
+ conditions.push('"_mantle_status" = ?');
108
+ binds.push(args.expectedStatus);
93
109
  }
94
- if (expectedVersion !== undefined) {
95
- guards.push(" AND version = ?");
96
- binds.push(expectedVersion);
110
+ if (args.expectedVersion !== undefined) {
111
+ conditions.push('"_mantle_version" = ?');
112
+ binds.push(args.expectedVersion);
97
113
  }
98
- const row = await this.db
99
- .prepare(`UPDATE entries SET status = ?, version = version + 1, updated_at = ?
100
- WHERE id = ?${guards.join("")}
101
- RETURNING id, collection, status, version, data, author_id, created_at, updated_at`)
102
- .bind(...binds)
103
- .first();
114
+ const row = await this.db.prepare(`UPDATE ${table.table} SET "_mantle_status" = ?, "_mantle_version" = "_mantle_version" + 1, "_mantle_updated_at" = ?
115
+ WHERE ${conditions.join(" AND ")} RETURNING ${table.selectColumns}`).bind(...binds).first();
104
116
  if (row)
105
- return rowFromDb(row);
106
- // Disambiguate version- vs. status-conflict from a single SELECT —
107
- // splitting into two SELECTs leaves a TOCTOU window where a third
108
- // concurrent writer between the two reads can flip which guard
109
- // appears to have failed.
110
- const after = await this.db
111
- .prepare(`SELECT version, status FROM entries WHERE id = ?`)
112
- .bind(args.id)
113
- .first();
114
- if (expectedVersion !== undefined && after && after.version !== expectedVersion) {
115
- throw new EntryVersionConflict(args.id, expectedVersion, after.version);
117
+ return rowFromDb(table, row);
118
+ const after = await this.db.prepare(`SELECT "_mantle_status" AS "status", "_mantle_version" AS "version" FROM ${table.table} WHERE "_mantle_id" = ?`)
119
+ .bind(args.id).first();
120
+ if (args.expectedVersion !== undefined && after && after.version !== args.expectedVersion) {
121
+ throw new EntryVersionConflict(args.id, args.expectedVersion, after.version);
116
122
  }
117
- throw new EntryStatusConflict(args.id, expectedStatus ?? args.to, after?.status ?? args.to);
123
+ throw new EntryStatusConflict(args.id, args.expectedStatus ?? args.to, after?.status ?? args.to);
118
124
  }
119
125
  async list(args) {
120
- // Use the shared clamp so direct repo callers (tests, future
121
- // adapters that bypass the use case) get the same default page
122
- // size as ListEntriesUseCase — not a silently different 100.
126
+ const table = this.table(args.collection);
123
127
  const limit = clampLimit(args.limit);
124
128
  const sort = args.sort ?? { field: "updatedAt", direction: "desc" };
125
- const schema = this.schemasByName.get(args.collection);
126
- const sortSql = entrySortSql(schema, sort.field);
127
- if (!sortSql)
128
- throw new Error(`unavailable entry sort field: ${sort.field}`);
129
+ const sortSql = args.sort ? requiredFieldSql(table.schema, sort.field) : '"_mantle_updated_at"';
129
130
  const cursor = decodeEntrySortCursor(args.cursor, sort.field, sort.direction);
130
131
  const backward = args.cursorDirection === "backward" && cursor !== null;
131
- const queryDirection = backward
132
- ? (sort.direction === "asc" ? "DESC" : "ASC")
133
- : sort.direction.toUpperCase();
134
- // Fetch limit+1 to detect a next page without a second query —
135
- // the extra row never reaches the caller.
136
- const probe = limit + 1;
137
- const conditions = ["collection = ?"];
138
- const binds = [args.collection];
132
+ const direction = backward ? (sort.direction === "asc" ? "DESC" : "ASC") : sort.direction.toUpperCase();
133
+ const conditions = [];
134
+ const binds = [];
139
135
  if (args.status) {
140
- conditions.push("status = ?");
136
+ conditions.push('"_mantle_status" = ?');
141
137
  binds.push(args.status);
142
138
  }
143
139
  if (args.search) {
144
140
  const term = escapeLikeTerm(args.search);
145
- const searchConditions = ["id LIKE '%'||?||'%' ESCAPE '\\'"];
141
+ const search = ['"_mantle_id" LIKE \'%\'||?||\'%\' ESCAPE \'\\\''];
146
142
  binds.push(term);
147
143
  for (const field of args.searchFields ?? []) {
148
- searchConditions.push("json_extract(data, ?) LIKE '%'||?||'%' ESCAPE '\\'");
149
- binds.push(jsonPathForTopLevelField(field), term);
144
+ search.push(`${requiredFieldSql(table.schema, field)} LIKE '%'||?||'%' ESCAPE '\\'`);
145
+ binds.push(term);
150
146
  }
151
- conditions.push(`(${searchConditions.join(" OR ")})`);
147
+ conditions.push(`(${search.join(" OR ")})`);
152
148
  }
153
149
  if (args.filter) {
154
- const compiled = compileDataPredicates(schema, [{
155
- field: args.filter.field,
156
- kind: "equal",
157
- value: args.filter.value,
158
- }]);
159
- conditions.push(...compiled.conditions);
160
- binds.push(...compiled.binds);
150
+ conditions.push(`${requiredFieldSql(table.schema, args.filter.field)} = ?`);
151
+ binds.push(encodeScalar(table.schema, args.filter.field, args.filter.value));
152
+ }
153
+ if (args.scope) {
154
+ conditions.push(`${requiredFieldSql(table.schema, args.scope.field)} = ?`);
155
+ binds.push(encodeScalar(table.schema, args.scope.field, args.scope.value));
161
156
  }
162
157
  if (cursor) {
163
- const comparison = backward
164
- ? (sort.direction === "asc" ? "<" : ">")
165
- : (sort.direction === "asc" ? ">" : "<");
166
- conditions.push(`(${sortSql}, id) ${comparison} (?, ?)`);
158
+ const comparison = backward ? (sort.direction === "asc" ? "<" : ">") : (sort.direction === "asc" ? ">" : "<");
159
+ conditions.push(`(${sortSql}, "_mantle_id") ${comparison} (?, ?)`);
167
160
  binds.push(...cursor);
168
161
  }
169
- binds.push(probe);
170
- const stmt = this.db
171
- .prepare(`SELECT id, collection, status, version, data, author_id, created_at, updated_at
172
- FROM entries WHERE ${conditions.join(" AND ")}
173
- ORDER BY ${sortSql} ${queryDirection}, id ${queryDirection} LIMIT ?`)
174
- .bind(...binds);
175
- const rows = await stmt.all();
162
+ binds.push(limit + 1);
163
+ const rows = await this.db.prepare(`SELECT ${table.selectColumns} FROM ${table.table}
164
+ ${conditions.length ? `WHERE ${conditions.join(" AND ")}` : ""}
165
+ ORDER BY ${sortSql} ${direction}, "_mantle_id" ${direction} LIMIT ?`).bind(...binds).all();
176
166
  const hasMore = rows.length > limit;
177
167
  const page = [...(hasMore ? rows.slice(0, limit) : rows)];
178
168
  if (backward)
179
169
  page.reverse();
180
170
  const first = page[0];
181
- const last = page[page.length - 1];
171
+ const last = page.at(-1);
182
172
  return {
183
- rows: page.map(rowFromDb),
184
- previousCursor: first && (backward ? hasMore : cursor !== null)
185
- ? encodeEntrySortCursor(sort.field, sort.direction, entrySortValueFromDb(first, sort.field), first.id)
186
- : undefined,
187
- nextCursor: last && (backward ? cursor !== null : hasMore)
188
- ? encodeEntrySortCursor(sort.field, sort.direction, entrySortValueFromDb(last, sort.field), last.id)
189
- : undefined,
173
+ rows: page.map((row) => rowFromDb(table, row)),
174
+ previousCursor: first && (backward ? hasMore : cursor !== null) ? encodeEntrySortCursor(sort.field, sort.direction, sortValue(first, table.schema, sort.field), first._mantle_id) : undefined,
175
+ nextCursor: last && (backward ? cursor !== null : hasMore) ? encodeEntrySortCursor(sort.field, sort.direction, sortValue(last, table.schema, sort.field), last._mantle_id) : undefined,
190
176
  };
191
177
  }
192
178
  async findByDataField(args) {
193
- return this.findOneByDataFields({
194
- collection: args.collection,
195
- status: args.status,
196
- fields: { [args.field]: args.value },
197
- });
179
+ return this.findOne({ ...args, fields: { [args.field]: args.value } });
198
180
  }
199
181
  async findByDataFields(args) {
200
- return this.findOneByDataFields(args);
182
+ return this.findOne(args);
201
183
  }
202
- async readById(id) {
203
- const row = await this.get(id);
184
+ async readById(args) {
185
+ const row = await this.get(args);
204
186
  return row ? projectPublicEntry(row) : null;
205
187
  }
206
188
  async readBySlug(args) {
207
- return this.readByDataField({
208
- collection: args.collection,
209
- field: "slug",
210
- value: args.slug,
211
- locale: args.locale,
212
- status: args.status,
213
- });
189
+ return this.readByDataField({ ...args, field: "slug", value: args.slug });
214
190
  }
215
191
  async readByDataField(args) {
216
- const row = await this.findOneByDataFields({
217
- collection: args.collection,
218
- status: args.status,
219
- fields: { [args.field]: args.value },
220
- locale: args.locale,
221
- });
192
+ const row = await this.findOne({ ...args, fields: { [args.field]: args.value } });
222
193
  return row ? projectPublicEntry(row) : null;
223
194
  }
224
195
  async readByDataFieldIn(args) {
225
196
  const values = [...new Set(args.values)];
226
- if (values.length === 0)
197
+ if (!values.length)
227
198
  return [];
228
- const entries = [];
229
- for (let start = 0; start < values.length; start += ENTRY_READ_BATCH_SIZE) {
230
- const chunk = values.slice(start, start + ENTRY_READ_BATCH_SIZE);
231
- const conditions = ["collection = ?"];
232
- const binds = [args.collection];
233
- const schema = this.schemasByName.get(args.collection);
234
- const compiled = compileDataPredicates(schema, [
235
- { field: args.field, kind: "in", values: chunk },
236
- ...localePredicates(args.locale),
237
- ]);
238
- conditions.push(...compiled.conditions);
239
- binds.push(...compiled.binds);
240
- if (args.status) {
241
- conditions.push("status = ?");
242
- binds.push(args.status);
243
- }
244
- const rows = await this.db
245
- .prepare(`SELECT ${ENTRY_COLUMNS} FROM entries
246
- WHERE ${conditions.join(" AND ")} ORDER BY updated_at DESC`)
247
- .bind(...binds)
248
- .all();
249
- entries.push(...rows.map(rowFromDb).map(projectPublicEntry));
199
+ const table = this.table(args.collection);
200
+ const field = requiredFieldSql(table.schema, args.field);
201
+ const output = [];
202
+ for (let start = 0; start < values.length; start += 95) {
203
+ const chunk = values.slice(start, start + 95);
204
+ const conditions = [`${field} IN (${chunk.map(() => "?").join(", ")})`];
205
+ const binds = chunk.map((value) => encodeScalar(table.schema, args.field, value));
206
+ this.addReadConditions(table, conditions, binds, args);
207
+ const sql = args.latestPerValue
208
+ ? `SELECT ${table.selectColumns} FROM ${table.table} WHERE "_mantle_id" IN (SELECT "_mantle_id" FROM (
209
+ SELECT "_mantle_id", ROW_NUMBER() OVER (PARTITION BY ${field} ORDER BY "_mantle_updated_at" DESC, "_mantle_id" DESC) AS rank
210
+ FROM ${table.table} WHERE ${conditions.join(" AND ")}) WHERE rank = 1) ORDER BY "_mantle_updated_at" DESC, "_mantle_id" DESC`
211
+ : `SELECT ${table.selectColumns} FROM ${table.table} WHERE ${conditions.join(" AND ")} ORDER BY "_mantle_updated_at" DESC`;
212
+ const rows = await this.db.prepare(sql).bind(...binds).all();
213
+ output.push(...rows.map((row) => projectPublicEntry(rowFromDb(table, row))));
250
214
  }
251
- entries.sort((a, b) => b.updatedAt - a.updatedAt);
252
- return entries;
215
+ return output.sort((a, b) => b.updatedAt - a.updatedAt);
253
216
  }
254
- async readPublished(args = {}) {
255
- const conditions = ["status = 'published'"];
217
+ async readPublished(args) {
218
+ const table = this.table(args.collection);
219
+ const conditions = ['"_mantle_status" = \'published\''];
256
220
  const binds = [];
257
- if (args.locale === null) {
258
- conditions.push("entry_locale IS NULL");
259
- }
260
- else if (args.locale !== undefined) {
261
- conditions.push("entry_locale = ?");
221
+ this.addLocaleCondition(table, conditions, binds, args.locale);
222
+ const limit = typeof args.limit === "number" && Number.isFinite(args.limit) && args.limit > 0 ? ` LIMIT ${Math.floor(args.limit)}` : "";
223
+ const rows = await this.db.prepare(`SELECT ${table.selectColumns} FROM ${table.table} WHERE ${conditions.join(" AND ")} ORDER BY "_mantle_updated_at" DESC${limit}`)
224
+ .bind(...binds).all();
225
+ return rows.map((row) => projectPublicEntry(rowFromDb(table, row)));
226
+ }
227
+ async readPublishedPage(args) {
228
+ const table = this.table(args.collection);
229
+ const limit = publishedPageLimit(args.limit);
230
+ const fields = args.dataFields
231
+ ? [...new Set([...args.dataFields.filter((field) => table.fields.includes(field)), ...(table.fields.includes("locale") ? ["locale"] : [])])]
232
+ : table.fields;
233
+ const columns = ['"_mantle_id"', '"_mantle_status"', '"_mantle_version"', '"_mantle_author_id"', '"_mantle_created_at"', '"_mantle_updated_at"', ...fields.map(quote)];
234
+ const conditions = ['"_mantle_status" = \'published\''];
235
+ const binds = [];
236
+ if (args.includeUnlocalized && typeof args.locale === "string") {
237
+ const locale = requiredFieldSql(table.schema, "locale");
238
+ conditions.push(`(${locale} = ? OR ${locale} IS NULL)`);
262
239
  binds.push(args.locale);
263
240
  }
264
- if (args.collection) {
265
- conditions.push("collection = ?");
266
- binds.push(args.collection);
241
+ else {
242
+ this.addLocaleCondition(table, conditions, binds, args.locale);
267
243
  }
268
- let sql = `SELECT ${ENTRY_COLUMNS} FROM entries ` +
269
- `WHERE ${conditions.join(" AND ")} ORDER BY updated_at DESC`;
270
- if (typeof args.limit === "number" &&
271
- Number.isFinite(args.limit) &&
272
- args.limit > 0) {
273
- sql += ` LIMIT ${Math.floor(args.limit)}`;
244
+ const cursor = decodeEntrySortCursor(args.cursor, "updatedAt", "desc");
245
+ if (cursor) {
246
+ conditions.push('("_mantle_updated_at", "_mantle_id") < (?, ?)');
247
+ binds.push(...cursor);
274
248
  }
275
- const rows = await this.db.prepare(sql).bind(...binds).all();
276
- return rows.map(rowFromDb).map(projectPublicEntry);
249
+ const rowBytes = fields.length
250
+ ? fields.map((field) => `length(CAST(json_quote(${quote(field)}) AS BLOB)) + ${new TextEncoder().encode(field).byteLength + 3}`).join(" + ")
251
+ : "2";
252
+ const rows = await this.db.prepare(`WITH candidates AS (
253
+ SELECT ${columns.join(", ")} FROM ${table.table} WHERE ${conditions.join(" AND ")}
254
+ ORDER BY "_mantle_updated_at" DESC, "_mantle_id" DESC LIMIT ${limit + 1}
255
+ ), budget AS (
256
+ SELECT *, (${rowBytes}) AS "_row_bytes", LEAD("_mantle_id") OVER (ORDER BY "_mantle_updated_at" DESC, "_mantle_id" DESC) AS "_next_id",
257
+ SUM(${rowBytes}) OVER (ORDER BY "_mantle_updated_at" DESC, "_mantle_id" DESC) AS "_data_bytes" FROM candidates
258
+ ) SELECT ${columns.join(", ")}, "_next_id" FROM budget
259
+ WHERE "_data_bytes" = "_row_bytes" OR "_data_bytes" <= ${PUBLISHED_PAGE_DATA_BUDGET}
260
+ ORDER BY "_mantle_updated_at" DESC, "_mantle_id" DESC`)
261
+ .bind(...binds).all();
262
+ const page = rows.slice(0, limit);
263
+ const last = page.at(-1);
264
+ return {
265
+ rows: page.map((row) => projectPublicEntry(rowFromDb(table, row, args.dataFields))),
266
+ ...(last?._next_id != null ? { nextCursor: encodeEntrySortCursor("updatedAt", "desc", last._mantle_updated_at, last._mantle_id) } : {}),
267
+ };
277
268
  }
278
269
  async findManyByDataField(args) {
279
- const conditions = ["collection = ?"];
280
- const binds = [args.collection];
281
- const compiled = compileDataPredicates(this.schemasByName.get(args.collection), [{ field: args.field, kind: "equal", value: args.value }]);
282
- conditions.push(...compiled.conditions);
283
- binds.push(...compiled.binds);
284
- const limit = Number.isFinite(args.limit) && args.limit > 0
285
- ? Math.floor(args.limit)
286
- : 1;
287
- const rows = await this.db
288
- .prepare(`SELECT ${ENTRY_COLUMNS} FROM entries
289
- WHERE ${conditions.join(" AND ")}
290
- ORDER BY updated_at DESC, id DESC LIMIT ${limit}`)
291
- .bind(...binds)
292
- .all();
293
- return rows.map(rowFromDb).map(projectPublicEntry);
270
+ const table = this.table(args.collection);
271
+ const field = requiredFieldSql(table.schema, args.field);
272
+ const limit = Number.isFinite(args.limit) && args.limit > 0 ? Math.floor(args.limit) : 1;
273
+ const rows = await this.db.prepare(`SELECT ${table.selectColumns} FROM ${table.table} WHERE ${field} = ? ORDER BY "_mantle_updated_at" DESC, "_mantle_id" DESC LIMIT ${limit}`)
274
+ .bind(encodeScalar(table.schema, args.field, args.value)).all();
275
+ return rows.map((row) => projectPublicEntry(rowFromDb(table, row)));
294
276
  }
295
- async findOneByDataFields(args) {
296
- const entries = Object.entries(args.fields);
297
- if (entries.length === 0)
277
+ table(collection) {
278
+ const schema = this.schemasByName.get(collection);
279
+ if (!schema)
280
+ throw new Error(`unknown Schema table: ${collection}`);
281
+ return sqliteSchemaTable(schema);
282
+ }
283
+ encodedData(table, data) {
284
+ const properties = table.schema.spec.schema.properties ?? {};
285
+ const unknown = Object.keys(data).find((field) => !Object.hasOwn(properties, field));
286
+ if (unknown)
287
+ throw new Error(`Schema '${table.schema.metadata.name}' has no field '${unknown}'.`);
288
+ return table.fields.map((field) => encodeField(data[field], properties[field]));
289
+ }
290
+ async findOne(args) {
291
+ const table = this.table(args.collection);
292
+ const conditions = [];
293
+ const binds = [];
294
+ for (const [field, value] of Object.entries(args.fields)) {
295
+ conditions.push(`${requiredFieldSql(table.schema, field)} = ?`);
296
+ binds.push(encodeScalar(table.schema, field, value));
297
+ }
298
+ if (!conditions.length)
298
299
  return null;
299
- const conditions = ["collection = ?"];
300
- const binds = [args.collection];
301
- const schema = this.schemasByName.get(args.collection);
302
300
  if (args.status) {
303
- conditions.push("status = ?");
301
+ conditions.push('"_mantle_status" = ?');
304
302
  binds.push(args.status);
305
303
  }
306
- const compiled = compileDataPredicates(schema, [
307
- ...entries.map(([field, value]) => ({
308
- field,
309
- kind: "equal",
310
- value,
311
- })),
312
- ...localePredicates(args.locale),
313
- ]);
314
- conditions.push(...compiled.conditions);
315
- binds.push(...compiled.binds);
304
+ this.addLocaleCondition(table, conditions, binds, args.locale);
316
305
  if (args.excludeId) {
317
- conditions.push("id <> ?");
306
+ conditions.push('"_mantle_id" <> ?');
318
307
  binds.push(args.excludeId);
319
308
  }
320
- const row = await this.db
321
- .prepare(`SELECT id, collection, status, version, data, author_id, created_at, updated_at
322
- FROM entries
323
- WHERE ${conditions.join(" AND ")}
324
- ORDER BY updated_at DESC LIMIT 1`)
325
- .bind(...binds)
326
- .first();
327
- return row ? rowFromDb(row) : null;
328
- }
329
- async versionConflict(id, expected) {
330
- const after = await this.db
331
- .prepare(`SELECT version FROM entries WHERE id = ?`)
332
- .bind(id)
333
- .first();
334
- return new EntryVersionConflict(id, expected, after?.version ?? -1);
309
+ const row = await this.db.prepare(`SELECT ${table.selectColumns} FROM ${table.table} WHERE ${conditions.join(" AND ")} ORDER BY "_mantle_updated_at" DESC LIMIT 1`)
310
+ .bind(...binds).first();
311
+ return row ? rowFromDb(table, row) : null;
335
312
  }
336
- }
337
- const ENTRY_COLUMNS = "id, collection, status, version, data, author_id, created_at, updated_at";
338
- // D1 accepts at most 100 bound parameters. The worst fallback shape uses
339
- // five fixed binds (collection, two JSON paths, locale, status), leaving 95
340
- // values for the parent `IN` predicate.
341
- const ENTRY_READ_BATCH_SIZE = 95;
342
- function localePredicates(locale) {
343
- if (locale === undefined)
344
- return [];
345
- return locale === null
346
- ? [{ field: "locale", kind: "null" }]
347
- : [{ field: "locale", kind: "equal", value: locale }];
348
- }
349
- function compileDataPredicates(schema, predicates) {
350
- const conditions = [];
351
- const binds = [];
352
- const indexed = usableIndexedFields(schema, predicates);
353
- for (const predicate of predicates) {
354
- const generated = schema && indexed.has(predicate.field)
355
- ? schemaIndexedFieldSql(schema, predicate.field)
356
- : null;
357
- const reference = generated ?? "json_extract(data, ?)";
358
- if (!generated)
359
- binds.push(jsonPathForTopLevelField(predicate.field));
360
- if (predicate.kind === "null") {
361
- conditions.push(`${reference} IS NULL`);
313
+ addReadConditions(table, conditions, binds, args) {
314
+ if (args.status) {
315
+ conditions.push('"_mantle_status" = ?');
316
+ binds.push(args.status);
362
317
  }
363
- else if (predicate.kind === "in") {
364
- conditions.push(`${reference} IN (${predicate.values.map(() => "?").join(", ")})`);
365
- binds.push(...predicate.values);
318
+ this.addLocaleCondition(table, conditions, binds, args.locale);
319
+ }
320
+ addLocaleCondition(table, conditions, binds, locale) {
321
+ if (locale === undefined)
322
+ return;
323
+ const field = fieldSql(table.schema, "locale");
324
+ if (!field) {
325
+ if (locale !== null)
326
+ conditions.push("0 = 1");
327
+ return;
366
328
  }
329
+ if (locale === null)
330
+ conditions.push(`${field} IS NULL`);
367
331
  else {
368
- conditions.push(`${reference} = ?`);
369
- binds.push(predicate.value);
332
+ conditions.push(`${field} = ?`);
333
+ binds.push(locale);
370
334
  }
371
335
  }
372
- return { conditions, binds };
336
+ async versionConflict(table, id, expected) {
337
+ const after = await this.db.prepare(`SELECT "_mantle_version" AS "version" FROM ${table.table} WHERE "_mantle_id" = ?`).bind(id).first();
338
+ return new EntryVersionConflict(id, expected, after?.version ?? -1);
339
+ }
373
340
  }
374
- function usableIndexedFields(schema, predicates) {
375
- if (!schema)
376
- return new Set();
377
- const byField = new Map(predicates.map((predicate) => [predicate.field, predicate]));
378
- const usable = new Set();
379
- const declarations = [
380
- ...(schema.spec.uniqueIndexes ?? []),
381
- ...(schema.spec.indexes ?? []),
382
- ];
383
- for (const declaration of declarations) {
384
- for (let index = 0; index < declaration.length; index += 1) {
385
- const field = declaration[index];
386
- const predicate = byField.get(field);
387
- if (!predicate)
388
- break;
389
- if (index === 0 &&
390
- (predicate.kind === "null" ||
391
- (predicate.kind === "equal" && predicate.value === null))) {
392
- break;
393
- }
394
- usable.add(field);
395
- }
341
+ function rowFromDb(table, row, dataFields) {
342
+ const properties = table.schema.spec.schema.properties ?? {};
343
+ const data = {};
344
+ let locale;
345
+ for (const field of table.fields) {
346
+ if (!Object.hasOwn(row, field))
347
+ continue;
348
+ const value = decodeField(row[field], properties[field]);
349
+ if (field === "locale" && typeof value === "string")
350
+ locale = value;
351
+ if ((!dataFields || dataFields.includes(field)) && value !== undefined)
352
+ data[field] = value;
396
353
  }
397
- return usable;
354
+ for (const field of dataFields ?? []) {
355
+ const property = properties[field];
356
+ if ((!property || isNullableJsonSchema(property)) && !Object.hasOwn(data, field))
357
+ data[field] = null;
358
+ }
359
+ return {
360
+ id: row._mantle_id,
361
+ collection: table.schema.metadata.name,
362
+ locale: locale ?? liftLocale(data),
363
+ status: row._mantle_status,
364
+ version: row._mantle_version,
365
+ data,
366
+ authorId: row._mantle_author_id,
367
+ createdAt: row._mantle_created_at,
368
+ updatedAt: row._mantle_updated_at,
369
+ };
398
370
  }
399
- function entrySortSql(schema, field) {
400
- if (field === "id")
401
- return "id";
402
- if (field === "status")
403
- return "status";
404
- if (field === "updatedAt")
405
- return "updated_at";
406
- return schema ? schemaIndexedFieldSql(schema, field) : null;
371
+ function requiredFieldSql(schema, field) {
372
+ const sql = fieldSql(schema, field);
373
+ if (!sql)
374
+ throw new Error(`Schema '${schema.metadata.name}' has no field '${field}'.`);
375
+ return sql;
407
376
  }
408
- function entrySortValueFromDb(row, field) {
409
- if (field === "id")
410
- return row.id;
411
- if (field === "status")
412
- return row.status;
413
- if (field === "updatedAt")
414
- return row.updated_at;
415
- const value = JSON.parse(row.data)[field];
416
- if (typeof value === "boolean")
417
- return Number(value);
418
- if (typeof value !== "string" && typeof value !== "number") {
377
+ function encodeScalar(schema, field, value) {
378
+ const property = schema.spec.schema.properties?.[field];
379
+ return property ? encodeField(value, property) : value;
380
+ }
381
+ function sortValue(row, schema, field) {
382
+ const physical = fieldColumn(schema, field);
383
+ if (!physical)
384
+ throw new Error(`Schema '${schema.metadata.name}' has no field '${field}'.`);
385
+ const value = row[physical];
386
+ if (typeof value !== "string" && typeof value !== "number")
419
387
  throw new Error(`non-scalar sort value for ${field}`);
420
- }
421
388
  return value;
422
389
  }
423
- function rowFromDb(row) {
424
- const data = JSON.parse(row.data);
425
- return {
426
- id: row.id,
427
- collection: row.collection,
428
- locale: liftLocale(data),
429
- status: row.status,
430
- version: row.version,
431
- data,
432
- authorId: row.author_id,
433
- createdAt: row.created_at,
434
- updatedAt: row.updated_at,
435
- };
390
+ function quote(value) {
391
+ return `"${value.replace(/"/g, '""')}"`;
436
392
  }
437
- function jsonPathForTopLevelField(field) {
438
- return `$."${field.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
393
+ /** Compatibility helper now requires the Schema that owns the table. */
394
+ export async function readEntryBySlug(db, schema, args) {
395
+ return new DatabaseEntryRepository(db, new Map([[schema.metadata.name, schema]])).readBySlug(args);
439
396
  }
440
- /** Public compatibility helper. Without a Schema map it intentionally uses
441
- * the safe JSON fallback; in-repo callers use `MantleRuntime.entries`. */
442
- export async function readEntryBySlug(db, args) {
443
- return new DatabaseEntryRepository(db).readBySlug(args);
397
+ function isDriverUniqueConstraintError(error) {
398
+ if (!(error instanceof Error))
399
+ return false;
400
+ const message = error.message || "";
401
+ return message.includes("UNIQUE constraint failed") || message.includes("unique constraint") ||
402
+ message.includes("duplicate key") || error.code === "SQLITE_CONSTRAINT_UNIQUE" ||
403
+ error.code === "23505";
444
404
  }
445
405
  //# sourceMappingURL=DatabaseEntryRepository.js.map