@aotter/mantle-runtime 0.1.2-alpha.1 → 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.
- package/dist/MantleRuntime.js +2 -2
- package/dist/MantleRuntime.js.map +1 -1
- package/dist/domain/model/EntryRow.d.ts +7 -16
- package/dist/domain/model/EntryRow.d.ts.map +1 -1
- package/dist/domain/model/EntryRow.js +15 -0
- package/dist/domain/model/EntryRow.js.map +1 -1
- package/dist/domain/port/DatabaseDriver.d.ts +2 -2
- package/dist/domain/port/DatabaseDriver.d.ts.map +1 -1
- package/dist/domain/port/EntryReader.d.ts +5 -4
- package/dist/domain/port/EntryReader.d.ts.map +1 -1
- package/dist/domain/port/EntryRepository.d.ts +8 -6
- package/dist/domain/port/EntryRepository.d.ts.map +1 -1
- package/dist/domain/port/index.d.ts +1 -1
- package/dist/domain/port/index.d.ts.map +1 -1
- package/dist/domain/port/index.js.map +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/infrastructure/boot/SqliteMigrationRunner.d.ts +1 -0
- package/dist/infrastructure/boot/SqliteMigrationRunner.d.ts.map +1 -1
- package/dist/infrastructure/boot/SqliteMigrationRunner.js +71 -12
- package/dist/infrastructure/boot/SqliteMigrationRunner.js.map +1 -1
- package/dist/infrastructure/boot/bootState.d.ts +2 -0
- package/dist/infrastructure/boot/bootState.d.ts.map +1 -1
- package/dist/infrastructure/boot/bootState.js +3 -1
- package/dist/infrastructure/boot/bootState.js.map +1 -1
- package/dist/infrastructure/boot/canonicalMigrations.d.ts +4 -17
- package/dist/infrastructure/boot/canonicalMigrations.d.ts.map +1 -1
- package/dist/infrastructure/boot/canonicalMigrations.js +14 -207
- package/dist/infrastructure/boot/canonicalMigrations.js.map +1 -1
- package/dist/infrastructure/boot/index.d.ts +2 -2
- package/dist/infrastructure/boot/index.d.ts.map +1 -1
- package/dist/infrastructure/boot/index.js +2 -2
- package/dist/infrastructure/boot/index.js.map +1 -1
- package/dist/infrastructure/mcp/McpJsonRpcDispatcher.d.ts.map +1 -1
- package/dist/infrastructure/mcp/McpJsonRpcDispatcher.js +22 -24
- package/dist/infrastructure/mcp/McpJsonRpcDispatcher.js.map +1 -1
- package/dist/infrastructure/mcp/McpToolCatalog.js +8 -8
- package/dist/infrastructure/mcp/McpToolCatalog.js.map +1 -1
- package/dist/infrastructure/persistence/DatabaseEntryRepository.d.ts +14 -21
- package/dist/infrastructure/persistence/DatabaseEntryRepository.d.ts.map +1 -1
- package/dist/infrastructure/persistence/DatabaseEntryRepository.js +262 -423
- package/dist/infrastructure/persistence/DatabaseEntryRepository.js.map +1 -1
- package/dist/infrastructure/persistence/LifecycleHookingEntryRepository.d.ts +2 -2
- package/dist/infrastructure/persistence/LifecycleHookingEntryRepository.d.ts.map +1 -1
- package/dist/infrastructure/persistence/LifecycleHookingEntryRepository.js +5 -5
- package/dist/infrastructure/persistence/LifecycleHookingEntryRepository.js.map +1 -1
- package/dist/infrastructure/persistence/Pagination.d.ts +1 -1
- package/dist/infrastructure/persistence/Pagination.d.ts.map +1 -1
- package/dist/infrastructure/persistence/Pagination.js +1 -1
- package/dist/infrastructure/persistence/Pagination.js.map +1 -1
- package/dist/infrastructure/storage/SqliteMantleStorageAdapter.d.ts +4 -0
- package/dist/infrastructure/storage/SqliteMantleStorageAdapter.d.ts.map +1 -1
- package/dist/infrastructure/storage/SqliteMantleStorageAdapter.js +57 -6
- package/dist/infrastructure/storage/SqliteMantleStorageAdapter.js.map +1 -1
- package/dist/infrastructure/storage/SqliteMigrationArtifact.d.ts +19 -0
- package/dist/infrastructure/storage/SqliteMigrationArtifact.d.ts.map +1 -0
- package/dist/infrastructure/storage/SqliteMigrationArtifact.js +41 -0
- package/dist/infrastructure/storage/SqliteMigrationArtifact.js.map +1 -0
- package/dist/infrastructure/storage/SqliteSchemaTables.d.ts +23 -0
- package/dist/infrastructure/storage/SqliteSchemaTables.d.ts.map +1 -0
- package/dist/infrastructure/storage/SqliteSchemaTables.js +244 -0
- package/dist/infrastructure/storage/SqliteSchemaTables.js.map +1 -0
- package/dist/infrastructure/storage/SqliteViewCompiler.d.ts +3 -5
- package/dist/infrastructure/storage/SqliteViewCompiler.d.ts.map +1 -1
- package/dist/infrastructure/storage/SqliteViewCompiler.js +20 -51
- package/dist/infrastructure/storage/SqliteViewCompiler.js.map +1 -1
- package/dist/infrastructure/testing/IndexCoverageHarness.d.ts.map +1 -1
- package/dist/infrastructure/testing/IndexCoverageHarness.js +19 -31
- package/dist/infrastructure/testing/IndexCoverageHarness.js.map +1 -1
- package/dist/infrastructure/testing/StorageConformance.js +21 -20
- package/dist/infrastructure/testing/StorageConformance.js.map +1 -1
- package/dist/usecase/content/ArchiveUseCase.js +4 -4
- package/dist/usecase/content/ArchiveUseCase.js.map +1 -1
- package/dist/usecase/content/DeleteEntryUseCase.js +4 -4
- package/dist/usecase/content/DeleteEntryUseCase.js.map +1 -1
- package/dist/usecase/content/GetEntryUseCase.d.ts +1 -2
- package/dist/usecase/content/GetEntryUseCase.d.ts.map +1 -1
- package/dist/usecase/content/GetEntryUseCase.js +2 -6
- package/dist/usecase/content/GetEntryUseCase.js.map +1 -1
- package/dist/usecase/content/RequestPublishUseCase.js +4 -4
- package/dist/usecase/content/RequestPublishUseCase.js.map +1 -1
- package/dist/usecase/content/UnpublishUseCase.js +4 -4
- package/dist/usecase/content/UnpublishUseCase.js.map +1 -1
- package/dist/usecase/content/UpdateDraftUseCase.js +4 -4
- package/dist/usecase/content/UpdateDraftUseCase.js.map +1 -1
- package/dist/usecase/dto/content/index.d.ts +6 -5
- package/dist/usecase/dto/content/index.d.ts.map +1 -1
- package/dist/usecase/procedure/InvokeBuiltinUseCase.js +6 -6
- package/dist/usecase/procedure/InvokeBuiltinUseCase.js.map +1 -1
- package/package.json +2 -2
|
@@ -1,18 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { clampLimit } from "../../domain/service/Pagination.js";
|
|
1
|
+
import { checkSchemaAdminUi, } from "@aotter/mantle-spec";
|
|
3
2
|
import { EntryStatusConflict, EntryUniqueConflict, EntryVersionConflict, liftLocale, projectPublicEntry, } from "../../domain/model/EntryRow.js";
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
*/
|
|
3
|
+
import { clampLimit } from "../../domain/service/Pagination.js";
|
|
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;
|
|
@@ -21,42 +12,39 @@ export class DatabaseEntryRepository {
|
|
|
21
12
|
this.schemasByName = schemasByName;
|
|
22
13
|
}
|
|
23
14
|
async readCreationStatistics(args) {
|
|
24
|
-
const {
|
|
15
|
+
const { from, to, bucketMs } = args;
|
|
25
16
|
if (![from, to, bucketMs].every(Number.isSafeInteger) || from < 0 || to <= from ||
|
|
26
17
|
bucketMs <= 0 || to - from > 20 * 86_400_000 || Math.ceil((to - from) / bucketMs) > 480) {
|
|
27
18
|
throw new RangeError("Statistics require a positive window <= 20 days and <= 480 buckets.");
|
|
28
19
|
}
|
|
29
|
-
const
|
|
30
|
-
const filter =
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const subtype = field
|
|
35
|
-
? `CASE WHEN ${field} IN (SELECT value FROM json_each(?)) THEN ${field} ELSE NULL END`
|
|
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`
|
|
36
25
|
: "NULL";
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
`).bind(collection, from, bucketMs, ...(field ? [JSON.stringify(filter.values)] : []), collection, from, to)
|
|
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)
|
|
44
32
|
.all();
|
|
45
|
-
return { total:
|
|
33
|
+
return { total: total?.count ?? 0, buckets: rows };
|
|
46
34
|
}
|
|
47
35
|
async create(args) {
|
|
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)];
|
|
48
39
|
try {
|
|
49
|
-
await this.db
|
|
50
|
-
.
|
|
51
|
-
VALUES (?, ?, ?, 1, ?, ?, ?, ?)`)
|
|
52
|
-
.bind(args.id, args.collection, args.status, JSON.stringify(args.data), args.authorId, args.now, args.now)
|
|
53
|
-
.run();
|
|
40
|
+
await this.db.prepare(`INSERT INTO ${table.table} (${columns.map(quote).join(", ")}) VALUES (${columns.map(() => "?").join(", ")})`)
|
|
41
|
+
.bind(...values).run();
|
|
54
42
|
}
|
|
55
|
-
catch (
|
|
56
|
-
if (isDriverUniqueConstraintError(
|
|
57
|
-
throw new EntryUniqueConflict(args.collection, args.data,
|
|
43
|
+
catch (error) {
|
|
44
|
+
if (isDriverUniqueConstraintError(error)) {
|
|
45
|
+
throw new EntryUniqueConflict(args.collection, args.data, error.message);
|
|
58
46
|
}
|
|
59
|
-
throw
|
|
47
|
+
throw error;
|
|
60
48
|
}
|
|
61
49
|
return {
|
|
62
50
|
id: args.id,
|
|
@@ -70,497 +58,348 @@ export class DatabaseEntryRepository {
|
|
|
70
58
|
updatedAt: args.now,
|
|
71
59
|
};
|
|
72
60
|
}
|
|
73
|
-
async get(
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
.first();
|
|
79
|
-
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;
|
|
80
66
|
}
|
|
81
67
|
async update(args) {
|
|
82
|
-
const
|
|
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" = ?'];
|
|
83
71
|
let row = null;
|
|
84
72
|
try {
|
|
85
|
-
row = await this.db
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
RETURNING id, collection, status, version, data, author_id, created_at, updated_at`)
|
|
89
|
-
.bind(JSON.stringify(args.data), newVersion, args.now, args.id, args.expectedVersion)
|
|
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)
|
|
90
76
|
.first();
|
|
91
77
|
}
|
|
92
|
-
catch (
|
|
93
|
-
if (isDriverUniqueConstraintError(
|
|
94
|
-
throw new EntryUniqueConflict(args.collection, args.data,
|
|
78
|
+
catch (error) {
|
|
79
|
+
if (isDriverUniqueConstraintError(error)) {
|
|
80
|
+
throw new EntryUniqueConflict(args.collection, args.data, error.message);
|
|
95
81
|
}
|
|
96
|
-
throw
|
|
82
|
+
throw error;
|
|
97
83
|
}
|
|
98
84
|
if (!row)
|
|
99
|
-
throw await this.versionConflict(args.id, args.expectedVersion);
|
|
100
|
-
return rowFromDb(row);
|
|
85
|
+
throw await this.versionConflict(table, args.id, args.expectedVersion);
|
|
86
|
+
return rowFromDb(table, row);
|
|
101
87
|
}
|
|
102
88
|
async delete(args) {
|
|
103
|
-
const
|
|
104
|
-
const
|
|
105
|
-
args.id,
|
|
106
|
-
args.collection,
|
|
107
|
-
args.expectedStatus,
|
|
108
|
-
args.expectedVersion,
|
|
109
|
-
];
|
|
110
|
-
const result = await this.db
|
|
111
|
-
.prepare(`DELETE FROM entries WHERE ${parentMatches}`)
|
|
112
|
-
.bind(...parentSnapshot)
|
|
113
|
-
.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();
|
|
114
92
|
if (result.meta.changes > 0)
|
|
115
93
|
return { removed: true };
|
|
116
|
-
const after = await this.db
|
|
117
|
-
.
|
|
118
|
-
|
|
119
|
-
.first();
|
|
120
|
-
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)
|
|
121
97
|
return { removed: false };
|
|
122
|
-
if (after.version !== args.expectedVersion)
|
|
98
|
+
if (after.version !== args.expectedVersion)
|
|
123
99
|
throw new EntryVersionConflict(args.id, args.expectedVersion, after.version);
|
|
124
|
-
}
|
|
125
100
|
throw new EntryStatusConflict(args.id, args.expectedStatus, after.status);
|
|
126
101
|
}
|
|
127
102
|
async transitionStatus(args) {
|
|
128
|
-
const
|
|
129
|
-
const
|
|
103
|
+
const table = this.table(args.collection);
|
|
104
|
+
const conditions = ['"_mantle_id" = ?'];
|
|
130
105
|
const binds = [args.to, args.now, args.id];
|
|
131
|
-
if (expectedStatus !== undefined) {
|
|
132
|
-
|
|
133
|
-
binds.push(expectedStatus);
|
|
106
|
+
if (args.expectedStatus !== undefined) {
|
|
107
|
+
conditions.push('"_mantle_status" = ?');
|
|
108
|
+
binds.push(args.expectedStatus);
|
|
134
109
|
}
|
|
135
|
-
if (expectedVersion !== undefined) {
|
|
136
|
-
|
|
137
|
-
binds.push(expectedVersion);
|
|
110
|
+
if (args.expectedVersion !== undefined) {
|
|
111
|
+
conditions.push('"_mantle_version" = ?');
|
|
112
|
+
binds.push(args.expectedVersion);
|
|
138
113
|
}
|
|
139
|
-
const row = await this.db
|
|
140
|
-
|
|
141
|
-
WHERE id = ?${guards.join("")}
|
|
142
|
-
RETURNING id, collection, status, version, data, author_id, created_at, updated_at`)
|
|
143
|
-
.bind(...binds)
|
|
144
|
-
.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();
|
|
145
116
|
if (row)
|
|
146
|
-
return rowFromDb(row);
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const after = await this.db
|
|
152
|
-
.prepare(`SELECT version, status FROM entries WHERE id = ?`)
|
|
153
|
-
.bind(args.id)
|
|
154
|
-
.first();
|
|
155
|
-
if (expectedVersion !== undefined && after && after.version !== expectedVersion) {
|
|
156
|
-
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);
|
|
157
122
|
}
|
|
158
|
-
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);
|
|
159
124
|
}
|
|
160
125
|
async list(args) {
|
|
161
|
-
|
|
162
|
-
// adapters that bypass the use case) get the same default page
|
|
163
|
-
// size as ListEntriesUseCase — not a silently different 100.
|
|
126
|
+
const table = this.table(args.collection);
|
|
164
127
|
const limit = clampLimit(args.limit);
|
|
165
128
|
const sort = args.sort ?? { field: "updatedAt", direction: "desc" };
|
|
166
|
-
const
|
|
167
|
-
const sortSql = entrySortSql(schema, sort.field);
|
|
168
|
-
if (!sortSql)
|
|
169
|
-
throw new Error(`unavailable entry sort field: ${sort.field}`);
|
|
129
|
+
const sortSql = args.sort ? requiredFieldSql(table.schema, sort.field) : '"_mantle_updated_at"';
|
|
170
130
|
const cursor = decodeEntrySortCursor(args.cursor, sort.field, sort.direction);
|
|
171
131
|
const backward = args.cursorDirection === "backward" && cursor !== null;
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
// Fetch limit+1 to detect a next page without a second query —
|
|
176
|
-
// the extra row never reaches the caller.
|
|
177
|
-
const probe = limit + 1;
|
|
178
|
-
const conditions = ["collection = ?"];
|
|
179
|
-
const binds = [args.collection];
|
|
132
|
+
const direction = backward ? (sort.direction === "asc" ? "DESC" : "ASC") : sort.direction.toUpperCase();
|
|
133
|
+
const conditions = [];
|
|
134
|
+
const binds = [];
|
|
180
135
|
if (args.status) {
|
|
181
|
-
conditions.push("
|
|
136
|
+
conditions.push('"_mantle_status" = ?');
|
|
182
137
|
binds.push(args.status);
|
|
183
138
|
}
|
|
184
139
|
if (args.search) {
|
|
185
140
|
const term = escapeLikeTerm(args.search);
|
|
186
|
-
const
|
|
141
|
+
const search = ['"_mantle_id" LIKE \'%\'||?||\'%\' ESCAPE \'\\\''];
|
|
187
142
|
binds.push(term);
|
|
188
143
|
for (const field of args.searchFields ?? []) {
|
|
189
|
-
|
|
190
|
-
binds.push(
|
|
144
|
+
search.push(`${requiredFieldSql(table.schema, field)} LIKE '%'||?||'%' ESCAPE '\\'`);
|
|
145
|
+
binds.push(term);
|
|
191
146
|
}
|
|
192
|
-
conditions.push(`(${
|
|
147
|
+
conditions.push(`(${search.join(" OR ")})`);
|
|
193
148
|
}
|
|
194
149
|
if (args.filter) {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
kind: "equal",
|
|
198
|
-
value: args.filter.value,
|
|
199
|
-
}]);
|
|
200
|
-
conditions.push(...compiled.conditions);
|
|
201
|
-
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));
|
|
202
152
|
}
|
|
203
153
|
if (args.scope) {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
kind: "equal",
|
|
207
|
-
value: args.scope.value,
|
|
208
|
-
}]);
|
|
209
|
-
conditions.push(...compiled.conditions);
|
|
210
|
-
binds.push(...compiled.binds);
|
|
154
|
+
conditions.push(`${requiredFieldSql(table.schema, args.scope.field)} = ?`);
|
|
155
|
+
binds.push(encodeScalar(table.schema, args.scope.field, args.scope.value));
|
|
211
156
|
}
|
|
212
157
|
if (cursor) {
|
|
213
|
-
const comparison = backward
|
|
214
|
-
|
|
215
|
-
: (sort.direction === "asc" ? ">" : "<");
|
|
216
|
-
conditions.push(`(${sortSql}, id) ${comparison} (?, ?)`);
|
|
158
|
+
const comparison = backward ? (sort.direction === "asc" ? "<" : ">") : (sort.direction === "asc" ? ">" : "<");
|
|
159
|
+
conditions.push(`(${sortSql}, "_mantle_id") ${comparison} (?, ?)`);
|
|
217
160
|
binds.push(...cursor);
|
|
218
161
|
}
|
|
219
|
-
binds.push(
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
ORDER BY ${sortSql} ${queryDirection}, id ${queryDirection} LIMIT ?`)
|
|
224
|
-
.bind(...binds);
|
|
225
|
-
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();
|
|
226
166
|
const hasMore = rows.length > limit;
|
|
227
167
|
const page = [...(hasMore ? rows.slice(0, limit) : rows)];
|
|
228
168
|
if (backward)
|
|
229
169
|
page.reverse();
|
|
230
170
|
const first = page[0];
|
|
231
|
-
const last = page
|
|
171
|
+
const last = page.at(-1);
|
|
232
172
|
return {
|
|
233
|
-
rows: page.map(rowFromDb),
|
|
234
|
-
previousCursor: first && (backward ? hasMore : cursor !== null)
|
|
235
|
-
|
|
236
|
-
: undefined,
|
|
237
|
-
nextCursor: last && (backward ? cursor !== null : hasMore)
|
|
238
|
-
? encodeEntrySortCursor(sort.field, sort.direction, entrySortValueFromDb(last, sort.field), last.id)
|
|
239
|
-
: 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,
|
|
240
176
|
};
|
|
241
177
|
}
|
|
242
178
|
async findByDataField(args) {
|
|
243
|
-
return this.
|
|
244
|
-
collection: args.collection,
|
|
245
|
-
status: args.status,
|
|
246
|
-
fields: { [args.field]: args.value },
|
|
247
|
-
});
|
|
179
|
+
return this.findOne({ ...args, fields: { [args.field]: args.value } });
|
|
248
180
|
}
|
|
249
181
|
async findByDataFields(args) {
|
|
250
|
-
return this.
|
|
182
|
+
return this.findOne(args);
|
|
251
183
|
}
|
|
252
|
-
async readById(
|
|
253
|
-
const row = await this.get(
|
|
184
|
+
async readById(args) {
|
|
185
|
+
const row = await this.get(args);
|
|
254
186
|
return row ? projectPublicEntry(row) : null;
|
|
255
187
|
}
|
|
256
188
|
async readBySlug(args) {
|
|
257
|
-
return this.readByDataField({
|
|
258
|
-
collection: args.collection,
|
|
259
|
-
field: "slug",
|
|
260
|
-
value: args.slug,
|
|
261
|
-
locale: args.locale,
|
|
262
|
-
status: args.status,
|
|
263
|
-
});
|
|
189
|
+
return this.readByDataField({ ...args, field: "slug", value: args.slug });
|
|
264
190
|
}
|
|
265
191
|
async readByDataField(args) {
|
|
266
|
-
const row = await this.
|
|
267
|
-
collection: args.collection,
|
|
268
|
-
status: args.status,
|
|
269
|
-
fields: { [args.field]: args.value },
|
|
270
|
-
locale: args.locale,
|
|
271
|
-
});
|
|
192
|
+
const row = await this.findOne({ ...args, fields: { [args.field]: args.value } });
|
|
272
193
|
return row ? projectPublicEntry(row) : null;
|
|
273
194
|
}
|
|
274
195
|
async readByDataFieldIn(args) {
|
|
275
196
|
const values = [...new Set(args.values)];
|
|
276
|
-
if (values.length
|
|
197
|
+
if (!values.length)
|
|
277
198
|
return [];
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
const
|
|
283
|
-
const
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
...localePredicates(args.locale),
|
|
287
|
-
]);
|
|
288
|
-
conditions.push(...compiled.conditions);
|
|
289
|
-
binds.push(...compiled.binds);
|
|
290
|
-
if (args.status) {
|
|
291
|
-
conditions.push("status = ?");
|
|
292
|
-
binds.push(args.status);
|
|
293
|
-
}
|
|
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);
|
|
294
207
|
const sql = args.latestPerValue
|
|
295
|
-
? `SELECT ${
|
|
296
|
-
SELECT
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
) WHERE parent_rank = 1
|
|
300
|
-
) ORDER BY updated_at DESC, id DESC`
|
|
301
|
-
: `SELECT ${ENTRY_COLUMNS} FROM entries WHERE ${conditions.join(" AND ")} ORDER BY updated_at DESC`;
|
|
302
|
-
if (args.latestPerValue)
|
|
303
|
-
binds.unshift(jsonPathForTopLevelField(args.field));
|
|
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`;
|
|
304
212
|
const rows = await this.db.prepare(sql).bind(...binds).all();
|
|
305
|
-
|
|
213
|
+
output.push(...rows.map((row) => projectPublicEntry(rowFromDb(table, row))));
|
|
306
214
|
}
|
|
307
|
-
|
|
308
|
-
return entries;
|
|
215
|
+
return output.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
309
216
|
}
|
|
310
|
-
async readPublished(args
|
|
311
|
-
const
|
|
217
|
+
async readPublished(args) {
|
|
218
|
+
const table = this.table(args.collection);
|
|
219
|
+
const conditions = ['"_mantle_status" = \'published\''];
|
|
312
220
|
const binds = [];
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
|
|
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)`);
|
|
318
239
|
binds.push(args.locale);
|
|
319
240
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
binds.push(args.collection);
|
|
241
|
+
else {
|
|
242
|
+
this.addLocaleCondition(table, conditions, binds, args.locale);
|
|
323
243
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
args.limit > 0) {
|
|
329
|
-
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);
|
|
330
248
|
}
|
|
331
|
-
const
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
async readPublishedPage(args = {}) {
|
|
335
|
-
const limit = publishedPageLimit(args.limit);
|
|
336
|
-
const cursor = decodeEntryCursor(args.cursor);
|
|
337
|
-
const binds = [];
|
|
338
|
-
const candidateQuery = (locale) => {
|
|
339
|
-
const conditions = ["status = 'published'"];
|
|
340
|
-
// One bound JSON field list avoids D1's bind/function-argument ceilings.
|
|
341
|
-
// -> retains JSON types (json_extract would turn true/false into 1/0).
|
|
342
|
-
const data = args.dataFields
|
|
343
|
-
? `(SELECT json_group_object(json_extract(field.value, '$[0]'),
|
|
344
|
-
json(entries.data -> json_extract(field.value, '$[1]')))
|
|
345
|
-
FROM json_each(?) AS field)` : "data";
|
|
346
|
-
if (args.dataFields)
|
|
347
|
-
binds.push(JSON.stringify(args.dataFields.map((field) => [field, jsonPathForTopLevelField(field)])));
|
|
348
|
-
if (args.collection !== undefined) {
|
|
349
|
-
conditions.push("collection = ?");
|
|
350
|
-
binds.push(args.collection);
|
|
351
|
-
}
|
|
352
|
-
if (locale === null)
|
|
353
|
-
conditions.push("entry_locale IS NULL");
|
|
354
|
-
else if (locale !== undefined) {
|
|
355
|
-
conditions.push("entry_locale = ?");
|
|
356
|
-
binds.push(locale);
|
|
357
|
-
}
|
|
358
|
-
if (cursor) {
|
|
359
|
-
conditions.push("(updated_at, id) < (?, ?)");
|
|
360
|
-
binds.push(...cursor);
|
|
361
|
-
}
|
|
362
|
-
return `SELECT id, collection, status, version, ${data} AS data, author_id,
|
|
363
|
-
created_at, updated_at, entry_locale FROM entries
|
|
364
|
-
WHERE ${conditions.join(" AND ")} ORDER BY updated_at DESC, id DESC LIMIT ${limit + 1}`;
|
|
365
|
-
};
|
|
366
|
-
const query = candidateQuery(args.locale);
|
|
367
|
-
const candidates = args.includeUnlocalized && typeof args.locale === "string"
|
|
368
|
-
? `SELECT * FROM (${query}) UNION ALL SELECT * FROM (${candidateQuery(null)})`
|
|
369
|
-
: query;
|
|
370
|
-
// Window work is bounded to limit + 1 indexed candidates. Apply the byte
|
|
371
|
-
// budget inside SQLite, before transferring/parsing JSON in the Worker.
|
|
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";
|
|
372
252
|
const rows = await this.db.prepare(`WITH candidates AS (
|
|
373
|
-
SELECT
|
|
253
|
+
SELECT ${columns.join(", ")} FROM ${table.table} WHERE ${conditions.join(" AND ")}
|
|
254
|
+
ORDER BY "_mantle_updated_at" DESC, "_mantle_id" DESC LIMIT ${limit + 1}
|
|
374
255
|
), budget AS (
|
|
375
|
-
SELECT *, LEAD(
|
|
376
|
-
SUM(
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
ORDER BY
|
|
380
|
-
.all();
|
|
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();
|
|
381
262
|
const page = rows.slice(0, limit);
|
|
382
263
|
const last = page.at(-1);
|
|
383
264
|
return {
|
|
384
|
-
rows: page.map(rowFromDb
|
|
385
|
-
...(last?.
|
|
386
|
-
? { nextCursor: encodeEntryCursor(last.updated_at, last.id) } : {}),
|
|
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) } : {}),
|
|
387
267
|
};
|
|
388
268
|
}
|
|
389
269
|
async findManyByDataField(args) {
|
|
390
|
-
const
|
|
391
|
-
const
|
|
392
|
-
const
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
const
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
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)));
|
|
276
|
+
}
|
|
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]));
|
|
405
289
|
}
|
|
406
|
-
async
|
|
407
|
-
const
|
|
408
|
-
|
|
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)
|
|
409
299
|
return null;
|
|
410
|
-
const conditions = ["collection = ?"];
|
|
411
|
-
const binds = [args.collection];
|
|
412
|
-
const schema = this.schemasByName.get(args.collection);
|
|
413
300
|
if (args.status) {
|
|
414
|
-
conditions.push("
|
|
301
|
+
conditions.push('"_mantle_status" = ?');
|
|
415
302
|
binds.push(args.status);
|
|
416
303
|
}
|
|
417
|
-
|
|
418
|
-
...entries.map(([field, value]) => ({
|
|
419
|
-
field,
|
|
420
|
-
kind: "equal",
|
|
421
|
-
value,
|
|
422
|
-
})),
|
|
423
|
-
...localePredicates(args.locale),
|
|
424
|
-
]);
|
|
425
|
-
conditions.push(...compiled.conditions);
|
|
426
|
-
binds.push(...compiled.binds);
|
|
304
|
+
this.addLocaleCondition(table, conditions, binds, args.locale);
|
|
427
305
|
if (args.excludeId) {
|
|
428
|
-
conditions.push("
|
|
306
|
+
conditions.push('"_mantle_id" <> ?');
|
|
429
307
|
binds.push(args.excludeId);
|
|
430
308
|
}
|
|
431
|
-
const row = await this.db
|
|
432
|
-
.
|
|
433
|
-
|
|
434
|
-
WHERE ${conditions.join(" AND ")}
|
|
435
|
-
ORDER BY updated_at DESC LIMIT 1`)
|
|
436
|
-
.bind(...binds)
|
|
437
|
-
.first();
|
|
438
|
-
return row ? rowFromDb(row) : null;
|
|
439
|
-
}
|
|
440
|
-
async versionConflict(id, expected) {
|
|
441
|
-
const after = await this.db
|
|
442
|
-
.prepare(`SELECT version FROM entries WHERE id = ?`)
|
|
443
|
-
.bind(id)
|
|
444
|
-
.first();
|
|
445
|
-
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;
|
|
446
312
|
}
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
// values for the parent `IN` predicate.
|
|
452
|
-
const ENTRY_READ_BATCH_SIZE = 95;
|
|
453
|
-
function localePredicates(locale) {
|
|
454
|
-
if (locale === undefined)
|
|
455
|
-
return [];
|
|
456
|
-
return locale === null
|
|
457
|
-
? [{ field: "locale", kind: "null" }]
|
|
458
|
-
: [{ field: "locale", kind: "equal", value: locale }];
|
|
459
|
-
}
|
|
460
|
-
function compileDataPredicates(schema, predicates) {
|
|
461
|
-
const conditions = [];
|
|
462
|
-
const binds = [];
|
|
463
|
-
const indexed = usableIndexedFields(schema, predicates);
|
|
464
|
-
for (const predicate of predicates) {
|
|
465
|
-
const generated = schema && indexed.has(predicate.field)
|
|
466
|
-
? schemaIndexedFieldSql(schema, predicate.field)
|
|
467
|
-
: null;
|
|
468
|
-
const reference = generated ?? "json_extract(data, ?)";
|
|
469
|
-
if (!generated)
|
|
470
|
-
binds.push(jsonPathForTopLevelField(predicate.field));
|
|
471
|
-
if (predicate.kind === "null") {
|
|
472
|
-
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);
|
|
473
317
|
}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
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;
|
|
477
328
|
}
|
|
329
|
+
if (locale === null)
|
|
330
|
+
conditions.push(`${field} IS NULL`);
|
|
478
331
|
else {
|
|
479
|
-
conditions.push(`${
|
|
480
|
-
binds.push(
|
|
332
|
+
conditions.push(`${field} = ?`);
|
|
333
|
+
binds.push(locale);
|
|
481
334
|
}
|
|
482
335
|
}
|
|
483
|
-
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
if (!schema)
|
|
487
|
-
return new Set();
|
|
488
|
-
const byField = new Map(predicates.map((predicate) => [predicate.field, predicate]));
|
|
489
|
-
const usable = new Set();
|
|
490
|
-
const declarations = [
|
|
491
|
-
...(schema.spec.uniqueIndexes ?? []),
|
|
492
|
-
...(schema.spec.indexes ?? []),
|
|
493
|
-
];
|
|
494
|
-
for (const declaration of declarations) {
|
|
495
|
-
for (let index = 0; index < declaration.length; index += 1) {
|
|
496
|
-
const field = declaration[index];
|
|
497
|
-
const predicate = byField.get(field);
|
|
498
|
-
if (!predicate)
|
|
499
|
-
break;
|
|
500
|
-
if (index === 0 &&
|
|
501
|
-
(predicate.kind === "null" ||
|
|
502
|
-
(predicate.kind === "equal" && predicate.value === null))) {
|
|
503
|
-
break;
|
|
504
|
-
}
|
|
505
|
-
usable.add(field);
|
|
506
|
-
}
|
|
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);
|
|
507
339
|
}
|
|
508
|
-
return usable;
|
|
509
|
-
}
|
|
510
|
-
function entrySortSql(schema, field) {
|
|
511
|
-
if (field === "id")
|
|
512
|
-
return "id";
|
|
513
|
-
if (field === "status")
|
|
514
|
-
return "status";
|
|
515
|
-
if (field === "updatedAt")
|
|
516
|
-
return "updated_at";
|
|
517
|
-
return schema ? schemaIndexedFieldSql(schema, field) : null;
|
|
518
340
|
}
|
|
519
|
-
function
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
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;
|
|
353
|
+
}
|
|
354
|
+
for (const field of dataFields ?? []) {
|
|
355
|
+
const property = properties[field];
|
|
356
|
+
if ((!property || isNullableJsonSchema(property)) && !Object.hasOwn(data, field))
|
|
357
|
+
data[field] = null;
|
|
531
358
|
}
|
|
532
|
-
return value;
|
|
533
|
-
}
|
|
534
|
-
function rowFromDb(row) {
|
|
535
|
-
const data = JSON.parse(row.data);
|
|
536
359
|
return {
|
|
537
|
-
id: row.
|
|
538
|
-
collection:
|
|
539
|
-
locale:
|
|
540
|
-
status: row.
|
|
541
|
-
version: row.
|
|
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,
|
|
542
365
|
data,
|
|
543
|
-
authorId: row.
|
|
544
|
-
createdAt: row.
|
|
545
|
-
updatedAt: row.
|
|
366
|
+
authorId: row._mantle_author_id,
|
|
367
|
+
createdAt: row._mantle_created_at,
|
|
368
|
+
updatedAt: row._mantle_updated_at,
|
|
546
369
|
};
|
|
547
370
|
}
|
|
548
|
-
function
|
|
549
|
-
|
|
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;
|
|
376
|
+
}
|
|
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")
|
|
387
|
+
throw new Error(`non-scalar sort value for ${field}`);
|
|
388
|
+
return value;
|
|
389
|
+
}
|
|
390
|
+
function quote(value) {
|
|
391
|
+
return `"${value.replace(/"/g, '""')}"`;
|
|
550
392
|
}
|
|
551
|
-
/**
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
return new DatabaseEntryRepository(db).readBySlug(args);
|
|
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);
|
|
555
396
|
}
|
|
556
|
-
function isDriverUniqueConstraintError(
|
|
557
|
-
if (!(
|
|
397
|
+
function isDriverUniqueConstraintError(error) {
|
|
398
|
+
if (!(error instanceof Error))
|
|
558
399
|
return false;
|
|
559
|
-
const
|
|
560
|
-
return
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
err.code === "SQLITE_CONSTRAINT_UNIQUE" ||
|
|
564
|
-
err.code === "23505");
|
|
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";
|
|
565
404
|
}
|
|
566
405
|
//# sourceMappingURL=DatabaseEntryRepository.js.map
|