@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.
- package/README.md +74 -4
- package/dist/MantleRuntime.js +14 -6
- package/dist/MantleRuntime.js.map +1 -1
- package/dist/domain/model/EntryRow.d.ts +16 -16
- package/dist/domain/model/EntryRow.d.ts.map +1 -1
- package/dist/domain/model/EntryRow.js +34 -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 +38 -3
- package/dist/domain/port/EntryReader.d.ts.map +1 -1
- package/dist/domain/port/EntryRepository.d.ts +13 -6
- package/dist/domain/port/EntryRepository.d.ts.map +1 -1
- package/dist/domain/port/index.d.ts +2 -2
- package/dist/domain/port/index.d.ts.map +1 -1
- package/dist/domain/port/index.js.map +1 -1
- package/dist/domain/service/CallableCapabilityProjector.d.ts +28 -0
- package/dist/domain/service/CallableCapabilityProjector.d.ts.map +1 -0
- package/dist/domain/service/CallableCapabilityProjector.js +67 -0
- package/dist/domain/service/CallableCapabilityProjector.js.map +1 -0
- package/dist/domain/service/EntryMutationDiagnostics.d.ts.map +1 -1
- package/dist/domain/service/EntryMutationDiagnostics.js +11 -1
- package/dist/domain/service/EntryMutationDiagnostics.js.map +1 -1
- package/dist/domain/service/PathMatcher.d.ts +7 -0
- package/dist/domain/service/PathMatcher.d.ts.map +1 -1
- package/dist/domain/service/PathMatcher.js +56 -0
- package/dist/domain/service/PathMatcher.js.map +1 -1
- package/dist/domain/service/RuntimePlanCompiler.d.ts +7 -2
- package/dist/domain/service/RuntimePlanCompiler.d.ts.map +1 -1
- package/dist/domain/service/RuntimePlanCompiler.js +1 -0
- package/dist/domain/service/RuntimePlanCompiler.js.map +1 -1
- package/dist/domain/service/index.d.ts +1 -0
- package/dist/domain/service/index.d.ts.map +1 -1
- package/dist/domain/service/index.js +1 -0
- package/dist/domain/service/index.js.map +1 -1
- package/dist/domain/service/io/JoinedEntryReader.d.ts.map +1 -1
- package/dist/domain/service/io/JoinedEntryReader.js +2 -3
- package/dist/domain/service/io/JoinedEntryReader.js.map +1 -1
- package/dist/index.d.ts +12 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -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 -15
- package/dist/infrastructure/boot/canonicalMigrations.d.ts.map +1 -1
- package/dist/infrastructure/boot/canonicalMigrations.js +14 -200
- 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/http/createMantleRequestHandler.d.ts +3 -0
- package/dist/infrastructure/http/createMantleRequestHandler.d.ts.map +1 -1
- package/dist/infrastructure/http/createMantleRequestHandler.js +36 -15
- package/dist/infrastructure/http/createMantleRequestHandler.js.map +1 -1
- package/dist/infrastructure/http/readJsonBody.d.ts +8 -0
- package/dist/infrastructure/http/readJsonBody.d.ts.map +1 -0
- package/dist/infrastructure/http/readJsonBody.js +36 -0
- package/dist/infrastructure/http/readJsonBody.js.map +1 -0
- package/dist/infrastructure/mcp/McpJsonRpcDispatcher.d.ts +22 -17
- package/dist/infrastructure/mcp/McpJsonRpcDispatcher.d.ts.map +1 -1
- package/dist/infrastructure/mcp/McpJsonRpcDispatcher.js +89 -86
- package/dist/infrastructure/mcp/McpJsonRpcDispatcher.js.map +1 -1
- package/dist/infrastructure/mcp/McpToolCatalog.d.ts +11 -9
- package/dist/infrastructure/mcp/McpToolCatalog.d.ts.map +1 -1
- package/dist/infrastructure/mcp/McpToolCatalog.js +70 -80
- package/dist/infrastructure/mcp/McpToolCatalog.js.map +1 -1
- package/dist/infrastructure/persistence/DatabaseEntryRepository.d.ts +15 -20
- package/dist/infrastructure/persistence/DatabaseEntryRepository.d.ts.map +1 -1
- package/dist/infrastructure/persistence/DatabaseEntryRepository.js +291 -331
- package/dist/infrastructure/persistence/DatabaseEntryRepository.js.map +1 -1
- package/dist/infrastructure/persistence/DatabaseSiteConfigRepository.d.ts +2 -0
- package/dist/infrastructure/persistence/DatabaseSiteConfigRepository.d.ts.map +1 -1
- package/dist/infrastructure/persistence/DatabaseSiteConfigRepository.js +5 -1
- package/dist/infrastructure/persistence/DatabaseSiteConfigRepository.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 +6 -0
- package/dist/infrastructure/persistence/Pagination.d.ts.map +1 -1
- package/dist/infrastructure/persistence/Pagination.js +25 -0
- package/dist/infrastructure/persistence/Pagination.js.map +1 -1
- package/dist/infrastructure/storage/SqliteMantleStorageAdapter.d.ts +15 -1
- package/dist/infrastructure/storage/SqliteMantleStorageAdapter.d.ts.map +1 -1
- package/dist/infrastructure/storage/SqliteMantleStorageAdapter.js +64 -9
- 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/HttpBenchmarkHarness.d.ts +26 -1
- package/dist/infrastructure/testing/HttpBenchmarkHarness.d.ts.map +1 -1
- package/dist/infrastructure/testing/HttpBenchmarkHarness.js +27 -12
- package/dist/infrastructure/testing/HttpBenchmarkHarness.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.d.ts +32 -0
- package/dist/infrastructure/testing/StorageConformance.d.ts.map +1 -0
- package/dist/infrastructure/testing/StorageConformance.js +306 -0
- package/dist/infrastructure/testing/StorageConformance.js.map +1 -0
- package/dist/testing-storage.d.ts +3 -0
- package/dist/testing-storage.d.ts.map +1 -0
- package/dist/testing-storage.js +3 -0
- package/dist/testing-storage.js.map +1 -0
- 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/ListEntriesUseCase.d.ts +1 -1
- package/dist/usecase/content/ListEntriesUseCase.d.ts.map +1 -1
- package/dist/usecase/content/ListEntriesUseCase.js +18 -2
- package/dist/usecase/content/ListEntriesUseCase.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 +8 -5
- package/dist/usecase/dto/content/index.d.ts.map +1 -1
- package/dist/usecase/media/DeleteMediaAssetUseCase.d.ts +3 -3
- package/dist/usecase/media/DeleteMediaAssetUseCase.d.ts.map +1 -1
- package/dist/usecase/media/DeleteMediaAssetUseCase.js +35 -19
- package/dist/usecase/media/DeleteMediaAssetUseCase.js.map +1 -1
- package/dist/usecase/procedure/InvokeBuiltinUseCase.d.ts +8 -2
- package/dist/usecase/procedure/InvokeBuiltinUseCase.d.ts.map +1 -1
- package/dist/usecase/procedure/InvokeBuiltinUseCase.js +98 -16
- package/dist/usecase/procedure/InvokeBuiltinUseCase.js.map +1 -1
- package/dist/usecase/procedure/InvokeProcedureUseCase.js +2 -2
- package/dist/usecase/procedure/InvokeProcedureUseCase.js.map +1 -1
- package/dist/usecase/site/UpdateSiteSettingsUseCase.d.ts.map +1 -1
- package/dist/usecase/site/UpdateSiteSettingsUseCase.js +8 -1
- package/dist/usecase/site/UpdateSiteSettingsUseCase.js.map +1 -1
- package/package.json +9 -5
|
@@ -1,18 +1,9 @@
|
|
|
1
|
-
import {
|
|
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 {
|
|
4
|
-
import {
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
.
|
|
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(
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
.
|
|
56
|
-
|
|
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
|
|
63
|
-
const
|
|
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
|
-
.
|
|
77
|
-
|
|
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
|
|
88
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
132
|
-
|
|
133
|
-
|
|
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("
|
|
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
|
|
141
|
+
const search = ['"_mantle_id" LIKE \'%\'||?||\'%\' ESCAPE \'\\\''];
|
|
146
142
|
binds.push(term);
|
|
147
143
|
for (const field of args.searchFields ?? []) {
|
|
148
|
-
|
|
149
|
-
binds.push(
|
|
144
|
+
search.push(`${requiredFieldSql(table.schema, field)} LIKE '%'||?||'%' ESCAPE '\\'`);
|
|
145
|
+
binds.push(term);
|
|
150
146
|
}
|
|
151
|
-
conditions.push(`(${
|
|
147
|
+
conditions.push(`(${search.join(" OR ")})`);
|
|
152
148
|
}
|
|
153
149
|
if (args.filter) {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
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(
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
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
|
|
171
|
+
const last = page.at(-1);
|
|
182
172
|
return {
|
|
183
|
-
rows: page.map(rowFromDb),
|
|
184
|
-
previousCursor: first && (backward ? hasMore : cursor !== null)
|
|
185
|
-
|
|
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.
|
|
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.
|
|
182
|
+
return this.findOne(args);
|
|
201
183
|
}
|
|
202
|
-
async readById(
|
|
203
|
-
const row = await this.get(
|
|
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.
|
|
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
|
|
197
|
+
if (!values.length)
|
|
227
198
|
return [];
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
const
|
|
233
|
-
const
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
-
|
|
252
|
-
return entries;
|
|
215
|
+
return output.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
253
216
|
}
|
|
254
|
-
async readPublished(args
|
|
255
|
-
const
|
|
217
|
+
async readPublished(args) {
|
|
218
|
+
const table = this.table(args.collection);
|
|
219
|
+
const conditions = ['"_mantle_status" = \'published\''];
|
|
256
220
|
const binds = [];
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
|
|
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
|
-
|
|
265
|
-
|
|
266
|
-
binds.push(args.collection);
|
|
241
|
+
else {
|
|
242
|
+
this.addLocaleCondition(table, conditions, binds, args.locale);
|
|
267
243
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
|
276
|
-
|
|
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
|
|
280
|
-
const
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
-
|
|
296
|
-
const
|
|
297
|
-
if (
|
|
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("
|
|
301
|
+
conditions.push('"_mantle_status" = ?');
|
|
304
302
|
binds.push(args.status);
|
|
305
303
|
}
|
|
306
|
-
|
|
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("
|
|
306
|
+
conditions.push('"_mantle_id" <> ?');
|
|
318
307
|
binds.push(args.excludeId);
|
|
319
308
|
}
|
|
320
|
-
const row = await this.db
|
|
321
|
-
.
|
|
322
|
-
|
|
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
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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(`${
|
|
369
|
-
binds.push(
|
|
332
|
+
conditions.push(`${field} = ?`);
|
|
333
|
+
binds.push(locale);
|
|
370
334
|
}
|
|
371
335
|
}
|
|
372
|
-
|
|
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
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
const
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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
|
-
|
|
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
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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
|
|
424
|
-
|
|
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
|
-
|
|
438
|
-
|
|
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
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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
|