@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.
Files changed (127) hide show
  1. package/LICENSE +21 -0
  2. package/dist/auth-adapter.d.ts +23 -0
  3. package/dist/auth-adapter.d.ts.map +1 -0
  4. package/dist/auth-adapter.js +159 -0
  5. package/dist/auth-adapter.js.map +1 -0
  6. package/dist/content-migration.d.ts +14 -0
  7. package/dist/content-migration.d.ts.map +1 -0
  8. package/dist/content-migration.js +64 -0
  9. package/dist/content-migration.js.map +1 -0
  10. package/dist/content-query.d.ts +23 -0
  11. package/dist/content-query.d.ts.map +1 -0
  12. package/dist/content-query.js +170 -0
  13. package/dist/content-query.js.map +1 -0
  14. package/dist/index.d.ts +17 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +15 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/migration-runner.d.ts +29 -0
  19. package/dist/migration-runner.d.ts.map +1 -0
  20. package/dist/migration-runner.js +123 -0
  21. package/dist/migration-runner.js.map +1 -0
  22. package/dist/migrations/index.d.ts +4 -0
  23. package/dist/migrations/index.d.ts.map +1 -0
  24. package/dist/migrations/index.js +32 -0
  25. package/dist/migrations/index.js.map +1 -0
  26. package/dist/migrations/v001-initial-schema.d.ts +10 -0
  27. package/dist/migrations/v001-initial-schema.d.ts.map +1 -0
  28. package/dist/migrations/v001-initial-schema.js +94 -0
  29. package/dist/migrations/v001-initial-schema.js.map +1 -0
  30. package/dist/migrations/v002-auth-tables.d.ts +8 -0
  31. package/dist/migrations/v002-auth-tables.d.ts.map +1 -0
  32. package/dist/migrations/v002-auth-tables.js +37 -0
  33. package/dist/migrations/v002-auth-tables.js.map +1 -0
  34. package/dist/migrations/v003-user-permissions.d.ts +6 -0
  35. package/dist/migrations/v003-user-permissions.d.ts.map +1 -0
  36. package/dist/migrations/v003-user-permissions.js +18 -0
  37. package/dist/migrations/v003-user-permissions.js.map +1 -0
  38. package/dist/migrations/v004-media-hotspot.d.ts +14 -0
  39. package/dist/migrations/v004-media-hotspot.d.ts.map +1 -0
  40. package/dist/migrations/v004-media-hotspot.js +25 -0
  41. package/dist/migrations/v004-media-hotspot.js.map +1 -0
  42. package/dist/migrations/v005-content-locks.d.ts +6 -0
  43. package/dist/migrations/v005-content-locks.d.ts.map +1 -0
  44. package/dist/migrations/v005-content-locks.js +21 -0
  45. package/dist/migrations/v005-content-locks.js.map +1 -0
  46. package/dist/migrations/v006-webhook-deliveries.d.ts +6 -0
  47. package/dist/migrations/v006-webhook-deliveries.d.ts.map +1 -0
  48. package/dist/migrations/v006-webhook-deliveries.js +24 -0
  49. package/dist/migrations/v006-webhook-deliveries.js.map +1 -0
  50. package/dist/migrations/v007-media-folders.d.ts +6 -0
  51. package/dist/migrations/v007-media-folders.d.ts.map +1 -0
  52. package/dist/migrations/v007-media-folders.js +19 -0
  53. package/dist/migrations/v007-media-folders.js.map +1 -0
  54. package/dist/migrations/v008-site-settings.d.ts +6 -0
  55. package/dist/migrations/v008-site-settings.d.ts.map +1 -0
  56. package/dist/migrations/v008-site-settings.js +17 -0
  57. package/dist/migrations/v008-site-settings.js.map +1 -0
  58. package/dist/migrations/v009-seo-config.d.ts +12 -0
  59. package/dist/migrations/v009-seo-config.d.ts.map +1 -0
  60. package/dist/migrations/v009-seo-config.js +27 -0
  61. package/dist/migrations/v009-seo-config.js.map +1 -0
  62. package/dist/migrations/v010-content-live-version.d.ts +15 -0
  63. package/dist/migrations/v010-content-live-version.d.ts.map +1 -0
  64. package/dist/migrations/v010-content-live-version.js +40 -0
  65. package/dist/migrations/v010-content-live-version.js.map +1 -0
  66. package/dist/migrations/v011-media.d.ts +12 -0
  67. package/dist/migrations/v011-media.d.ts.map +1 -0
  68. package/dist/migrations/v011-media.js +43 -0
  69. package/dist/migrations/v011-media.js.map +1 -0
  70. package/dist/migrations/v012-page-cache.d.ts +12 -0
  71. package/dist/migrations/v012-page-cache.d.ts.map +1 -0
  72. package/dist/migrations/v012-page-cache.js +31 -0
  73. package/dist/migrations/v012-page-cache.js.map +1 -0
  74. package/dist/migrations/v013-content-contracts.d.ts +17 -0
  75. package/dist/migrations/v013-content-contracts.d.ts.map +1 -0
  76. package/dist/migrations/v013-content-contracts.js +41 -0
  77. package/dist/migrations/v013-content-contracts.js.map +1 -0
  78. package/dist/migrations/v014-content-manifest-commit.d.ts +7 -0
  79. package/dist/migrations/v014-content-manifest-commit.d.ts.map +1 -0
  80. package/dist/migrations/v014-content-manifest-commit.js +12 -0
  81. package/dist/migrations/v014-content-manifest-commit.js.map +1 -0
  82. package/dist/postgres-adapter.d.ts +71 -0
  83. package/dist/postgres-adapter.d.ts.map +1 -0
  84. package/dist/postgres-adapter.js +327 -0
  85. package/dist/postgres-adapter.js.map +1 -0
  86. package/dist/postgres-checkpoints.d.ts +20 -0
  87. package/dist/postgres-checkpoints.d.ts.map +1 -0
  88. package/dist/postgres-checkpoints.js +109 -0
  89. package/dist/postgres-checkpoints.js.map +1 -0
  90. package/dist/postgres-lock-adapter.d.ts +12 -0
  91. package/dist/postgres-lock-adapter.d.ts.map +1 -0
  92. package/dist/postgres-lock-adapter.js +62 -0
  93. package/dist/postgres-lock-adapter.js.map +1 -0
  94. package/dist/postgres-media-adapter.d.ts +28 -0
  95. package/dist/postgres-media-adapter.d.ts.map +1 -0
  96. package/dist/postgres-media-adapter.js +167 -0
  97. package/dist/postgres-media-adapter.js.map +1 -0
  98. package/dist/postgres-media-folder-adapter.d.ts +12 -0
  99. package/dist/postgres-media-folder-adapter.d.ts.map +1 -0
  100. package/dist/postgres-media-folder-adapter.js +60 -0
  101. package/dist/postgres-media-folder-adapter.js.map +1 -0
  102. package/dist/postgres-page-cache-store.d.ts +23 -0
  103. package/dist/postgres-page-cache-store.d.ts.map +1 -0
  104. package/dist/postgres-page-cache-store.js +80 -0
  105. package/dist/postgres-page-cache-store.js.map +1 -0
  106. package/dist/postgres-retention-adapter.d.ts +14 -0
  107. package/dist/postgres-retention-adapter.d.ts.map +1 -0
  108. package/dist/postgres-retention-adapter.js +61 -0
  109. package/dist/postgres-retention-adapter.js.map +1 -0
  110. package/dist/postgres-seo-config-adapter.d.ts +14 -0
  111. package/dist/postgres-seo-config-adapter.d.ts.map +1 -0
  112. package/dist/postgres-seo-config-adapter.js +59 -0
  113. package/dist/postgres-seo-config-adapter.js.map +1 -0
  114. package/dist/postgres-site-settings-adapter.d.ts +11 -0
  115. package/dist/postgres-site-settings-adapter.d.ts.map +1 -0
  116. package/dist/postgres-site-settings-adapter.js +30 -0
  117. package/dist/postgres-site-settings-adapter.js.map +1 -0
  118. package/dist/postgres-webhook-delivery-adapter.d.ts +27 -0
  119. package/dist/postgres-webhook-delivery-adapter.d.ts.map +1 -0
  120. package/dist/postgres-webhook-delivery-adapter.js +99 -0
  121. package/dist/postgres-webhook-delivery-adapter.js.map +1 -0
  122. package/dist/row-mapping.d.ts +40 -0
  123. package/dist/row-mapping.d.ts.map +1 -0
  124. package/dist/row-mapping.js +49 -0
  125. package/dist/row-mapping.js.map +1 -0
  126. package/package.json +38 -3
  127. 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"}