@krantho/persistence-postgres 0.0.0-stage → 1.0.1
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/LICENSE +21 -0
- package/dist/auth-adapter.d.ts +23 -0
- package/dist/auth-adapter.d.ts.map +1 -0
- package/dist/auth-adapter.js +159 -0
- package/dist/auth-adapter.js.map +1 -0
- package/dist/content-migration.d.ts +14 -0
- package/dist/content-migration.d.ts.map +1 -0
- package/dist/content-migration.js +64 -0
- package/dist/content-migration.js.map +1 -0
- package/dist/content-query.d.ts +23 -0
- package/dist/content-query.d.ts.map +1 -0
- package/dist/content-query.js +170 -0
- package/dist/content-query.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/migration-runner.d.ts +29 -0
- package/dist/migration-runner.d.ts.map +1 -0
- package/dist/migration-runner.js +123 -0
- package/dist/migration-runner.js.map +1 -0
- package/dist/migrations/index.d.ts +4 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/migrations/index.js +32 -0
- package/dist/migrations/index.js.map +1 -0
- package/dist/migrations/v001-initial-schema.d.ts +10 -0
- package/dist/migrations/v001-initial-schema.d.ts.map +1 -0
- package/dist/migrations/v001-initial-schema.js +94 -0
- package/dist/migrations/v001-initial-schema.js.map +1 -0
- package/dist/migrations/v002-auth-tables.d.ts +8 -0
- package/dist/migrations/v002-auth-tables.d.ts.map +1 -0
- package/dist/migrations/v002-auth-tables.js +37 -0
- package/dist/migrations/v002-auth-tables.js.map +1 -0
- package/dist/migrations/v003-user-permissions.d.ts +6 -0
- package/dist/migrations/v003-user-permissions.d.ts.map +1 -0
- package/dist/migrations/v003-user-permissions.js +18 -0
- package/dist/migrations/v003-user-permissions.js.map +1 -0
- package/dist/migrations/v004-media-hotspot.d.ts +14 -0
- package/dist/migrations/v004-media-hotspot.d.ts.map +1 -0
- package/dist/migrations/v004-media-hotspot.js +25 -0
- package/dist/migrations/v004-media-hotspot.js.map +1 -0
- package/dist/migrations/v005-content-locks.d.ts +6 -0
- package/dist/migrations/v005-content-locks.d.ts.map +1 -0
- package/dist/migrations/v005-content-locks.js +21 -0
- package/dist/migrations/v005-content-locks.js.map +1 -0
- package/dist/migrations/v006-webhook-deliveries.d.ts +6 -0
- package/dist/migrations/v006-webhook-deliveries.d.ts.map +1 -0
- package/dist/migrations/v006-webhook-deliveries.js +24 -0
- package/dist/migrations/v006-webhook-deliveries.js.map +1 -0
- package/dist/migrations/v007-media-folders.d.ts +6 -0
- package/dist/migrations/v007-media-folders.d.ts.map +1 -0
- package/dist/migrations/v007-media-folders.js +19 -0
- package/dist/migrations/v007-media-folders.js.map +1 -0
- package/dist/migrations/v008-site-settings.d.ts +6 -0
- package/dist/migrations/v008-site-settings.d.ts.map +1 -0
- package/dist/migrations/v008-site-settings.js +17 -0
- package/dist/migrations/v008-site-settings.js.map +1 -0
- package/dist/migrations/v009-seo-config.d.ts +12 -0
- package/dist/migrations/v009-seo-config.d.ts.map +1 -0
- package/dist/migrations/v009-seo-config.js +27 -0
- package/dist/migrations/v009-seo-config.js.map +1 -0
- package/dist/migrations/v010-content-live-version.d.ts +15 -0
- package/dist/migrations/v010-content-live-version.d.ts.map +1 -0
- package/dist/migrations/v010-content-live-version.js +40 -0
- package/dist/migrations/v010-content-live-version.js.map +1 -0
- package/dist/migrations/v011-media.d.ts +12 -0
- package/dist/migrations/v011-media.d.ts.map +1 -0
- package/dist/migrations/v011-media.js +43 -0
- package/dist/migrations/v011-media.js.map +1 -0
- package/dist/migrations/v012-page-cache.d.ts +12 -0
- package/dist/migrations/v012-page-cache.d.ts.map +1 -0
- package/dist/migrations/v012-page-cache.js +31 -0
- package/dist/migrations/v012-page-cache.js.map +1 -0
- package/dist/migrations/v013-content-contracts.d.ts +17 -0
- package/dist/migrations/v013-content-contracts.d.ts.map +1 -0
- package/dist/migrations/v013-content-contracts.js +41 -0
- package/dist/migrations/v013-content-contracts.js.map +1 -0
- package/dist/migrations/v014-content-manifest-commit.d.ts +7 -0
- package/dist/migrations/v014-content-manifest-commit.d.ts.map +1 -0
- package/dist/migrations/v014-content-manifest-commit.js +12 -0
- package/dist/migrations/v014-content-manifest-commit.js.map +1 -0
- package/dist/postgres-adapter.d.ts +71 -0
- package/dist/postgres-adapter.d.ts.map +1 -0
- package/dist/postgres-adapter.js +327 -0
- package/dist/postgres-adapter.js.map +1 -0
- package/dist/postgres-checkpoints.d.ts +20 -0
- package/dist/postgres-checkpoints.d.ts.map +1 -0
- package/dist/postgres-checkpoints.js +109 -0
- package/dist/postgres-checkpoints.js.map +1 -0
- package/dist/postgres-lock-adapter.d.ts +12 -0
- package/dist/postgres-lock-adapter.d.ts.map +1 -0
- package/dist/postgres-lock-adapter.js +62 -0
- package/dist/postgres-lock-adapter.js.map +1 -0
- package/dist/postgres-media-adapter.d.ts +28 -0
- package/dist/postgres-media-adapter.d.ts.map +1 -0
- package/dist/postgres-media-adapter.js +167 -0
- package/dist/postgres-media-adapter.js.map +1 -0
- package/dist/postgres-media-folder-adapter.d.ts +12 -0
- package/dist/postgres-media-folder-adapter.d.ts.map +1 -0
- package/dist/postgres-media-folder-adapter.js +60 -0
- package/dist/postgres-media-folder-adapter.js.map +1 -0
- package/dist/postgres-page-cache-store.d.ts +23 -0
- package/dist/postgres-page-cache-store.d.ts.map +1 -0
- package/dist/postgres-page-cache-store.js +80 -0
- package/dist/postgres-page-cache-store.js.map +1 -0
- package/dist/postgres-retention-adapter.d.ts +14 -0
- package/dist/postgres-retention-adapter.d.ts.map +1 -0
- package/dist/postgres-retention-adapter.js +61 -0
- package/dist/postgres-retention-adapter.js.map +1 -0
- package/dist/postgres-seo-config-adapter.d.ts +14 -0
- package/dist/postgres-seo-config-adapter.d.ts.map +1 -0
- package/dist/postgres-seo-config-adapter.js +59 -0
- package/dist/postgres-seo-config-adapter.js.map +1 -0
- package/dist/postgres-site-settings-adapter.d.ts +11 -0
- package/dist/postgres-site-settings-adapter.d.ts.map +1 -0
- package/dist/postgres-site-settings-adapter.js +30 -0
- package/dist/postgres-site-settings-adapter.js.map +1 -0
- package/dist/postgres-webhook-delivery-adapter.d.ts +27 -0
- package/dist/postgres-webhook-delivery-adapter.d.ts.map +1 -0
- package/dist/postgres-webhook-delivery-adapter.js +99 -0
- package/dist/postgres-webhook-delivery-adapter.js.map +1 -0
- package/dist/row-mapping.d.ts +40 -0
- package/dist/row-mapping.d.ts.map +1 -0
- package/dist/row-mapping.js +49 -0
- package/dist/row-mapping.js.map +1 -0
- package/package.json +39 -3
- package/README.md +0 -3
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { canonicalJson } from '@krantho/core';
|
|
3
|
+
import { authoredOf, ContentWriteConflict, liveOnWrite, nextAuthored, publicationOfFile, resolveLiveVersion, sameAuthoredValues, storedStatus, } from '@krantho/persistence';
|
|
4
|
+
import { runContentQuery } from './content-query.js';
|
|
5
|
+
import { runMigrations } from './migration-runner.js';
|
|
6
|
+
import { postgresMigrations } from './migrations/index.js';
|
|
7
|
+
import { rowToContentDocument, rowToDocumentMeta } from './row-mapping.js';
|
|
8
|
+
/**
|
|
9
|
+
* Status default used when a file declares none.
|
|
10
|
+
*
|
|
11
|
+
* A deployment sets it through `content.defaultStatus.filesystem` in the
|
|
12
|
+
* krantho configuration tree; the server passes the resolved value down. This
|
|
13
|
+
* literal is the standalone-library fallback and mirrors that tree's default.
|
|
14
|
+
*/
|
|
15
|
+
export const DEFAULT_POSTGRES_STATUS = {
|
|
16
|
+
filesystem: 'published',
|
|
17
|
+
};
|
|
18
|
+
export class PostgresAdapter {
|
|
19
|
+
sql;
|
|
20
|
+
defaultStatus;
|
|
21
|
+
defaultLocale;
|
|
22
|
+
initialization = null;
|
|
23
|
+
constructor(sql, options) {
|
|
24
|
+
this.sql = sql;
|
|
25
|
+
this.defaultLocale = options.defaultLocale;
|
|
26
|
+
this.defaultStatus = { ...DEFAULT_POSTGRES_STATUS, ...options.defaultStatus };
|
|
27
|
+
}
|
|
28
|
+
static async create(sql, options) {
|
|
29
|
+
const adapter = new PostgresAdapter(sql, options);
|
|
30
|
+
await adapter.ensureInitialized();
|
|
31
|
+
return adapter;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Run migrations once per instance. Concurrent callers share the in-flight
|
|
35
|
+
* run; a failed run is dropped so the next call retries it.
|
|
36
|
+
*/
|
|
37
|
+
ensureInitialized() {
|
|
38
|
+
this.initialization ??= runMigrations(this.sql, postgresMigrations).then(() => undefined, (err) => {
|
|
39
|
+
this.initialization = null;
|
|
40
|
+
throw err;
|
|
41
|
+
});
|
|
42
|
+
return this.initialization;
|
|
43
|
+
}
|
|
44
|
+
// --- IContentLoader methods ---
|
|
45
|
+
async exists(slug) {
|
|
46
|
+
await this.ensureInitialized();
|
|
47
|
+
const rows = await this.sql `
|
|
48
|
+
SELECT COUNT(*) as count FROM content
|
|
49
|
+
WHERE slug = ${slug} AND is_current = TRUE AND deleted_at IS NULL`;
|
|
50
|
+
return parseInt(rows[0]?.count ?? '0', 10) > 0;
|
|
51
|
+
}
|
|
52
|
+
async read(slug) {
|
|
53
|
+
await this.ensureInitialized();
|
|
54
|
+
const rows = await this.sql `
|
|
55
|
+
SELECT * FROM content
|
|
56
|
+
WHERE slug = ${slug} AND is_current = TRUE AND deleted_at IS NULL
|
|
57
|
+
LIMIT 1`;
|
|
58
|
+
const row = rows[0];
|
|
59
|
+
if (!row)
|
|
60
|
+
throw new Error(`Content not found: ${slug}`);
|
|
61
|
+
const yamlLines = Object.entries(row.fields).map(([k, v]) => `${k}: ${JSON.stringify(v)}`);
|
|
62
|
+
return `---\n${yamlLines.join('\n')}\n---\n${row.body}`;
|
|
63
|
+
}
|
|
64
|
+
async getMetadata(slug) {
|
|
65
|
+
await this.ensureInitialized();
|
|
66
|
+
const rows = await this.sql `
|
|
67
|
+
SELECT * FROM content
|
|
68
|
+
WHERE slug = ${slug} AND is_current = TRUE AND deleted_at IS NULL
|
|
69
|
+
LIMIT 1`;
|
|
70
|
+
const row = rows[0];
|
|
71
|
+
if (!row)
|
|
72
|
+
throw new Error(`Content not found: ${slug}`);
|
|
73
|
+
return {
|
|
74
|
+
size: row.body.length + JSON.stringify(row.fields).length,
|
|
75
|
+
lastModified: new Date(row.created_at),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
// --- IPersistenceAdapter methods ---
|
|
79
|
+
async listSlugs() {
|
|
80
|
+
await this.ensureInitialized();
|
|
81
|
+
const rows = await this.sql `
|
|
82
|
+
SELECT DISTINCT slug FROM content WHERE is_current = TRUE AND deleted_at IS NULL`;
|
|
83
|
+
return rows.map((r) => r.slug);
|
|
84
|
+
}
|
|
85
|
+
async writeContent(data, changedBy) {
|
|
86
|
+
await this.ensureInitialized();
|
|
87
|
+
await this.appendVersion(data.collection, data.slug, data.locale ?? this.defaultLocale, {
|
|
88
|
+
authored: authoredOf(data),
|
|
89
|
+
expectedVersion: data.expectedVersion,
|
|
90
|
+
fields: data.fields,
|
|
91
|
+
body: data.body,
|
|
92
|
+
html: data.html ?? '',
|
|
93
|
+
sections: data.sections ?? {},
|
|
94
|
+
source: 'api',
|
|
95
|
+
filePath: null,
|
|
96
|
+
changedBy: changedBy ?? null,
|
|
97
|
+
publication: data.publication,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
async deleteContent(collection, slug, locale, changedBy) {
|
|
101
|
+
await this.ensureInitialized();
|
|
102
|
+
const loc = locale ?? this.defaultLocale;
|
|
103
|
+
const rows = await this.sql `
|
|
104
|
+
SELECT * FROM content
|
|
105
|
+
WHERE collection = ${collection} AND slug = ${slug} AND locale = ${loc}
|
|
106
|
+
AND is_current = TRUE AND deleted_at IS NULL
|
|
107
|
+
LIMIT 1`;
|
|
108
|
+
const row = rows[0];
|
|
109
|
+
if (!row)
|
|
110
|
+
return;
|
|
111
|
+
const maxRows = await this.sql `
|
|
112
|
+
SELECT MAX(version) as max_version FROM content
|
|
113
|
+
WHERE collection = ${collection} AND slug = ${slug} AND locale = ${row.locale}`;
|
|
114
|
+
const nextVersion = (maxRows[0]?.max_version ?? 0) + 1;
|
|
115
|
+
const now = new Date().toISOString();
|
|
116
|
+
await this.sql.begin(async (tx) => {
|
|
117
|
+
await tx.unsafe('UPDATE content SET is_current = FALSE WHERE collection = $1 AND slug = $2 AND locale = $3 AND is_current = TRUE', [collection, slug, row.locale]);
|
|
118
|
+
await tx.unsafe(`INSERT INTO content (id, collection, slug, locale, version, is_current, status, fields, body, html, sections, source, file_path, changed_by, deleted_at, created_at, live_version)
|
|
119
|
+
VALUES ($1, $2, $3, $4, $5, TRUE, 'deleted', '{}'::jsonb, '', '', '{}'::jsonb, 'api', NULL, $6, $7, $7, NULL)`, [randomUUID(), collection, slug, row.locale, nextVersion, changedBy ?? null, now]);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
async getContent(collection, slug, locale) {
|
|
123
|
+
await this.ensureInitialized();
|
|
124
|
+
const loc = locale ?? this.defaultLocale;
|
|
125
|
+
const rows = await this.sql `
|
|
126
|
+
SELECT * FROM content
|
|
127
|
+
WHERE collection = ${collection} AND slug = ${slug} AND locale = ${loc}
|
|
128
|
+
AND is_current = TRUE AND deleted_at IS NULL
|
|
129
|
+
LIMIT 1`;
|
|
130
|
+
const row = rows[0];
|
|
131
|
+
return row ? rowToContentDocument(row) : null;
|
|
132
|
+
}
|
|
133
|
+
async getPublished(collection, slug, locale) {
|
|
134
|
+
await this.ensureInitialized();
|
|
135
|
+
const loc = locale ?? this.defaultLocale;
|
|
136
|
+
const rows = await this.sql `
|
|
137
|
+
SELECT live.* FROM content cur
|
|
138
|
+
JOIN content live ON live.collection = cur.collection AND live.slug = cur.slug
|
|
139
|
+
AND live.locale = cur.locale AND live.version = cur.live_version
|
|
140
|
+
WHERE cur.collection = ${collection} AND cur.slug = ${slug} AND cur.locale = ${loc}
|
|
141
|
+
AND cur.is_current = TRUE AND cur.deleted_at IS NULL
|
|
142
|
+
LIMIT 1`;
|
|
143
|
+
const row = rows[0];
|
|
144
|
+
return row ? rowToContentDocument(row) : null;
|
|
145
|
+
}
|
|
146
|
+
async query(spec) {
|
|
147
|
+
await this.ensureInitialized();
|
|
148
|
+
return runContentQuery(this.sql, spec);
|
|
149
|
+
}
|
|
150
|
+
async listVersions(collection, slug, locale) {
|
|
151
|
+
await this.ensureInitialized();
|
|
152
|
+
const loc = locale ?? this.defaultLocale;
|
|
153
|
+
const rows = await this.sql `
|
|
154
|
+
SELECT id, collection, slug, locale, version, is_current, status, source, file_path,
|
|
155
|
+
changed_by, deleted_at, created_at, live_version
|
|
156
|
+
FROM content
|
|
157
|
+
WHERE collection = ${collection} AND slug = ${slug} AND locale = ${loc}
|
|
158
|
+
ORDER BY version DESC`;
|
|
159
|
+
return rows.map(rowToDocumentMeta);
|
|
160
|
+
}
|
|
161
|
+
async getVersion(collection, slug, version, locale) {
|
|
162
|
+
await this.ensureInitialized();
|
|
163
|
+
const loc = locale ?? this.defaultLocale;
|
|
164
|
+
const rows = await this.sql `
|
|
165
|
+
SELECT * FROM content
|
|
166
|
+
WHERE collection = ${collection} AND slug = ${slug} AND locale = ${loc} AND version = ${version}
|
|
167
|
+
LIMIT 1`;
|
|
168
|
+
const row = rows[0];
|
|
169
|
+
return row ? rowToContentDocument(row) : null;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Import a filesystem content item — creates a new version if content changed.
|
|
173
|
+
* `modifiedAt` (ISO 8601) dates the version when the file last changed, such as
|
|
174
|
+
* its last git commit; without it the version is dated now.
|
|
175
|
+
*/
|
|
176
|
+
async upsertFromFile(collection, slug, fields, body, html, sections, filePath, locale, modifiedAt, authored) {
|
|
177
|
+
await this.ensureInitialized();
|
|
178
|
+
const loc = locale ?? this.defaultLocale;
|
|
179
|
+
const status = typeof fields['status'] === 'string' ? fields['status'] : this.defaultStatus.filesystem;
|
|
180
|
+
// Check if content exists and is unchanged
|
|
181
|
+
const existing = await this.sql `
|
|
182
|
+
SELECT * FROM content
|
|
183
|
+
WHERE collection = ${collection} AND slug = ${slug} AND locale = ${loc}
|
|
184
|
+
AND is_current = TRUE AND deleted_at IS NULL
|
|
185
|
+
LIMIT 1`;
|
|
186
|
+
const row = existing[0];
|
|
187
|
+
if (row &&
|
|
188
|
+
canonicalJson(row.fields) === canonicalJson(fields) &&
|
|
189
|
+
row.body === body &&
|
|
190
|
+
sameAuthoredValues(row.authored, authored ?? {})) {
|
|
191
|
+
return; // No change — skip
|
|
192
|
+
}
|
|
193
|
+
await this.appendVersion(collection, slug, loc, {
|
|
194
|
+
fields,
|
|
195
|
+
body,
|
|
196
|
+
html,
|
|
197
|
+
sections,
|
|
198
|
+
source: 'filesystem',
|
|
199
|
+
filePath,
|
|
200
|
+
changedBy: null,
|
|
201
|
+
publication: authored ? authored.publication : publicationOfFile(status),
|
|
202
|
+
authored: authoredOf(authored ?? {}),
|
|
203
|
+
expectedVersion: authored?.expectedVersion,
|
|
204
|
+
createdAt: modifiedAt,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
/** Restore content to a specific version — a save: a new current version from the old one, the live version kept */
|
|
208
|
+
async restoreVersion(collection, slug, version, changedBy, locale) {
|
|
209
|
+
await this.ensureInitialized();
|
|
210
|
+
const loc = locale ?? this.defaultLocale;
|
|
211
|
+
const rows = await this.sql `
|
|
212
|
+
SELECT * FROM content
|
|
213
|
+
WHERE collection = ${collection} AND slug = ${slug} AND locale = ${loc} AND version = ${version}
|
|
214
|
+
LIMIT 1`;
|
|
215
|
+
const target = rows[0];
|
|
216
|
+
if (!target)
|
|
217
|
+
return false;
|
|
218
|
+
await this.appendVersion(collection, slug, target.locale, {
|
|
219
|
+
authored: target.authored,
|
|
220
|
+
fields: target.fields,
|
|
221
|
+
body: target.body,
|
|
222
|
+
html: target.html,
|
|
223
|
+
sections: target.sections,
|
|
224
|
+
source: target.source,
|
|
225
|
+
filePath: target.file_path,
|
|
226
|
+
changedBy: changedBy ?? `restore:v${version}`,
|
|
227
|
+
publication: undefined,
|
|
228
|
+
});
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
// --- Runtime Schema methods ---
|
|
232
|
+
async getRuntimeSchema(collection) {
|
|
233
|
+
await this.ensureInitialized();
|
|
234
|
+
const rows = await this.sql `SELECT collection, fields, metadata_groups, created_at FROM collection_schemas
|
|
235
|
+
WHERE collection = ${collection} AND is_current = TRUE AND deleted_at IS NULL`;
|
|
236
|
+
const row = rows[0];
|
|
237
|
+
if (!row)
|
|
238
|
+
return null;
|
|
239
|
+
return {
|
|
240
|
+
collection: row.collection,
|
|
241
|
+
fields: row.fields,
|
|
242
|
+
metadata: row.metadata_groups,
|
|
243
|
+
updatedAt: row.created_at instanceof Date ? row.created_at.toISOString() : String(row.created_at),
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
async saveRuntimeSchema(collection, schema, changedBy) {
|
|
247
|
+
await this.ensureInitialized();
|
|
248
|
+
const current = await this.sql `
|
|
249
|
+
SELECT version FROM collection_schemas
|
|
250
|
+
WHERE collection = ${collection} AND is_current = TRUE`;
|
|
251
|
+
const nextVersion = current[0] ? current[0].version + 1 : 1;
|
|
252
|
+
const now = new Date().toISOString();
|
|
253
|
+
// See `appendVersion` for the JSONB binding explanation — `sql.json()`
|
|
254
|
+
// wraps the fields array as a JSONB parameter so the driver binds it
|
|
255
|
+
// as an object, not a JSON-text-wrapped-as-string.
|
|
256
|
+
await this.sql.begin(async (tx) => {
|
|
257
|
+
if (current[0]) {
|
|
258
|
+
await tx.unsafe('UPDATE collection_schemas SET is_current = FALSE WHERE collection = $1 AND is_current = TRUE', [collection]);
|
|
259
|
+
}
|
|
260
|
+
await tx.unsafe(`INSERT INTO collection_schemas (row_id, collection, version, is_current, fields, changed_by, created_at, metadata_groups)
|
|
261
|
+
VALUES ($1, $2, $3, TRUE, $4, $5, $6, $7)`, [
|
|
262
|
+
randomUUID(),
|
|
263
|
+
collection,
|
|
264
|
+
nextVersion,
|
|
265
|
+
this.sql.json(schema.fields),
|
|
266
|
+
changedBy ?? null,
|
|
267
|
+
now,
|
|
268
|
+
this.sql.json((schema.metadata ?? [])),
|
|
269
|
+
]);
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
/** Close the database connection */
|
|
273
|
+
async close() {
|
|
274
|
+
await this.sql.end();
|
|
275
|
+
}
|
|
276
|
+
// --- Private helpers ---
|
|
277
|
+
/**
|
|
278
|
+
* Core append-only write: flip current, insert the new version. Its live
|
|
279
|
+
* version follows the write's publication (a save keeps it) and its stored
|
|
280
|
+
* status follows the live version.
|
|
281
|
+
*/
|
|
282
|
+
async appendVersion(collection, slug, locale, data) {
|
|
283
|
+
const now = data.createdAt ?? new Date().toISOString();
|
|
284
|
+
// JSONB binding note: the postgres driver's `sql.json(value)` helper
|
|
285
|
+
// wraps an object as a JSONB parameter for `.unsafe()` calls. A previous
|
|
286
|
+
// implementation used `JSON.stringify(fields)` with a `::jsonb` cast,
|
|
287
|
+
// which double-encoded the value — JSONB stored the JSON text as a
|
|
288
|
+
// string literal, so every read returned the stringified text instead of
|
|
289
|
+
// the parsed object. Pass objects through `this.sql.json(...)` to bind
|
|
290
|
+
// correctly as JSONB without the redundant text→jsonb cast.
|
|
291
|
+
await this.sql.begin(async (tx) => {
|
|
292
|
+
await tx `SELECT pg_advisory_xact_lock(hashtextextended(${JSON.stringify([collection, slug, locale])}, 0))`;
|
|
293
|
+
const maxRows = await tx `
|
|
294
|
+
SELECT MAX(version) as max_version FROM content
|
|
295
|
+
WHERE collection = ${collection} AND slug = ${slug} AND locale = ${locale}`;
|
|
296
|
+
const nextVersion = (maxRows[0]?.max_version ?? 0) + 1;
|
|
297
|
+
if (data.expectedVersion !== undefined &&
|
|
298
|
+
data.expectedVersion !== (maxRows[0]?.max_version ?? 0)) {
|
|
299
|
+
throw new ContentWriteConflict(maxRows[0]?.max_version ?? 0, data.expectedVersion);
|
|
300
|
+
}
|
|
301
|
+
const previous = (await tx.unsafe('SELECT live_version, status, authored FROM content WHERE collection = $1 AND slug = $2 AND locale = $3 AND is_current = TRUE', [collection, slug, locale]));
|
|
302
|
+
const liveVersion = resolveLiveVersion(liveOnWrite(data.publication), nextVersion, previous[0]?.live_version ?? null);
|
|
303
|
+
const status = storedStatus(data.publication, liveVersion, previous[0]?.status);
|
|
304
|
+
await tx.unsafe('UPDATE content SET is_current = FALSE WHERE collection = $1 AND slug = $2 AND locale = $3 AND is_current = TRUE', [collection, slug, locale]);
|
|
305
|
+
await tx.unsafe(`INSERT INTO content (id, collection, slug, locale, version, is_current, status, fields, body, html, sections, source, file_path, changed_by, created_at, live_version, authored)
|
|
306
|
+
VALUES ($1, $2, $3, $4, $5, TRUE, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)`, [
|
|
307
|
+
randomUUID(),
|
|
308
|
+
collection,
|
|
309
|
+
slug,
|
|
310
|
+
locale,
|
|
311
|
+
nextVersion,
|
|
312
|
+
status,
|
|
313
|
+
this.sql.json(data.fields),
|
|
314
|
+
data.body,
|
|
315
|
+
data.html,
|
|
316
|
+
this.sql.json(data.sections),
|
|
317
|
+
data.source,
|
|
318
|
+
data.filePath,
|
|
319
|
+
data.changedBy,
|
|
320
|
+
now,
|
|
321
|
+
liveVersion,
|
|
322
|
+
this.sql.json(nextAuthored(previous[0]?.authored ?? null, data.authored ?? {}, nextVersion)),
|
|
323
|
+
]);
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
//# sourceMappingURL=postgres-adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postgres-adapter.js","sourceRoot":"","sources":["../src/postgres-adapter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { CheckpointInfo, CheckpointTrigger, ICheckpointAdapter } from '@krantho/persistence';
|
|
2
|
+
import type { Sql } from 'postgres';
|
|
3
|
+
/**
|
|
4
|
+
* Postgres checkpoint adapter.
|
|
5
|
+
*
|
|
6
|
+
* Checkpoints are lightweight timestamp markers. Creating a checkpoint
|
|
7
|
+
* records the current timestamp. Restoring a checkpoint rewinds all
|
|
8
|
+
* versioned tables to their state at that timestamp by flipping
|
|
9
|
+
* `is_current` flags.
|
|
10
|
+
*/
|
|
11
|
+
export declare class PostgresCheckpointAdapter implements ICheckpointAdapter {
|
|
12
|
+
private readonly sql;
|
|
13
|
+
constructor(sql: Sql);
|
|
14
|
+
createCheckpoint(name?: string, trigger?: CheckpointTrigger): Promise<CheckpointInfo>;
|
|
15
|
+
listCheckpoints(): Promise<CheckpointInfo[]>;
|
|
16
|
+
getCheckpoint(version: number): Promise<CheckpointInfo | null>;
|
|
17
|
+
restoreCheckpoint(version: number): Promise<CheckpointInfo>;
|
|
18
|
+
deleteCheckpoint(version: number): Promise<boolean>;
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=postgres-checkpoints.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postgres-checkpoints.d.ts","sourceRoot":"","sources":["../src/postgres-checkpoints.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AACjG,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,UAAU,CAAA;AAqBnC;;;;;;;GAOG;AACH,qBAAa,yBAA0B,YAAW,kBAAkB;IACtD,OAAO,CAAC,QAAQ,CAAC,GAAG;gBAAH,GAAG,EAAE,GAAG;IAE/B,gBAAgB,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAAC,cAAc,CAAC;IA4BrF,eAAe,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC;IAM5C,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC;IAO9D,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;IAmE3D,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;CAK1D"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
function rowToCheckpointInfo(row) {
|
|
2
|
+
return {
|
|
3
|
+
id: row.id,
|
|
4
|
+
version: row.version,
|
|
5
|
+
name: row.name,
|
|
6
|
+
trigger: row.trigger,
|
|
7
|
+
createdAt: row.created_at instanceof Date ? row.created_at.toISOString() : String(row.created_at),
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Postgres checkpoint adapter.
|
|
12
|
+
*
|
|
13
|
+
* Checkpoints are lightweight timestamp markers. Creating a checkpoint
|
|
14
|
+
* records the current timestamp. Restoring a checkpoint rewinds all
|
|
15
|
+
* versioned tables to their state at that timestamp by flipping
|
|
16
|
+
* `is_current` flags.
|
|
17
|
+
*/
|
|
18
|
+
export class PostgresCheckpointAdapter {
|
|
19
|
+
sql;
|
|
20
|
+
constructor(sql) {
|
|
21
|
+
this.sql = sql;
|
|
22
|
+
}
|
|
23
|
+
async createCheckpoint(name, trigger) {
|
|
24
|
+
const now = new Date().toISOString();
|
|
25
|
+
const triggerValue = trigger ?? 'manual';
|
|
26
|
+
// Allocate next version number and insert checkpoint in a transaction
|
|
27
|
+
const result = await this.sql.begin(async (tx) => {
|
|
28
|
+
const versionRows = (await tx.unsafe('UPDATE checkpoint_counter SET next = next + 1 RETURNING next - 1 as version'));
|
|
29
|
+
const version = versionRows[0].version;
|
|
30
|
+
const insertRows = (await tx.unsafe('INSERT INTO checkpoints (version, name, trigger, created_at) VALUES ($1, $2, $3, $4) RETURNING id', [version, name ?? null, triggerValue, now]));
|
|
31
|
+
return { id: insertRows[0].id, version };
|
|
32
|
+
});
|
|
33
|
+
return {
|
|
34
|
+
id: result.id,
|
|
35
|
+
version: result.version,
|
|
36
|
+
name: name ?? null,
|
|
37
|
+
trigger: triggerValue,
|
|
38
|
+
createdAt: now,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
async listCheckpoints() {
|
|
42
|
+
const rows = await this.sql `
|
|
43
|
+
SELECT * FROM checkpoints ORDER BY version DESC`;
|
|
44
|
+
return rows.map(rowToCheckpointInfo);
|
|
45
|
+
}
|
|
46
|
+
async getCheckpoint(version) {
|
|
47
|
+
const rows = await this.sql `
|
|
48
|
+
SELECT * FROM checkpoints WHERE version = ${version}`;
|
|
49
|
+
const row = rows[0];
|
|
50
|
+
return row ? rowToCheckpointInfo(row) : null;
|
|
51
|
+
}
|
|
52
|
+
async restoreCheckpoint(version) {
|
|
53
|
+
const checkpoint = await this.getCheckpoint(version);
|
|
54
|
+
if (!checkpoint) {
|
|
55
|
+
throw new Error(`Checkpoint version ${version} not found`);
|
|
56
|
+
}
|
|
57
|
+
// Create pre-restore checkpoint first
|
|
58
|
+
await this.createCheckpoint(`pre-restore-v${version}`, 'pre-restore');
|
|
59
|
+
const timestamp = checkpoint.createdAt;
|
|
60
|
+
// Restore all versioned tables to their state at checkpoint time
|
|
61
|
+
await this.sql.begin(async (tx) => {
|
|
62
|
+
// --- Content ---
|
|
63
|
+
// Clear all current flags
|
|
64
|
+
await tx.unsafe('UPDATE content SET is_current = FALSE WHERE is_current = TRUE');
|
|
65
|
+
// Re-activate the latest version of each entity that existed at checkpoint time
|
|
66
|
+
await tx.unsafe(`UPDATE content SET is_current = TRUE
|
|
67
|
+
WHERE id IN (
|
|
68
|
+
SELECT id FROM (
|
|
69
|
+
SELECT id, collection, slug, locale, created_at,
|
|
70
|
+
ROW_NUMBER() OVER (PARTITION BY collection, slug, locale ORDER BY version DESC) as rn
|
|
71
|
+
FROM content
|
|
72
|
+
WHERE created_at <= $1
|
|
73
|
+
AND (deleted_at IS NULL OR deleted_at > $1)
|
|
74
|
+
) sub WHERE rn = 1
|
|
75
|
+
)`, [timestamp]);
|
|
76
|
+
// --- Collection schemas ---
|
|
77
|
+
await tx.unsafe('UPDATE collection_schemas SET is_current = FALSE WHERE is_current = TRUE');
|
|
78
|
+
await tx.unsafe(`UPDATE collection_schemas SET is_current = TRUE
|
|
79
|
+
WHERE row_id IN (
|
|
80
|
+
SELECT row_id FROM (
|
|
81
|
+
SELECT row_id, collection,
|
|
82
|
+
ROW_NUMBER() OVER (PARTITION BY collection ORDER BY version DESC) as rn
|
|
83
|
+
FROM collection_schemas
|
|
84
|
+
WHERE created_at <= $1
|
|
85
|
+
AND (deleted_at IS NULL OR deleted_at > $1)
|
|
86
|
+
) sub WHERE rn = 1
|
|
87
|
+
)`, [timestamp]);
|
|
88
|
+
// --- Media ---
|
|
89
|
+
await tx.unsafe('UPDATE media SET is_current = FALSE WHERE is_current = TRUE');
|
|
90
|
+
await tx.unsafe(`UPDATE media SET is_current = TRUE
|
|
91
|
+
WHERE row_id IN (
|
|
92
|
+
SELECT row_id FROM (
|
|
93
|
+
SELECT row_id, id,
|
|
94
|
+
ROW_NUMBER() OVER (PARTITION BY id ORDER BY version DESC) as rn
|
|
95
|
+
FROM media
|
|
96
|
+
WHERE created_at <= $1
|
|
97
|
+
AND (deleted_at IS NULL OR deleted_at > $1)
|
|
98
|
+
) sub WHERE rn = 1
|
|
99
|
+
)`, [timestamp]);
|
|
100
|
+
});
|
|
101
|
+
return checkpoint;
|
|
102
|
+
}
|
|
103
|
+
async deleteCheckpoint(version) {
|
|
104
|
+
const result = await this.sql `
|
|
105
|
+
DELETE FROM checkpoints WHERE version = ${version}`;
|
|
106
|
+
return result.count > 0;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=postgres-checkpoints.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postgres-checkpoints.js","sourceRoot":"","sources":["../src/postgres-checkpoints.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ContentLock, ILockAdapter } from '@krantho/persistence';
|
|
2
|
+
import type { Sql } from 'postgres';
|
|
3
|
+
/** Postgres implementation of advisory content locking */
|
|
4
|
+
export declare class PostgresLockAdapter implements ILockAdapter {
|
|
5
|
+
private readonly sql;
|
|
6
|
+
constructor(sql: Sql);
|
|
7
|
+
getLock(collection: string, slug: string, locale: string): Promise<ContentLock | null>;
|
|
8
|
+
acquireLock(collection: string, slug: string, locale: string, lockedBy: string, expiresAt: string): Promise<void>;
|
|
9
|
+
releaseLock(collection: string, slug: string, locale: string): Promise<void>;
|
|
10
|
+
listLocks(): Promise<ContentLock[]>;
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=postgres-lock-adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postgres-lock-adapter.d.ts","sourceRoot":"","sources":["../src/postgres-lock-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;AACrE,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,UAAU,CAAA;AAWnC,0DAA0D;AAC1D,qBAAa,mBAAoB,YAAW,YAAY;IAC1C,OAAO,CAAC,QAAQ,CAAC,GAAG;gBAAH,GAAG,EAAE,GAAG;IAE/B,OAAO,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC;IA6BtF,WAAW,CACf,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC;IAWV,WAAW,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAM5E,SAAS,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;CAe1C"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/** Postgres implementation of advisory content locking */
|
|
2
|
+
export class PostgresLockAdapter {
|
|
3
|
+
sql;
|
|
4
|
+
constructor(sql) {
|
|
5
|
+
this.sql = sql;
|
|
6
|
+
}
|
|
7
|
+
async getLock(collection, slug, locale) {
|
|
8
|
+
const rows = await this.sql `
|
|
9
|
+
SELECT * FROM content_locks
|
|
10
|
+
WHERE collection = ${collection} AND slug = ${slug} AND locale = ${locale}`;
|
|
11
|
+
const row = rows[0];
|
|
12
|
+
if (!row)
|
|
13
|
+
return null;
|
|
14
|
+
// Clean up expired locks
|
|
15
|
+
const expiresAt = row.expires_at instanceof Date ? row.expires_at : new Date(String(row.expires_at));
|
|
16
|
+
if (expiresAt <= new Date()) {
|
|
17
|
+
await this.sql `
|
|
18
|
+
DELETE FROM content_locks
|
|
19
|
+
WHERE collection = ${collection} AND slug = ${slug} AND locale = ${locale}`;
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
const lockedAt = row.locked_at instanceof Date ? row.locked_at : new Date(String(row.locked_at));
|
|
23
|
+
return {
|
|
24
|
+
collection: row.collection,
|
|
25
|
+
slug: row.slug,
|
|
26
|
+
locale: row.locale,
|
|
27
|
+
lockedBy: row.locked_by,
|
|
28
|
+
lockedAt: lockedAt.toISOString(),
|
|
29
|
+
expiresAt: expiresAt.toISOString(),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
async acquireLock(collection, slug, locale, lockedBy, expiresAt) {
|
|
33
|
+
const now = new Date().toISOString();
|
|
34
|
+
await this.sql `
|
|
35
|
+
INSERT INTO content_locks (collection, slug, locale, locked_by, locked_at, expires_at)
|
|
36
|
+
VALUES (${collection}, ${slug}, ${locale}, ${lockedBy}, ${now}, ${expiresAt})
|
|
37
|
+
ON CONFLICT (collection, slug, locale) DO UPDATE SET
|
|
38
|
+
locked_by = EXCLUDED.locked_by,
|
|
39
|
+
locked_at = EXCLUDED.locked_at,
|
|
40
|
+
expires_at = EXCLUDED.expires_at`;
|
|
41
|
+
}
|
|
42
|
+
async releaseLock(collection, slug, locale) {
|
|
43
|
+
await this.sql `
|
|
44
|
+
DELETE FROM content_locks
|
|
45
|
+
WHERE collection = ${collection} AND slug = ${slug} AND locale = ${locale}`;
|
|
46
|
+
}
|
|
47
|
+
async listLocks() {
|
|
48
|
+
await this.sql `DELETE FROM content_locks WHERE expires_at <= now()`;
|
|
49
|
+
const rows = await this.sql `
|
|
50
|
+
SELECT * FROM content_locks ORDER BY collection, slug, locale`;
|
|
51
|
+
const iso = (value) => (value instanceof Date ? value : new Date(String(value))).toISOString();
|
|
52
|
+
return rows.map((row) => ({
|
|
53
|
+
collection: row.collection,
|
|
54
|
+
slug: row.slug,
|
|
55
|
+
locale: row.locale,
|
|
56
|
+
lockedBy: row.locked_by,
|
|
57
|
+
lockedAt: iso(row.locked_at),
|
|
58
|
+
expiresAt: iso(row.expires_at),
|
|
59
|
+
}));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=postgres-lock-adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postgres-lock-adapter.js","sourceRoot":"","sources":["../src/postgres-lock-adapter.ts"],"names":[],"mappings":"AAYA,0DAA0D;AAC1D,MAAM,OAAO,mBAAmB;IACD;IAA7B,YAA6B,GAAQ;QAAR,QAAG,GAAH,GAAG,CAAK;IAAG,CAAC;IAEzC,KAAK,CAAC,OAAO,CAAC,UAAkB,EAAE,IAAY,EAAE,MAAc;QAC5D,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,GAAG,CAAW;;2BAEf,UAAU,eAAe,IAAI,iBAAiB,MAAM,EAAE,CAAA;QAE7E,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,CAAA;QACnB,IAAI,CAAC,GAAG;YAAE,OAAO,IAAI,CAAA;QAErB,yBAAyB;QACzB,MAAM,SAAS,GACb,GAAG,CAAC,UAAU,YAAY,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAA;QACpF,IAAI,SAAS,IAAI,IAAI,IAAI,EAAE,EAAE,CAAC;YAC5B,MAAM,IAAI,CAAC,GAAG,CAAA;;6BAES,UAAU,eAAe,IAAI,iBAAiB,MAAM,EAAE,CAAA;YAC7E,OAAO,IAAI,CAAA;QACb,CAAC;QAED,MAAM,QAAQ,GAAG,GAAG,CAAC,SAAS,YAAY,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAA;QAChG,OAAO;YACL,UAAU,EAAE,GAAG,CAAC,UAAU;YAC1B,IAAI,EAAE,GAAG,CAAC,IAAI;YACd,MAAM,EAAE,GAAG,CAAC,MAAM;YAClB,QAAQ,EAAE,GAAG,CAAC,SAAS;YACvB,QAAQ,EAAE,QAAQ,CAAC,WAAW,EAAE;YAChC,SAAS,EAAE,SAAS,CAAC,WAAW,EAAE;SACnC,CAAA;IACH,CAAC;IAED,KAAK,CAAC,WAAW,CACf,UAAkB,EAClB,IAAY,EACZ,MAAc,EACd,QAAgB,EAChB,SAAiB;QAEjB,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAA;QACpC,MAAM,IAAI,CAAC,GAAG,CAAA;;gBAEF,UAAU,KAAK,IAAI,KAAK,MAAM,KAAK,QAAQ,KAAK,GAAG,KAAK,SAAS;;;;yCAIxC,CAAA;IACvC,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,UAAkB,EAAE,IAAY,EAAE,MAAc;QAChE,MAAM,IAAI,CAAC,GAAG,CAAA;;2BAES,UAAU,eAAe,IAAI,iBAAiB,MAAM,EAAE,CAAA;IAC/E,CAAC;IAED,KAAK,CAAC,SAAS;QACb,MAAM,IAAI,CAAC,GAAG,CAAA,qDAAqD,CAAA;QACnE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,GAAG,CAAW;oEAC0B,CAAA;QAChE,MAAM,GAAG,GAAG,CAAC,KAAW,EAAU,EAAE,CAClC,CAAC,KAAK,YAAY,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAAA;QACzE,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;YACxB,UAAU,EAAE,GAAG,CAAC,UAAU;YAC1B,IAAI,EAAE,GAAG,CAAC,IAAI;YACd,MAAM,EAAE,GAAG,CAAC,MAAM;YAClB,QAAQ,EAAE,GAAG,CAAC,SAAS;YACvB,QAAQ,EAAE,GAAG,CAAC,GAAG,CAAC,SAAS,CAAC;YAC5B,SAAS,EAAE,GAAG,CAAC,GAAG,CAAC,UAAU,CAAC;SAC/B,CAAC,CAAC,CAAA;IACL,CAAC;CACF"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { IMediaMetadataAdapter, MediaListOptions, MediaRecord, MediaRecordChanges, MediaStats, NewMediaRecord } from '@krantho/persistence';
|
|
2
|
+
import type { Sql } from 'postgres';
|
|
3
|
+
/**
|
|
4
|
+
* Postgres implementation of IMediaMetadataAdapter.
|
|
5
|
+
*
|
|
6
|
+
* Append-only: every change inserts a new version row and flips
|
|
7
|
+
* `is_current` inside one transaction; `UNIQUE (id, version)` rejects a
|
|
8
|
+
* concurrent duplicate. A delete appends a tombstone with `deleted_at` set.
|
|
9
|
+
*/
|
|
10
|
+
export declare class PostgresMediaAdapter implements IMediaMetadataAdapter {
|
|
11
|
+
private readonly sql;
|
|
12
|
+
constructor(sql: Sql);
|
|
13
|
+
insert(record: NewMediaRecord): Promise<MediaRecord>;
|
|
14
|
+
get(id: string): Promise<MediaRecord | null>;
|
|
15
|
+
list(options?: MediaListOptions | undefined): Promise<{
|
|
16
|
+
items: MediaRecord[];
|
|
17
|
+
total: number;
|
|
18
|
+
}>;
|
|
19
|
+
appendVersion(id: string, changes: MediaRecordChanges): Promise<MediaRecord | null>;
|
|
20
|
+
markDeleted(id: string): Promise<MediaRecord | null>;
|
|
21
|
+
stats(): Promise<MediaStats>;
|
|
22
|
+
listTags(): Promise<string[]>;
|
|
23
|
+
/** Insert the next version of `id` and make it current, inside `tx` */
|
|
24
|
+
private writeVersion;
|
|
25
|
+
/** WHERE clause for current records matching search, folder, and tag filters */
|
|
26
|
+
private buildFilter;
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=postgres-media-adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postgres-media-adapter.d.ts","sourceRoot":"","sources":["../src/postgres-media-adapter.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,qBAAqB,EAGrB,gBAAgB,EAChB,WAAW,EACX,kBAAkB,EAClB,UAAU,EACV,cAAc,EACf,MAAM,sBAAsB,CAAA;AAE7B,OAAO,KAAK,EAAE,GAAG,EAAkB,MAAM,UAAU,CAAA;AA8BnD;;;;;;GAMG;AACH,qBAAa,oBAAqB,YAAW,qBAAqB;IACpD,OAAO,CAAC,QAAQ,CAAC,GAAG;gBAAH,GAAG,EAAE,GAAG;IAE/B,MAAM,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,WAAW,CAAC;IAQpD,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC;IAK5C,IAAI,CACR,OAAO,CAAC,EAAE,gBAAgB,GAAG,SAAS,GACrC,OAAO,CAAC;QAAE,KAAK,EAAE,WAAW,EAAE,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IAe7C,aAAa,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC;IAUnF,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC;IAUpD,KAAK,IAAI,OAAO,CAAC,UAAU,CAAC;IAW5B,QAAQ,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;IAOnC,uEAAuE;YACzD,YAAY;IAoC1B,gFAAgF;IAChF,OAAO,CAAC,WAAW;CA0BpB"}
|