@krantho/persistence-postgres 0.0.0-stage → 1.0.0
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 +38 -3
- package/README.md +0 -3
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { escapeSqlLikePattern } from '@krantho/persistence';
|
|
3
|
+
const CURRENT = 'is_current AND deleted_at IS NULL';
|
|
4
|
+
/**
|
|
5
|
+
* Postgres implementation of IMediaMetadataAdapter.
|
|
6
|
+
*
|
|
7
|
+
* Append-only: every change inserts a new version row and flips
|
|
8
|
+
* `is_current` inside one transaction; `UNIQUE (id, version)` rejects a
|
|
9
|
+
* concurrent duplicate. A delete appends a tombstone with `deleted_at` set.
|
|
10
|
+
*/
|
|
11
|
+
export class PostgresMediaAdapter {
|
|
12
|
+
sql;
|
|
13
|
+
constructor(sql) {
|
|
14
|
+
this.sql = sql;
|
|
15
|
+
}
|
|
16
|
+
async insert(record) {
|
|
17
|
+
const { id, ...data } = record;
|
|
18
|
+
return this.sql.begin(async (tx) => {
|
|
19
|
+
await this.writeVersion(tx, id, data, null);
|
|
20
|
+
return requireCurrent(tx, id);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
async get(id) {
|
|
24
|
+
const row = await currentRow(this.sql, id);
|
|
25
|
+
return row ? rowToRecord(row) : null;
|
|
26
|
+
}
|
|
27
|
+
async list(options) {
|
|
28
|
+
const { where, params } = this.buildFilter(options);
|
|
29
|
+
const limit = `$${params.length + 1}`;
|
|
30
|
+
const offset = `$${params.length + 2}`;
|
|
31
|
+
const rows = (await this.sql.unsafe(`SELECT * FROM media ${where} ORDER BY created_at DESC LIMIT ${limit} OFFSET ${offset}`, [...params, options?.limit ?? null, options?.offset ?? 0]));
|
|
32
|
+
const count = (await this.sql.unsafe(`SELECT COUNT(*)::int AS count FROM media ${where}`, params));
|
|
33
|
+
return { items: rows.map(rowToRecord), total: count[0]?.count ?? 0 };
|
|
34
|
+
}
|
|
35
|
+
async appendVersion(id, changes) {
|
|
36
|
+
return this.sql.begin(async (tx) => {
|
|
37
|
+
const current = await currentRow(tx, id);
|
|
38
|
+
if (!current)
|
|
39
|
+
return null;
|
|
40
|
+
const { id: _id, createdAt: _createdAt, ...data } = rowToRecord(current);
|
|
41
|
+
await this.writeVersion(tx, id, { ...data, ...changes }, null);
|
|
42
|
+
return requireCurrent(tx, id);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
async markDeleted(id) {
|
|
46
|
+
return this.sql.begin(async (tx) => {
|
|
47
|
+
const current = await currentRow(tx, id);
|
|
48
|
+
if (!current)
|
|
49
|
+
return null;
|
|
50
|
+
const superseded = rowToRecord(current);
|
|
51
|
+
await this.writeVersion(tx, id, tombstone(superseded), new Date().toISOString());
|
|
52
|
+
return superseded;
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
async stats() {
|
|
56
|
+
const [totals] = (await this.sql.unsafe(`SELECT COUNT(*)::int AS items, COALESCE(SUM(size), 0)::bigint AS size FROM media WHERE ${CURRENT}`));
|
|
57
|
+
const folders = (await this.sql.unsafe(`SELECT folder, COUNT(*)::int AS count FROM media WHERE ${CURRENT} AND folder IS NOT NULL GROUP BY folder`));
|
|
58
|
+
const byFolder = Object.fromEntries(folders.map((row) => [row.folder, row.count]));
|
|
59
|
+
return { totalItems: totals?.items ?? 0, totalSize: Number(totals?.size ?? 0), byFolder };
|
|
60
|
+
}
|
|
61
|
+
async listTags() {
|
|
62
|
+
const rows = (await this.sql.unsafe(`SELECT DISTINCT jsonb_array_elements_text(tags) AS tag FROM media WHERE ${CURRENT}`));
|
|
63
|
+
return rows.map((row) => row.tag).sort();
|
|
64
|
+
}
|
|
65
|
+
/** Insert the next version of `id` and make it current, inside `tx` */
|
|
66
|
+
async writeVersion(tx, id, data, deletedAt) {
|
|
67
|
+
const max = (await tx.unsafe('SELECT MAX(version) AS max_version FROM media WHERE id = $1', [
|
|
68
|
+
id,
|
|
69
|
+
]));
|
|
70
|
+
const nextVersion = (max[0]?.max_version ?? 0) + 1;
|
|
71
|
+
await tx.unsafe('UPDATE media SET is_current = FALSE WHERE id = $1 AND is_current', [id]);
|
|
72
|
+
await tx.unsafe(`INSERT INTO media (row_id, id, version, is_current, filename, storage_key, url, alt_text, mime_type, size, width, height, folder, tags, crop, hotspot, deleted_at, created_at)
|
|
73
|
+
VALUES ($1, $2, $3, TRUE, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)`, [
|
|
74
|
+
randomUUID(),
|
|
75
|
+
id,
|
|
76
|
+
nextVersion,
|
|
77
|
+
data.filename,
|
|
78
|
+
data.storageKey,
|
|
79
|
+
data.url,
|
|
80
|
+
data.altText,
|
|
81
|
+
data.mimeType,
|
|
82
|
+
data.size,
|
|
83
|
+
data.width,
|
|
84
|
+
data.height,
|
|
85
|
+
data.folder,
|
|
86
|
+
this.sql.json(data.tags),
|
|
87
|
+
data.crop ? this.sql.json({ ...data.crop }) : null,
|
|
88
|
+
data.hotspot ? this.sql.json({ ...data.hotspot }) : null,
|
|
89
|
+
deletedAt,
|
|
90
|
+
deletedAt ?? new Date().toISOString(),
|
|
91
|
+
]);
|
|
92
|
+
}
|
|
93
|
+
/** WHERE clause for current records matching search, folder, and tag filters */
|
|
94
|
+
buildFilter(options) {
|
|
95
|
+
const conditions = [CURRENT];
|
|
96
|
+
const params = [];
|
|
97
|
+
if (options?.search) {
|
|
98
|
+
params.push(`%${escapeSqlLikePattern(options.search)}%`);
|
|
99
|
+
conditions.push(`filename ILIKE $${params.length} ESCAPE '\\'`);
|
|
100
|
+
}
|
|
101
|
+
if (options?.folder !== undefined) {
|
|
102
|
+
if (options.folder === '') {
|
|
103
|
+
conditions.push('folder IS NULL');
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
params.push(options.folder);
|
|
107
|
+
conditions.push(`folder = $${params.length}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (options?.tag) {
|
|
111
|
+
params.push(this.sql.json([options.tag]));
|
|
112
|
+
conditions.push(`tags @> $${params.length}`);
|
|
113
|
+
}
|
|
114
|
+
return { where: `WHERE ${conditions.join(' AND ')}`, params };
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
async function currentRow(sql, id) {
|
|
118
|
+
const rows = (await sql.unsafe(`SELECT * FROM media WHERE id = $1 AND ${CURRENT}`, [
|
|
119
|
+
id,
|
|
120
|
+
]));
|
|
121
|
+
return rows[0];
|
|
122
|
+
}
|
|
123
|
+
async function requireCurrent(tx, id) {
|
|
124
|
+
const row = await currentRow(tx, id);
|
|
125
|
+
if (!row)
|
|
126
|
+
throw new Error(`Media item ${id} not found after write`);
|
|
127
|
+
return rowToRecord(row);
|
|
128
|
+
}
|
|
129
|
+
/** The tombstone version that replaces a deleted record */
|
|
130
|
+
function tombstone(record) {
|
|
131
|
+
return {
|
|
132
|
+
filename: record.filename,
|
|
133
|
+
storageKey: record.storageKey,
|
|
134
|
+
url: record.url,
|
|
135
|
+
altText: '',
|
|
136
|
+
mimeType: record.mimeType,
|
|
137
|
+
size: 0,
|
|
138
|
+
width: null,
|
|
139
|
+
height: null,
|
|
140
|
+
folder: null,
|
|
141
|
+
tags: [],
|
|
142
|
+
crop: null,
|
|
143
|
+
hotspot: null,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
function toTags(value) {
|
|
147
|
+
return Array.isArray(value) ? value.filter((tag) => typeof tag === 'string') : [];
|
|
148
|
+
}
|
|
149
|
+
function rowToRecord(row) {
|
|
150
|
+
return {
|
|
151
|
+
id: row.id,
|
|
152
|
+
filename: row.filename,
|
|
153
|
+
storageKey: row.storage_key,
|
|
154
|
+
url: row.url,
|
|
155
|
+
altText: row.alt_text,
|
|
156
|
+
mimeType: row.mime_type,
|
|
157
|
+
size: row.size,
|
|
158
|
+
width: row.width,
|
|
159
|
+
height: row.height,
|
|
160
|
+
folder: row.folder,
|
|
161
|
+
tags: toTags(row.tags),
|
|
162
|
+
crop: row.crop,
|
|
163
|
+
hotspot: row.hotspot,
|
|
164
|
+
createdAt: row.created_at.toISOString(),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
//# sourceMappingURL=postgres-media-adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postgres-media-adapter.js","sourceRoot":"","sources":["../src/postgres-media-adapter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { IMediaFolderStorage, MediaFolder } from '@krantho/persistence';
|
|
2
|
+
import type { Sql } from 'postgres';
|
|
3
|
+
/** Postgres implementation of IMediaFolderStorage, over the `media_folders` table (v007) */
|
|
4
|
+
export declare class PostgresMediaFolderAdapter implements IMediaFolderStorage {
|
|
5
|
+
private readonly sql;
|
|
6
|
+
constructor(sql: Sql);
|
|
7
|
+
listFolders(): Promise<MediaFolder[]>;
|
|
8
|
+
createFolder(name: string, parentId?: string | null): Promise<MediaFolder>;
|
|
9
|
+
renameFolder(id: string, name: string): Promise<MediaFolder | null>;
|
|
10
|
+
deleteFolder(id: string): Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=postgres-media-folder-adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postgres-media-folder-adapter.d.ts","sourceRoot":"","sources":["../src/postgres-media-folder-adapter.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,mBAAmB,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAA;AAC5E,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,UAAU,CAAA;AASnC,4FAA4F;AAC5F,qBAAa,0BAA2B,YAAW,mBAAmB;IACxD,OAAO,CAAC,QAAQ,CAAC,GAAG;gBAAH,GAAG,EAAE,GAAG;IAE/B,WAAW,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;IAOrC,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,WAAW,CAAC;IAkB1E,YAAY,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC;IAOnE,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAc9C"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
/** Postgres implementation of IMediaFolderStorage, over the `media_folders` table (v007) */
|
|
3
|
+
export class PostgresMediaFolderAdapter {
|
|
4
|
+
sql;
|
|
5
|
+
constructor(sql) {
|
|
6
|
+
this.sql = sql;
|
|
7
|
+
}
|
|
8
|
+
async listFolders() {
|
|
9
|
+
// Byte-order collation, matching SQLite's BINARY ordering
|
|
10
|
+
const rows = await this.sql `
|
|
11
|
+
SELECT * FROM media_folders ORDER BY name COLLATE "C"`;
|
|
12
|
+
return rows.map(rowToFolder);
|
|
13
|
+
}
|
|
14
|
+
async createFolder(name, parentId) {
|
|
15
|
+
if (parentId) {
|
|
16
|
+
const parent = await this.sql `SELECT id FROM media_folders WHERE id = ${parentId}`;
|
|
17
|
+
if (parent.length === 0)
|
|
18
|
+
throw new Error(`Parent folder "${parentId}" not found`);
|
|
19
|
+
}
|
|
20
|
+
const folder = {
|
|
21
|
+
id: randomUUID(),
|
|
22
|
+
name: name.trim(),
|
|
23
|
+
parentId: parentId ?? null,
|
|
24
|
+
createdAt: new Date().toISOString(),
|
|
25
|
+
};
|
|
26
|
+
await this.sql `
|
|
27
|
+
INSERT INTO media_folders (id, name, parent_id, created_at)
|
|
28
|
+
VALUES (${folder.id}, ${folder.name}, ${folder.parentId}, ${folder.createdAt})`;
|
|
29
|
+
return folder;
|
|
30
|
+
}
|
|
31
|
+
async renameFolder(id, name) {
|
|
32
|
+
const rows = await this.sql `
|
|
33
|
+
UPDATE media_folders SET name = ${name.trim()} WHERE id = ${id} RETURNING *`;
|
|
34
|
+
const row = rows[0];
|
|
35
|
+
return row ? rowToFolder(row) : null;
|
|
36
|
+
}
|
|
37
|
+
async deleteFolder(id) {
|
|
38
|
+
const existing = await this.sql `SELECT id FROM media_folders WHERE id = ${id}`;
|
|
39
|
+
if (existing.length === 0)
|
|
40
|
+
throw new Error(`Folder "${id}" not found`);
|
|
41
|
+
const child = await this.sql `SELECT id FROM media_folders WHERE parent_id = ${id} LIMIT 1`;
|
|
42
|
+
if (child.length > 0)
|
|
43
|
+
throw new Error('Cannot delete folder: has child folders');
|
|
44
|
+
const media = await this.sql `
|
|
45
|
+
SELECT id FROM media
|
|
46
|
+
WHERE folder = ${id} AND is_current AND deleted_at IS NULL LIMIT 1`;
|
|
47
|
+
if (media.length > 0)
|
|
48
|
+
throw new Error('Cannot delete folder: contains media items');
|
|
49
|
+
await this.sql `DELETE FROM media_folders WHERE id = ${id}`;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function rowToFolder(row) {
|
|
53
|
+
return {
|
|
54
|
+
id: row.id,
|
|
55
|
+
name: row.name,
|
|
56
|
+
parentId: row.parent_id,
|
|
57
|
+
createdAt: row.created_at.toISOString(),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=postgres-media-folder-adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postgres-media-folder-adapter.js","sourceRoot":"","sources":["../src/postgres-media-folder-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAWxC,4FAA4F;AAC5F,MAAM,OAAO,0BAA0B;IACR;IAA7B,YAA6B,GAAQ;QAAR,QAAG,GAAH,GAAG,CAAK;IAAG,CAAC;IAEzC,KAAK,CAAC,WAAW;QACf,0DAA0D;QAC1D,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,GAAG,CAAa;4DACgB,CAAA;QACxD,OAAO,IAAI,CAAC,GAAG,CAAC,WAAW,CAAC,CAAA;IAC9B,CAAC;IAED,KAAK,CAAC,YAAY,CAAC,IAAY,EAAE,QAAwB;QACvD,IAAI,QAAQ,EAAE,CAAC;YACb,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,GAAG,CAAA,2CAA2C,QAAQ,EAAE,CAAA;YAClF,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;gBAAE,MAAM,IAAI,KAAK,CAAC,kBAAkB,QAAQ,aAAa,CAAC,CAAA;QACnF,CAAC;QAED,MAAM,MAAM,GAAgB;YAC1B,EAAE,EAAE,UAAU,EAAE;YAChB,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE;YACjB,QAAQ,EAAE,QAAQ,IAAI,IAAI;YAC1B,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;SACpC,CAAA;QACD,MAAM,IAAI,CAAC,GAAG,CAAA;;gBAEF,MAAM,CAAC,EAAE,KAAK,MAAM,CAAC,IAAI,KAAK,MAAM,CAAC,QAAQ,KAAK,MAAM,CAAC,SAAS,GAAG,CAAA;QACjF,OAAO,MAAM,CAAA;IACf,CAAC;IAED,KAAK,CAAC,YAAY,CAAC,EAAU,EAAE,IAAY;QACzC,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,GAAG,CAAa;wCACJ,IAAI,CAAC,IAAI,EAAE,eAAe,EAAE,cAAc,CAAA;QAC9E,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,CAAA;QACnB,OAAO,GAAG,CAAC,CAAC,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;IACtC,CAAC;IAED,KAAK,CAAC,YAAY,CAAC,EAAU;QAC3B,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,GAAG,CAAA,2CAA2C,EAAE,EAAE,CAAA;QAC9E,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,WAAW,EAAE,aAAa,CAAC,CAAA;QAEtE,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,GAAG,CAAA,kDAAkD,EAAE,UAAU,CAAA;QAC1F,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,yCAAyC,CAAC,CAAA;QAEhF,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,GAAG,CAAA;;uBAET,EAAE,gDAAgD,CAAA;QACrE,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,4CAA4C,CAAC,CAAA;QAEnF,MAAM,IAAI,CAAC,GAAG,CAAA,wCAAwC,EAAE,EAAE,CAAA;IAC5D,CAAC;CACF;AAED,SAAS,WAAW,CAAC,GAAc;IACjC,OAAO;QACL,EAAE,EAAE,GAAG,CAAC,EAAE;QACV,IAAI,EAAE,GAAG,CAAC,IAAI;QACd,QAAQ,EAAE,GAAG,CAAC,SAAS;QACvB,SAAS,EAAE,GAAG,CAAC,UAAU,CAAC,WAAW,EAAE;KACxC,CAAA;AACH,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { IPageCacheStore, PageCacheEntry, PageCacheMarkOptions, PageCacheTagState, PageCacheValue } from '@krantho/persistence';
|
|
2
|
+
import type { Sql } from 'postgres';
|
|
3
|
+
/**
|
|
4
|
+
* Postgres implementation of `IPageCacheStore`, shared by every instance of a
|
|
5
|
+
* site. Timestamps come from the database clock (`clock_timestamp()`), so an
|
|
6
|
+
* entry written on one instance and a mark made on another compare on one clock.
|
|
7
|
+
*/
|
|
8
|
+
export declare class PostgresPageCacheStore implements IPageCacheStore {
|
|
9
|
+
private readonly sql;
|
|
10
|
+
constructor(sql: Sql);
|
|
11
|
+
get(key: string): Promise<PageCacheEntry | null>;
|
|
12
|
+
set(key: string, value: PageCacheValue, tags: readonly string[]): Promise<void>;
|
|
13
|
+
tagsRevalidatedSince(tags: readonly string[], since: number): Promise<PageCacheTagState>;
|
|
14
|
+
/**
|
|
15
|
+
* One statement: atomic, and each tag's timestamps only ever move forward
|
|
16
|
+
* (`GREATEST` ignores NULL, so a stale mark keeps an earlier expiry). Tags are
|
|
17
|
+
* upserted in sorted order so concurrent marks lock rows in one order and
|
|
18
|
+
* cannot deadlock.
|
|
19
|
+
*/
|
|
20
|
+
markTags(tags: readonly string[], options: PageCacheMarkOptions): Promise<void>;
|
|
21
|
+
countTagged(tags: readonly string[]): Promise<number>;
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=postgres-page-cache-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postgres-page-cache-store.d.ts","sourceRoot":"","sources":["../src/postgres-page-cache-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EACf,cAAc,EACd,oBAAoB,EACpB,iBAAiB,EACjB,cAAc,EACf,MAAM,sBAAsB,CAAA;AAM7B,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,UAAU,CAAA;AAanC;;;;GAIG;AACH,qBAAa,sBAAuB,YAAW,eAAe;IAChD,OAAO,CAAC,QAAQ,CAAC,GAAG;gBAAH,GAAG,EAAE,GAAG;IAE/B,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC;IAYhD,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,cAAc,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAe/E,oBAAoB,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAa9F;;;;;OAKG;IACG,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC;IAa/E,WAAW,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC;CAO5D"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { decodePageCacheValue, encodePageCacheValue, uniquePageCacheTags, } from '@krantho/persistence';
|
|
2
|
+
/**
|
|
3
|
+
* Postgres implementation of `IPageCacheStore`, shared by every instance of a
|
|
4
|
+
* site. Timestamps come from the database clock (`clock_timestamp()`), so an
|
|
5
|
+
* entry written on one instance and a mark made on another compare on one clock.
|
|
6
|
+
*/
|
|
7
|
+
export class PostgresPageCacheStore {
|
|
8
|
+
sql;
|
|
9
|
+
constructor(sql) {
|
|
10
|
+
this.sql = sql;
|
|
11
|
+
}
|
|
12
|
+
async get(key) {
|
|
13
|
+
const rows = await this.sql `
|
|
14
|
+
SELECT value, tags, last_modified FROM page_cache_entries WHERE key = ${key}`;
|
|
15
|
+
const row = rows[0];
|
|
16
|
+
if (!row)
|
|
17
|
+
return null;
|
|
18
|
+
return {
|
|
19
|
+
value: decodePageCacheValue(row.value),
|
|
20
|
+
tags: row.tags,
|
|
21
|
+
lastModified: Number(row.last_modified),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
async set(key, value, tags) {
|
|
25
|
+
await this.sql `
|
|
26
|
+
INSERT INTO page_cache_entries (key, value, tags, last_modified)
|
|
27
|
+
VALUES (
|
|
28
|
+
${key},
|
|
29
|
+
${encodePageCacheValue(value)},
|
|
30
|
+
${this.sql.array(uniquePageCacheTags(tags))}::text[],
|
|
31
|
+
floor(extract(epoch FROM clock_timestamp()) * 1000)::bigint
|
|
32
|
+
)
|
|
33
|
+
ON CONFLICT (key) DO UPDATE SET
|
|
34
|
+
value = EXCLUDED.value,
|
|
35
|
+
tags = EXCLUDED.tags,
|
|
36
|
+
last_modified = EXCLUDED.last_modified`;
|
|
37
|
+
}
|
|
38
|
+
async tagsRevalidatedSince(tags, since) {
|
|
39
|
+
if (tags.length === 0)
|
|
40
|
+
return 'fresh';
|
|
41
|
+
const rows = await this.sql `
|
|
42
|
+
SELECT
|
|
43
|
+
COALESCE(bool_or(expired_at >= ${Math.floor(since)}::bigint), false) AS expired,
|
|
44
|
+
COALESCE(bool_or(stale_at >= ${Math.floor(since)}::bigint), false) AS stale
|
|
45
|
+
FROM page_cache_tags
|
|
46
|
+
WHERE tag = ANY(${this.sql.array(uniquePageCacheTags(tags))}::text[])`;
|
|
47
|
+
const row = rows[0];
|
|
48
|
+
if (row?.expired)
|
|
49
|
+
return 'expired';
|
|
50
|
+
return row?.stale ? 'stale' : 'fresh';
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* One statement: atomic, and each tag's timestamps only ever move forward
|
|
54
|
+
* (`GREATEST` ignores NULL, so a stale mark keeps an earlier expiry). Tags are
|
|
55
|
+
* upserted in sorted order so concurrent marks lock rows in one order and
|
|
56
|
+
* cannot deadlock.
|
|
57
|
+
*/
|
|
58
|
+
async markTags(tags, options) {
|
|
59
|
+
if (tags.length === 0)
|
|
60
|
+
return;
|
|
61
|
+
await this.sql `
|
|
62
|
+
WITH clock AS (SELECT floor(extract(epoch FROM clock_timestamp()) * 1000)::bigint AS ms)
|
|
63
|
+
INSERT INTO page_cache_tags (tag, stale_at, expired_at)
|
|
64
|
+
SELECT t, clock.ms, CASE WHEN ${options.expire}::boolean THEN clock.ms END
|
|
65
|
+
FROM unnest(${this.sql.array(uniquePageCacheTags(tags).sort())}::text[]) AS t, clock
|
|
66
|
+
ORDER BY t
|
|
67
|
+
ON CONFLICT (tag) DO UPDATE SET
|
|
68
|
+
stale_at = GREATEST(page_cache_tags.stale_at, EXCLUDED.stale_at),
|
|
69
|
+
expired_at = GREATEST(page_cache_tags.expired_at, EXCLUDED.expired_at)`;
|
|
70
|
+
}
|
|
71
|
+
async countTagged(tags) {
|
|
72
|
+
if (tags.length === 0)
|
|
73
|
+
return 0;
|
|
74
|
+
const rows = await this.sql `
|
|
75
|
+
SELECT COUNT(*) AS count FROM page_cache_entries
|
|
76
|
+
WHERE tags && ${this.sql.array(uniquePageCacheTags(tags))}::text[]`;
|
|
77
|
+
return Number(rows[0]?.count ?? 0);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=postgres-page-cache-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postgres-page-cache-store.js","sourceRoot":"","sources":["../src/postgres-page-cache-store.ts"],"names":[],"mappings":"AAOA,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,mBAAmB,GACpB,MAAM,sBAAsB,CAAA;AAc7B;;;;GAIG;AACH,MAAM,OAAO,sBAAsB;IACJ;IAA7B,YAA6B,GAAQ;QAAR,QAAG,GAAH,GAAG,CAAK;IAAG,CAAC;IAEzC,KAAK,CAAC,GAAG,CAAC,GAAW;QACnB,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,GAAG,CAAY;8EACmC,GAAG,EAAE,CAAA;QAC/E,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,CAAA;QACnB,IAAI,CAAC,GAAG;YAAE,OAAO,IAAI,CAAA;QACrB,OAAO;YACL,KAAK,EAAE,oBAAoB,CAAC,GAAG,CAAC,KAAK,CAAC;YACtC,IAAI,EAAE,GAAG,CAAC,IAAI;YACd,YAAY,EAAE,MAAM,CAAC,GAAG,CAAC,aAAa,CAAC;SACxC,CAAA;IACH,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,GAAW,EAAE,KAAqB,EAAE,IAAuB;QACnE,MAAM,IAAI,CAAC,GAAG,CAAA;;;UAGR,GAAG;UACH,oBAAoB,CAAC,KAAK,CAAC;UAC3B,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,mBAAmB,CAAC,IAAI,CAAC,CAAC;;;;;;+CAMJ,CAAA;IAC7C,CAAC;IAED,KAAK,CAAC,oBAAoB,CAAC,IAAuB,EAAE,KAAa;QAC/D,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,OAAO,CAAA;QACrC,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,GAAG,CAAe;;yCAEL,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC;uCACnB,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC;;wBAEhC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,mBAAmB,CAAC,IAAI,CAAC,CAAC,WAAW,CAAA;QACxE,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,CAAA;QACnB,IAAI,GAAG,EAAE,OAAO;YAAE,OAAO,SAAS,CAAA;QAClC,OAAO,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAA;IACvC,CAAC;IAED;;;;;OAKG;IACH,KAAK,CAAC,QAAQ,CAAC,IAAuB,EAAE,OAA6B;QACnE,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;YAAE,OAAM;QAC7B,MAAM,IAAI,CAAC,GAAG,CAAA;;;sCAGoB,OAAO,CAAC,MAAM;oBAChC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,mBAAmB,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC;;;;+EAIW,CAAA;IAC7E,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,IAAuB;QACvC,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,CAAC,CAAA;QAC/B,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,GAAG,CAAqB;;sBAE9B,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,mBAAmB,CAAC,IAAI,CAAC,CAAC,UAAU,CAAA;QACrE,OAAO,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,KAAK,IAAI,CAAC,CAAC,CAAA;IACpC,CAAC;CACF"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { IRetentionAdapter, PurgeResult } from '@krantho/persistence';
|
|
2
|
+
import type { Sql } from 'postgres';
|
|
3
|
+
/** Postgres implementation of version retention/purge */
|
|
4
|
+
export declare class PostgresRetentionAdapter implements IRetentionAdapter {
|
|
5
|
+
private readonly sql;
|
|
6
|
+
constructor(sql: Sql);
|
|
7
|
+
purgeVersions(maxVersionsPerItem: number): Promise<PurgeResult[]>;
|
|
8
|
+
countVersions(): Promise<{
|
|
9
|
+
total: number;
|
|
10
|
+
current: number;
|
|
11
|
+
historical: number;
|
|
12
|
+
}>;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=postgres-retention-adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postgres-retention-adapter.d.ts","sourceRoot":"","sources":["../src/postgres-retention-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAA;AAC1E,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,UAAU,CAAA;AASnC,yDAAyD;AACzD,qBAAa,wBAAyB,YAAW,iBAAiB;IACpD,OAAO,CAAC,QAAQ,CAAC,GAAG;gBAAH,GAAG,EAAE,GAAG;IAE/B,aAAa,CAAC,kBAAkB,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;IAkDjE,aAAa,IAAI,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC;CAevF"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/** Postgres implementation of version retention/purge */
|
|
2
|
+
export class PostgresRetentionAdapter {
|
|
3
|
+
sql;
|
|
4
|
+
constructor(sql) {
|
|
5
|
+
this.sql = sql;
|
|
6
|
+
}
|
|
7
|
+
async purgeVersions(maxVersionsPerItem) {
|
|
8
|
+
if (maxVersionsPerItem < 1) {
|
|
9
|
+
throw new Error('maxVersionsPerItem must be at least 1');
|
|
10
|
+
}
|
|
11
|
+
// Find items with more versions than the limit
|
|
12
|
+
const groups = await this.sql `
|
|
13
|
+
SELECT collection, slug, locale, COUNT(*) as version_count
|
|
14
|
+
FROM content
|
|
15
|
+
WHERE deleted_at IS NULL
|
|
16
|
+
GROUP BY collection, slug, locale
|
|
17
|
+
HAVING COUNT(*) > ${maxVersionsPerItem}`;
|
|
18
|
+
const results = [];
|
|
19
|
+
for (const group of groups) {
|
|
20
|
+
const versionCount = parseInt(group.version_count, 10);
|
|
21
|
+
const toPurge = versionCount - maxVersionsPerItem;
|
|
22
|
+
// Delete oldest non-current versions, never the one that is live. When the
|
|
23
|
+
// live version is among the oldest, fewer rows go and the document stays
|
|
24
|
+
// over the limit until later versions age out.
|
|
25
|
+
const deleted = await this.sql.unsafe(`DELETE FROM content WHERE id IN (
|
|
26
|
+
SELECT id FROM content
|
|
27
|
+
WHERE collection = $1 AND slug = $2 AND locale = $3 AND is_current = FALSE
|
|
28
|
+
AND version IS DISTINCT FROM (
|
|
29
|
+
SELECT live_version FROM content
|
|
30
|
+
WHERE collection = $1 AND slug = $2 AND locale = $3 AND is_current = TRUE
|
|
31
|
+
)
|
|
32
|
+
ORDER BY version ASC
|
|
33
|
+
LIMIT $4
|
|
34
|
+
)`, [group.collection, group.slug, group.locale, toPurge]);
|
|
35
|
+
const changes = deleted.count;
|
|
36
|
+
if (changes > 0) {
|
|
37
|
+
results.push({
|
|
38
|
+
collection: group.collection,
|
|
39
|
+
slug: group.slug,
|
|
40
|
+
purged: changes,
|
|
41
|
+
retained: versionCount - changes,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return results;
|
|
46
|
+
}
|
|
47
|
+
async countVersions() {
|
|
48
|
+
const totalRows = await this.sql `
|
|
49
|
+
SELECT COUNT(*) as count FROM content WHERE deleted_at IS NULL`;
|
|
50
|
+
const currentRows = await this.sql `
|
|
51
|
+
SELECT COUNT(*) as count FROM content WHERE is_current = TRUE AND deleted_at IS NULL`;
|
|
52
|
+
const total = parseInt(totalRows[0]?.count ?? '0', 10);
|
|
53
|
+
const current = parseInt(currentRows[0]?.count ?? '0', 10);
|
|
54
|
+
return {
|
|
55
|
+
total,
|
|
56
|
+
current,
|
|
57
|
+
historical: total - current,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=postgres-retention-adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postgres-retention-adapter.js","sourceRoot":"","sources":["../src/postgres-retention-adapter.ts"],"names":[],"mappings":"AAUA,yDAAyD;AACzD,MAAM,OAAO,wBAAwB;IACN;IAA7B,YAA6B,GAAQ;QAAR,QAAG,GAAH,GAAG,CAAK;IAAG,CAAC;IAEzC,KAAK,CAAC,aAAa,CAAC,kBAA0B;QAC5C,IAAI,kBAAkB,GAAG,CAAC,EAAE,CAAC;YAC3B,MAAM,IAAI,KAAK,CAAC,uCAAuC,CAAC,CAAA;QAC1D,CAAC;QAED,+CAA+C;QAC/C,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,GAAG,CAAgB;;;;;0BAKvB,kBAAkB,EAAE,CAAA;QAE1C,MAAM,OAAO,GAAkB,EAAE,CAAA;QAEjC,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;YAC3B,MAAM,YAAY,GAAG,QAAQ,CAAC,KAAK,CAAC,aAAa,EAAE,EAAE,CAAC,CAAA;YACtD,MAAM,OAAO,GAAG,YAAY,GAAG,kBAAkB,CAAA;YAEjD,2EAA2E;YAC3E,yEAAyE;YACzE,+CAA+C;YAC/C,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,MAAM,CACnC;;;;;;;;;UASE,EACF,CAAC,KAAK,CAAC,UAAU,EAAE,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,MAAM,EAAE,OAAO,CAAC,CACtD,CAAA;YAED,MAAM,OAAO,GAAG,OAAO,CAAC,KAAK,CAAA;YAC7B,IAAI,OAAO,GAAG,CAAC,EAAE,CAAC;gBAChB,OAAO,CAAC,IAAI,CAAC;oBACX,UAAU,EAAE,KAAK,CAAC,UAAU;oBAC5B,IAAI,EAAE,KAAK,CAAC,IAAI;oBAChB,MAAM,EAAE,OAAO;oBACf,QAAQ,EAAE,YAAY,GAAG,OAAO;iBACjC,CAAC,CAAA;YACJ,CAAC;QACH,CAAC;QAED,OAAO,OAAO,CAAA;IAChB,CAAC;IAED,KAAK,CAAC,aAAa;QACjB,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC,GAAG,CAAqB;qEACY,CAAA;QACjE,MAAM,WAAW,GAAG,MAAM,IAAI,CAAC,GAAG,CAAqB;2FACgC,CAAA;QAEvF,MAAM,KAAK,GAAG,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,IAAI,GAAG,EAAE,EAAE,CAAC,CAAA;QACtD,MAAM,OAAO,GAAG,QAAQ,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,KAAK,IAAI,GAAG,EAAE,EAAE,CAAC,CAAA;QAE1D,OAAO;YACL,KAAK;YACL,OAAO;YACP,UAAU,EAAE,KAAK,GAAG,OAAO;SAC5B,CAAA;IACH,CAAC;CACF"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ISeoConfigAdapter, SeoConfig, SeoConfigScope } from '@krantho/persistence';
|
|
2
|
+
import type { Sql } from 'postgres';
|
|
3
|
+
/** Postgres implementation of ISeoConfigAdapter */
|
|
4
|
+
export declare class PostgresSeoConfigAdapter implements ISeoConfigAdapter {
|
|
5
|
+
private readonly sql;
|
|
6
|
+
constructor(sql: Sql);
|
|
7
|
+
getConfig(id: string): Promise<SeoConfig | null>;
|
|
8
|
+
saveConfig(config: Omit<SeoConfig, 'createdAt' | 'updatedAt'>): Promise<void>;
|
|
9
|
+
deleteConfig(id: string): Promise<void>;
|
|
10
|
+
listConfigs(scope?: SeoConfigScope): Promise<SeoConfig[]>;
|
|
11
|
+
listConfigsByPrefix(prefix: string): Promise<SeoConfig[]>;
|
|
12
|
+
deleteAllConfigs(): Promise<number>;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=postgres-seo-config-adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postgres-seo-config-adapter.d.ts","sourceRoot":"","sources":["../src/postgres-seo-config-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAGxF,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,UAAU,CAAA;AAsBnC,mDAAmD;AACnD,qBAAa,wBAAyB,YAAW,iBAAiB;IACpD,OAAO,CAAC,QAAQ,CAAC,GAAG;gBAAH,GAAG,EAAE,GAAG;IAE/B,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC;IAOhD,UAAU,CAAC,MAAM,EAAE,IAAI,CAAC,SAAS,EAAE,WAAW,GAAG,WAAW,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAiB7E,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIvC,WAAW,CAAC,KAAK,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;IAUzD,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;IAQzD,gBAAgB,IAAI,OAAO,CAAC,MAAM,CAAC;CAI1C"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { escapeSqlLikePattern } from '@krantho/persistence';
|
|
2
|
+
function toSeoConfig(row) {
|
|
3
|
+
return {
|
|
4
|
+
id: row.id,
|
|
5
|
+
scope: row.scope,
|
|
6
|
+
config: row.config,
|
|
7
|
+
createdAt: row.created_at instanceof Date ? row.created_at.toISOString() : String(row.created_at),
|
|
8
|
+
updatedAt: row.updated_at instanceof Date ? row.updated_at.toISOString() : String(row.updated_at),
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
/** Postgres implementation of ISeoConfigAdapter */
|
|
12
|
+
export class PostgresSeoConfigAdapter {
|
|
13
|
+
sql;
|
|
14
|
+
constructor(sql) {
|
|
15
|
+
this.sql = sql;
|
|
16
|
+
}
|
|
17
|
+
async getConfig(id) {
|
|
18
|
+
const rows = await this.sql `
|
|
19
|
+
SELECT * FROM seo_config WHERE id = ${id}`;
|
|
20
|
+
const row = rows[0];
|
|
21
|
+
return row ? toSeoConfig(row) : null;
|
|
22
|
+
}
|
|
23
|
+
async saveConfig(config) {
|
|
24
|
+
if (config.scope === 'document' || config.id.startsWith('doc:')) {
|
|
25
|
+
throw new Error('Document SEO belongs in versioned document metadata');
|
|
26
|
+
}
|
|
27
|
+
// Bind the object with sql.json: JSON.stringify plus a ::jsonb cast is
|
|
28
|
+
// encoded again by the driver and stores the JSON text as a string
|
|
29
|
+
await this.sql.unsafe(`INSERT INTO seo_config (id, scope, config, updated_at)
|
|
30
|
+
VALUES ($1, $2, $3, NOW())
|
|
31
|
+
ON CONFLICT (id) DO UPDATE SET
|
|
32
|
+
scope = EXCLUDED.scope,
|
|
33
|
+
config = EXCLUDED.config,
|
|
34
|
+
updated_at = NOW()`, [config.id, config.scope, this.sql.json(config.config)]);
|
|
35
|
+
}
|
|
36
|
+
async deleteConfig(id) {
|
|
37
|
+
await this.sql `DELETE FROM seo_config WHERE id = ${id}`;
|
|
38
|
+
}
|
|
39
|
+
async listConfigs(scope) {
|
|
40
|
+
const rows = scope
|
|
41
|
+
? await this.sql `
|
|
42
|
+
SELECT * FROM seo_config WHERE scope = ${scope} ORDER BY id`
|
|
43
|
+
: await this.sql `
|
|
44
|
+
SELECT * FROM seo_config ORDER BY id`;
|
|
45
|
+
return rows.map(toSeoConfig);
|
|
46
|
+
}
|
|
47
|
+
async listConfigsByPrefix(prefix) {
|
|
48
|
+
const escaped = escapeSqlLikePattern(prefix);
|
|
49
|
+
const pattern = `${escaped}%`;
|
|
50
|
+
const rows = await this.sql `
|
|
51
|
+
SELECT * FROM seo_config WHERE id LIKE ${pattern} ESCAPE '\\' ORDER BY id`;
|
|
52
|
+
return rows.map(toSeoConfig);
|
|
53
|
+
}
|
|
54
|
+
async deleteAllConfigs() {
|
|
55
|
+
const result = await this.sql `DELETE FROM seo_config`;
|
|
56
|
+
return result.count;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=postgres-seo-config-adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postgres-seo-config-adapter.js","sourceRoot":"","sources":["../src/postgres-seo-config-adapter.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAA;AAY3D,SAAS,WAAW,CAAC,GAAiB;IACpC,OAAO;QACL,EAAE,EAAE,GAAG,CAAC,EAAE;QACV,KAAK,EAAE,GAAG,CAAC,KAAuB;QAClC,MAAM,EAAE,GAAG,CAAC,MAAM;QAClB,SAAS,EACP,GAAG,CAAC,UAAU,YAAY,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC;QACxF,SAAS,EACP,GAAG,CAAC,UAAU,YAAY,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC;KACzF,CAAA;AACH,CAAC;AAED,mDAAmD;AACnD,MAAM,OAAO,wBAAwB;IACN;IAA7B,YAA6B,GAAQ;QAAR,QAAG,GAAH,GAAG,CAAK;IAAG,CAAC;IAEzC,KAAK,CAAC,SAAS,CAAC,EAAU;QACxB,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,GAAG,CAAgB;4CACH,EAAE,EAAE,CAAA;QAC5C,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,CAAA;QACnB,OAAO,GAAG,CAAC,CAAC,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;IACtC,CAAC;IAED,KAAK,CAAC,UAAU,CAAC,MAAkD;QACjE,IAAI,MAAM,CAAC,KAAK,KAAK,UAAU,IAAI,MAAM,CAAC,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;YAChE,MAAM,IAAI,KAAK,CAAC,qDAAqD,CAAC,CAAA;QACxE,CAAC;QACD,uEAAuE;QACvE,mEAAmE;QACnE,MAAM,IAAI,CAAC,GAAG,CAAC,MAAM,CACnB;;;;;4BAKsB,EACtB,CAAC,MAAM,CAAC,EAAE,EAAE,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,MAAe,CAAC,CAAC,CACjE,CAAA;IACH,CAAC;IAED,KAAK,CAAC,YAAY,CAAC,EAAU;QAC3B,MAAM,IAAI,CAAC,GAAG,CAAA,qCAAqC,EAAE,EAAE,CAAA;IACzD,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,KAAsB;QACtC,MAAM,IAAI,GAAG,KAAK;YAChB,CAAC,CAAC,MAAM,IAAI,CAAC,GAAG,CAAgB;mDACa,KAAK,cAAc;YAChE,CAAC,CAAC,MAAM,IAAI,CAAC,GAAG,CAAgB;+CACS,CAAA;QAE3C,OAAO,IAAI,CAAC,GAAG,CAAC,WAAW,CAAC,CAAA;IAC9B,CAAC;IAED,KAAK,CAAC,mBAAmB,CAAC,MAAc;QACtC,MAAM,OAAO,GAAG,oBAAoB,CAAC,MAAM,CAAC,CAAA;QAC5C,MAAM,OAAO,GAAG,GAAG,OAAO,GAAG,CAAA;QAC7B,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,GAAG,CAAgB;+CACA,OAAO,0BAA0B,CAAA;QAC5E,OAAO,IAAI,CAAC,GAAG,CAAC,WAAW,CAAC,CAAA;IAC9B,CAAC;IAED,KAAK,CAAC,gBAAgB;QACpB,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,GAAG,CAAA,wBAAwB,CAAA;QACrD,OAAO,MAAM,CAAC,KAAK,CAAA;IACrB,CAAC;CACF"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ISiteSettingsAdapter } from '@krantho/persistence';
|
|
2
|
+
import type { Sql } from 'postgres';
|
|
3
|
+
/** Postgres implementation of runtime site settings */
|
|
4
|
+
export declare class PostgresSiteSettingsAdapter implements ISiteSettingsAdapter {
|
|
5
|
+
private readonly sql;
|
|
6
|
+
constructor(sql: Sql);
|
|
7
|
+
getSetting(key: string): Promise<string | null>;
|
|
8
|
+
setSetting(key: string, value: string): Promise<void>;
|
|
9
|
+
getAllSettings(): Promise<Record<string, string>>;
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=postgres-site-settings-adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postgres-site-settings-adapter.d.ts","sourceRoot":"","sources":["../src/postgres-site-settings-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAA;AAChE,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,UAAU,CAAA;AAEnC,uDAAuD;AACvD,qBAAa,2BAA4B,YAAW,oBAAoB;IAC1D,OAAO,CAAC,QAAQ,CAAC,GAAG;gBAAH,GAAG,EAAE,GAAG;IAE/B,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAM/C,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IASrD,cAAc,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CASxD"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** Postgres implementation of runtime site settings */
|
|
2
|
+
export class PostgresSiteSettingsAdapter {
|
|
3
|
+
sql;
|
|
4
|
+
constructor(sql) {
|
|
5
|
+
this.sql = sql;
|
|
6
|
+
}
|
|
7
|
+
async getSetting(key) {
|
|
8
|
+
const rows = await this.sql `
|
|
9
|
+
SELECT value FROM site_settings WHERE key = ${key}`;
|
|
10
|
+
return rows[0]?.value ?? null;
|
|
11
|
+
}
|
|
12
|
+
async setSetting(key, value) {
|
|
13
|
+
await this.sql `
|
|
14
|
+
INSERT INTO site_settings (key, value, updated_at)
|
|
15
|
+
VALUES (${key}, ${value}, NOW())
|
|
16
|
+
ON CONFLICT (key) DO UPDATE SET
|
|
17
|
+
value = EXCLUDED.value,
|
|
18
|
+
updated_at = NOW()`;
|
|
19
|
+
}
|
|
20
|
+
async getAllSettings() {
|
|
21
|
+
const rows = await this.sql `
|
|
22
|
+
SELECT key, value FROM site_settings`;
|
|
23
|
+
const result = {};
|
|
24
|
+
for (const row of rows) {
|
|
25
|
+
result[row.key] = row.value;
|
|
26
|
+
}
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=postgres-site-settings-adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postgres-site-settings-adapter.js","sourceRoot":"","sources":["../src/postgres-site-settings-adapter.ts"],"names":[],"mappings":"AAGA,uDAAuD;AACvD,MAAM,OAAO,2BAA2B;IACT;IAA7B,YAA6B,GAAQ;QAAR,QAAG,GAAH,GAAG,CAAK;IAAG,CAAC;IAEzC,KAAK,CAAC,UAAU,CAAC,GAAW;QAC1B,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,GAAG,CAAqB;oDACA,GAAG,EAAE,CAAA;QACrD,OAAO,IAAI,CAAC,CAAC,CAAC,EAAE,KAAK,IAAI,IAAI,CAAA;IAC/B,CAAC;IAED,KAAK,CAAC,UAAU,CAAC,GAAW,EAAE,KAAa;QACzC,MAAM,IAAI,CAAC,GAAG,CAAA;;gBAEF,GAAG,KAAK,KAAK;;;2BAGF,CAAA;IACzB,CAAC;IAED,KAAK,CAAC,cAAc;QAClB,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,GAAG,CAAkC;2CACtB,CAAA;QACvC,MAAM,MAAM,GAA2B,EAAE,CAAA;QACzC,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;YACvB,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC,KAAK,CAAA;QAC7B,CAAC;QACD,OAAO,MAAM,CAAA;IACf,CAAC;CACF"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { IWebhookDeliveryAdapter, NewWebhookDelivery, StoredWebhookDelivery, WebhookDeliveryRecord } from '@krantho/persistence';
|
|
2
|
+
import type { Sql } from 'postgres';
|
|
3
|
+
/** Postgres implementation of webhook delivery log */
|
|
4
|
+
export declare class PostgresWebhookDeliveryAdapter implements IWebhookDeliveryAdapter {
|
|
5
|
+
private readonly sql;
|
|
6
|
+
constructor(sql: Sql);
|
|
7
|
+
listDeliveries(options: {
|
|
8
|
+
limit: number;
|
|
9
|
+
offset: number;
|
|
10
|
+
status?: 'pending' | 'delivered' | 'failed' | undefined;
|
|
11
|
+
}): Promise<{
|
|
12
|
+
deliveries: WebhookDeliveryRecord[];
|
|
13
|
+
total: number;
|
|
14
|
+
}>;
|
|
15
|
+
getDelivery(id: number): Promise<StoredWebhookDelivery | null>;
|
|
16
|
+
requeueDelivery(id: number, dueAt: Date): Promise<WebhookDeliveryRecord | null>;
|
|
17
|
+
logDelivery(delivery: NewWebhookDelivery): Promise<number>;
|
|
18
|
+
markDelivered(id: number, status: number): Promise<void>;
|
|
19
|
+
markFailed(id: number, status: number, nextRetryAt: Date): Promise<void>;
|
|
20
|
+
claimDueDeliveries(options: {
|
|
21
|
+
urls: readonly string[];
|
|
22
|
+
now: Date;
|
|
23
|
+
leaseUntil: Date;
|
|
24
|
+
limit: number;
|
|
25
|
+
}): Promise<StoredWebhookDelivery[]>;
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=postgres-webhook-delivery-adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postgres-webhook-delivery-adapter.d.ts","sourceRoot":"","sources":["../src/postgres-webhook-delivery-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,uBAAuB,EACvB,kBAAkB,EAClB,qBAAqB,EACrB,qBAAqB,EACtB,MAAM,sBAAsB,CAAA;AAE7B,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,UAAU,CAAA;AAsCnC,sDAAsD;AACtD,qBAAa,8BAA+B,YAAW,uBAAuB;IAChE,OAAO,CAAC,QAAQ,CAAC,GAAG;gBAAH,GAAG,EAAE,GAAG;IAE/B,cAAc,CAAC,OAAO,EAAE;QAC5B,KAAK,EAAE,MAAM,CAAA;QACb,MAAM,EAAE,MAAM,CAAA;QACd,MAAM,CAAC,EAAE,SAAS,GAAG,WAAW,GAAG,QAAQ,GAAG,SAAS,CAAA;KACxD,GAAG,OAAO,CAAC;QAAE,UAAU,EAAE,qBAAqB,EAAE,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IAwB7D,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,GAAG,IAAI,CAAC;IAQ9D,eAAe,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,GAAG,OAAO,CAAC,qBAAqB,GAAG,IAAI,CAAC;IAU/E,WAAW,CAAC,QAAQ,EAAE,kBAAkB,GAAG,OAAO,CAAC,MAAM,CAAC;IAY1D,aAAa,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAKxD,UAAU,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAQxE,kBAAkB,CAAC,OAAO,EAAE;QAChC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;QACvB,GAAG,EAAE,IAAI,CAAA;QACT,UAAU,EAAE,IAAI,CAAA;QAChB,KAAK,EAAE,MAAM,CAAA;KACd,GAAG,OAAO,CAAC,qBAAqB,EAAE,CAAC;CAkBrC"}
|