@krantho/persistence-sqlite 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 (135) 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/checkpoints.d.ts +20 -0
  7. package/dist/checkpoints.d.ts.map +1 -0
  8. package/dist/checkpoints.js +119 -0
  9. package/dist/checkpoints.js.map +1 -0
  10. package/dist/connection.d.ts +29 -0
  11. package/dist/connection.d.ts.map +1 -0
  12. package/dist/connection.js +77 -0
  13. package/dist/connection.js.map +1 -0
  14. package/dist/content-migration.d.ts +18 -0
  15. package/dist/content-migration.d.ts.map +1 -0
  16. package/dist/content-migration.js +68 -0
  17. package/dist/content-migration.js.map +1 -0
  18. package/dist/content-query.d.ts +16 -0
  19. package/dist/content-query.d.ts.map +1 -0
  20. package/dist/content-query.js +155 -0
  21. package/dist/content-query.js.map +1 -0
  22. package/dist/index.d.ts +20 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +16 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/lock-adapter.d.ts +11 -0
  27. package/dist/lock-adapter.d.ts.map +1 -0
  28. package/dist/lock-adapter.js +63 -0
  29. package/dist/lock-adapter.js.map +1 -0
  30. package/dist/media-adapter.d.ts +27 -0
  31. package/dist/media-adapter.d.ts.map +1 -0
  32. package/dist/media-adapter.js +175 -0
  33. package/dist/media-adapter.js.map +1 -0
  34. package/dist/media-folder-adapter.d.ts +13 -0
  35. package/dist/media-folder-adapter.d.ts.map +1 -0
  36. package/dist/media-folder-adapter.js +58 -0
  37. package/dist/media-folder-adapter.js.map +1 -0
  38. package/dist/migration-runner.d.ts +28 -0
  39. package/dist/migration-runner.d.ts.map +1 -0
  40. package/dist/migration-runner.js +109 -0
  41. package/dist/migration-runner.js.map +1 -0
  42. package/dist/migrations/index.d.ts +4 -0
  43. package/dist/migrations/index.d.ts.map +1 -0
  44. package/dist/migrations/index.js +32 -0
  45. package/dist/migrations/index.js.map +1 -0
  46. package/dist/migrations/v001-initial-schema.d.ts +10 -0
  47. package/dist/migrations/v001-initial-schema.d.ts.map +1 -0
  48. package/dist/migrations/v001-initial-schema.js +94 -0
  49. package/dist/migrations/v001-initial-schema.js.map +1 -0
  50. package/dist/migrations/v002-auth-tables.d.ts +9 -0
  51. package/dist/migrations/v002-auth-tables.d.ts.map +1 -0
  52. package/dist/migrations/v002-auth-tables.js +38 -0
  53. package/dist/migrations/v002-auth-tables.js.map +1 -0
  54. package/dist/migrations/v003-user-permissions.d.ts +9 -0
  55. package/dist/migrations/v003-user-permissions.d.ts.map +1 -0
  56. package/dist/migrations/v003-user-permissions.js +21 -0
  57. package/dist/migrations/v003-user-permissions.js.map +1 -0
  58. package/dist/migrations/v004-media-hotspot.d.ts +14 -0
  59. package/dist/migrations/v004-media-hotspot.d.ts.map +1 -0
  60. package/dist/migrations/v004-media-hotspot.js +24 -0
  61. package/dist/migrations/v004-media-hotspot.js.map +1 -0
  62. package/dist/migrations/v005-content-locks.d.ts +9 -0
  63. package/dist/migrations/v005-content-locks.d.ts.map +1 -0
  64. package/dist/migrations/v005-content-locks.js +24 -0
  65. package/dist/migrations/v005-content-locks.js.map +1 -0
  66. package/dist/migrations/v006-webhook-deliveries.d.ts +6 -0
  67. package/dist/migrations/v006-webhook-deliveries.d.ts.map +1 -0
  68. package/dist/migrations/v006-webhook-deliveries.js +24 -0
  69. package/dist/migrations/v006-webhook-deliveries.js.map +1 -0
  70. package/dist/migrations/v007-media-folders.d.ts +10 -0
  71. package/dist/migrations/v007-media-folders.d.ts.map +1 -0
  72. package/dist/migrations/v007-media-folders.js +23 -0
  73. package/dist/migrations/v007-media-folders.js.map +1 -0
  74. package/dist/migrations/v008-site-settings.d.ts +9 -0
  75. package/dist/migrations/v008-site-settings.d.ts.map +1 -0
  76. package/dist/migrations/v008-site-settings.js +20 -0
  77. package/dist/migrations/v008-site-settings.js.map +1 -0
  78. package/dist/migrations/v009-seo-config.d.ts +15 -0
  79. package/dist/migrations/v009-seo-config.d.ts.map +1 -0
  80. package/dist/migrations/v009-seo-config.js +29 -0
  81. package/dist/migrations/v009-seo-config.js.map +1 -0
  82. package/dist/migrations/v010-content-live-version.d.ts +15 -0
  83. package/dist/migrations/v010-content-live-version.d.ts.map +1 -0
  84. package/dist/migrations/v010-content-live-version.js +40 -0
  85. package/dist/migrations/v010-content-live-version.js.map +1 -0
  86. package/dist/migrations/v011-media.d.ts +12 -0
  87. package/dist/migrations/v011-media.d.ts.map +1 -0
  88. package/dist/migrations/v011-media.js +43 -0
  89. package/dist/migrations/v011-media.js.map +1 -0
  90. package/dist/migrations/v012-page-cache.d.ts +12 -0
  91. package/dist/migrations/v012-page-cache.d.ts.map +1 -0
  92. package/dist/migrations/v012-page-cache.js +31 -0
  93. package/dist/migrations/v012-page-cache.js.map +1 -0
  94. package/dist/migrations/v013-content-contracts.d.ts +17 -0
  95. package/dist/migrations/v013-content-contracts.d.ts.map +1 -0
  96. package/dist/migrations/v013-content-contracts.js +46 -0
  97. package/dist/migrations/v013-content-contracts.js.map +1 -0
  98. package/dist/migrations/v014-content-manifest-commit.d.ts +7 -0
  99. package/dist/migrations/v014-content-manifest-commit.d.ts.map +1 -0
  100. package/dist/migrations/v014-content-manifest-commit.js +12 -0
  101. package/dist/migrations/v014-content-manifest-commit.js.map +1 -0
  102. package/dist/page-cache-store.d.ts +17 -0
  103. package/dist/page-cache-store.d.ts.map +1 -0
  104. package/dist/page-cache-store.js +76 -0
  105. package/dist/page-cache-store.js.map +1 -0
  106. package/dist/queries.d.ts +82 -0
  107. package/dist/queries.d.ts.map +1 -0
  108. package/dist/queries.js +25 -0
  109. package/dist/queries.js.map +1 -0
  110. package/dist/retention-adapter.d.ts +13 -0
  111. package/dist/retention-adapter.d.ts.map +1 -0
  112. package/dist/retention-adapter.js +62 -0
  113. package/dist/retention-adapter.js.map +1 -0
  114. package/dist/row-mapping.d.ts +14 -0
  115. package/dist/row-mapping.d.ts.map +1 -0
  116. package/dist/row-mapping.js +71 -0
  117. package/dist/row-mapping.js.map +1 -0
  118. package/dist/seo-config-adapter.d.ts +14 -0
  119. package/dist/seo-config-adapter.d.ts.map +1 -0
  120. package/dist/seo-config-adapter.js +57 -0
  121. package/dist/seo-config-adapter.js.map +1 -0
  122. package/dist/site-settings-adapter.d.ts +10 -0
  123. package/dist/site-settings-adapter.d.ts.map +1 -0
  124. package/dist/site-settings-adapter.js +27 -0
  125. package/dist/site-settings-adapter.js.map +1 -0
  126. package/dist/sqlite-adapter.d.ts +69 -0
  127. package/dist/sqlite-adapter.d.ts.map +1 -0
  128. package/dist/sqlite-adapter.js +242 -0
  129. package/dist/sqlite-adapter.js.map +1 -0
  130. package/dist/webhook-delivery-adapter.d.ts +27 -0
  131. package/dist/webhook-delivery-adapter.d.ts.map +1 -0
  132. package/dist/webhook-delivery-adapter.js +93 -0
  133. package/dist/webhook-delivery-adapter.js.map +1 -0
  134. package/package.json +37 -3
  135. package/README.md +0 -3
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Descoped
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,23 @@
1
+ import type { CreateUserData, IAuthAdapter, RefreshTokenRecord, Role, UpdateUserData, UserPermission, UserRecord } from '@krantho/persistence';
2
+ import type Database from 'better-sqlite3';
3
+ /** SQLite implementation of the auth adapter */
4
+ export declare class SqliteAuthAdapter implements IAuthAdapter {
5
+ private readonly db;
6
+ constructor(db: Database.Database);
7
+ createUser(data: CreateUserData): Promise<UserRecord>;
8
+ getUser(id: string): Promise<UserRecord | null>;
9
+ getUserByEmail(email: string): Promise<UserRecord | null>;
10
+ getUserByOidc(issuer: string, subject: string): Promise<UserRecord | null>;
11
+ listUsers(): Promise<UserRecord[]>;
12
+ updateUser(id: string, data: UpdateUserData): Promise<UserRecord | null>;
13
+ deleteUser(id: string): Promise<boolean>;
14
+ createRefreshToken(userId: string, tokenHash: string, expiresAt: string): Promise<string>;
15
+ getRefreshTokenByHash(tokenHash: string): Promise<RefreshTokenRecord | null>;
16
+ deleteRefreshToken(id: string): Promise<void>;
17
+ deleteUserRefreshTokens(userId: string): Promise<void>;
18
+ deleteExpiredRefreshTokens(): Promise<number>;
19
+ getUserPermissions(userId: string): Promise<UserPermission[]>;
20
+ setUserPermission(userId: string, collection: string, role: Role): Promise<void>;
21
+ deleteUserPermission(userId: string, collection: string): Promise<boolean>;
22
+ }
23
+ //# sourceMappingURL=auth-adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth-adapter.d.ts","sourceRoot":"","sources":["../src/auth-adapter.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,cAAc,EACd,YAAY,EACZ,kBAAkB,EAClB,IAAI,EACJ,cAAc,EACd,cAAc,EACd,UAAU,EACX,MAAM,sBAAsB,CAAA;AAC7B,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAA;AAE1C,gDAAgD;AAChD,qBAAa,iBAAkB,YAAW,YAAY;IACpD,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAmB;gBAE1B,EAAE,EAAE,QAAQ,CAAC,QAAQ;IAM3B,UAAU,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,UAAU,CAAC;IAkCrD,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IAO/C,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IAOzD,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IAO1E,SAAS,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC;IAOlC,UAAU,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IAyCxE,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAOxC,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAUzF,qBAAqB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC;IAe5E,kBAAkB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI7C,uBAAuB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAItD,0BAA0B,IAAI,OAAO,CAAC,MAAM,CAAC;IAS7C,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;IAY7D,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAShF,oBAAoB,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;CAMjF"}
@@ -0,0 +1,159 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ /** SQLite implementation of the auth adapter */
3
+ export class SqliteAuthAdapter {
4
+ db;
5
+ constructor(db) {
6
+ this.db = db;
7
+ }
8
+ // --- Users ---
9
+ async createUser(data) {
10
+ const id = randomUUID();
11
+ const now = new Date().toISOString();
12
+ const roles = JSON.stringify(data.roles);
13
+ this.db
14
+ .prepare(`INSERT INTO users (id, email, name, roles, oidc_subject, oidc_issuer, created_at, updated_at)
15
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
16
+ .run(id, data.email, data.name, roles, data.oidcSubject ?? null, data.oidcIssuer ?? null, now, now);
17
+ return {
18
+ id,
19
+ email: data.email,
20
+ name: data.name,
21
+ roles: data.roles,
22
+ oidcSubject: data.oidcSubject ?? null,
23
+ oidcIssuer: data.oidcIssuer ?? null,
24
+ isActive: true,
25
+ createdAt: now,
26
+ updatedAt: now,
27
+ };
28
+ }
29
+ async getUser(id) {
30
+ const row = this.db.prepare('SELECT * FROM users WHERE id = ?').get(id);
31
+ return row ? rowToUser(row) : null;
32
+ }
33
+ async getUserByEmail(email) {
34
+ const row = this.db.prepare('SELECT * FROM users WHERE email = ?').get(email);
35
+ return row ? rowToUser(row) : null;
36
+ }
37
+ async getUserByOidc(issuer, subject) {
38
+ const row = this.db
39
+ .prepare('SELECT * FROM users WHERE oidc_issuer = ? AND oidc_subject = ?')
40
+ .get(issuer, subject);
41
+ return row ? rowToUser(row) : null;
42
+ }
43
+ async listUsers() {
44
+ const rows = this.db
45
+ .prepare('SELECT * FROM users ORDER BY created_at DESC')
46
+ .all();
47
+ return rows.map(rowToUser);
48
+ }
49
+ async updateUser(id, data) {
50
+ const existing = this.db.prepare('SELECT * FROM users WHERE id = ?').get(id);
51
+ if (!existing)
52
+ return null;
53
+ const now = new Date().toISOString();
54
+ const updates = ['updated_at = ?'];
55
+ const params = [now];
56
+ if (data.email !== undefined) {
57
+ updates.push('email = ?');
58
+ params.push(data.email);
59
+ }
60
+ if (data.name !== undefined) {
61
+ updates.push('name = ?');
62
+ params.push(data.name);
63
+ }
64
+ if (data.roles !== undefined) {
65
+ updates.push('roles = ?');
66
+ params.push(JSON.stringify(data.roles));
67
+ }
68
+ if (data.isActive !== undefined) {
69
+ updates.push('is_active = ?');
70
+ params.push(data.isActive ? 1 : 0);
71
+ }
72
+ if (data.oidcSubject !== undefined) {
73
+ updates.push('oidc_subject = ?');
74
+ params.push(data.oidcSubject);
75
+ }
76
+ if (data.oidcIssuer !== undefined) {
77
+ updates.push('oidc_issuer = ?');
78
+ params.push(data.oidcIssuer);
79
+ }
80
+ params.push(id);
81
+ this.db.prepare(`UPDATE users SET ${updates.join(', ')} WHERE id = ?`).run(...params);
82
+ return this.getUser(id);
83
+ }
84
+ async deleteUser(id) {
85
+ const result = this.db.prepare('DELETE FROM users WHERE id = ?').run(id);
86
+ return result.changes > 0;
87
+ }
88
+ // --- Refresh Tokens ---
89
+ async createRefreshToken(userId, tokenHash, expiresAt) {
90
+ const id = randomUUID();
91
+ this.db
92
+ .prepare('INSERT INTO refresh_tokens (id, user_id, token_hash, expires_at) VALUES (?, ?, ?, ?)')
93
+ .run(id, userId, tokenHash, expiresAt);
94
+ return id;
95
+ }
96
+ async getRefreshTokenByHash(tokenHash) {
97
+ const row = this.db
98
+ .prepare('SELECT * FROM refresh_tokens WHERE token_hash = ?')
99
+ .get(tokenHash);
100
+ if (!row)
101
+ return null;
102
+ return {
103
+ id: row.id,
104
+ userId: row.user_id,
105
+ tokenHash: row.token_hash,
106
+ expiresAt: row.expires_at,
107
+ createdAt: row.created_at,
108
+ };
109
+ }
110
+ async deleteRefreshToken(id) {
111
+ this.db.prepare('DELETE FROM refresh_tokens WHERE id = ?').run(id);
112
+ }
113
+ async deleteUserRefreshTokens(userId) {
114
+ this.db.prepare('DELETE FROM refresh_tokens WHERE user_id = ?').run(userId);
115
+ }
116
+ async deleteExpiredRefreshTokens() {
117
+ const result = this.db
118
+ .prepare('DELETE FROM refresh_tokens WHERE expires_at < ?')
119
+ .run(new Date().toISOString());
120
+ return result.changes;
121
+ }
122
+ // --- Per-Collection Permissions ---
123
+ async getUserPermissions(userId) {
124
+ const rows = this.db
125
+ .prepare('SELECT user_id, collection, role FROM user_permissions WHERE user_id = ?')
126
+ .all(userId);
127
+ return rows.map((r) => ({
128
+ userId: r.user_id,
129
+ collection: r.collection,
130
+ role: r.role,
131
+ }));
132
+ }
133
+ async setUserPermission(userId, collection, role) {
134
+ this.db
135
+ .prepare(`INSERT INTO user_permissions (user_id, collection, role) VALUES (?, ?, ?)
136
+ ON CONFLICT (user_id, collection) DO UPDATE SET role = excluded.role`)
137
+ .run(userId, collection, role);
138
+ }
139
+ async deleteUserPermission(userId, collection) {
140
+ const result = this.db
141
+ .prepare('DELETE FROM user_permissions WHERE user_id = ? AND collection = ?')
142
+ .run(userId, collection);
143
+ return result.changes > 0;
144
+ }
145
+ }
146
+ function rowToUser(row) {
147
+ return {
148
+ id: row.id,
149
+ email: row.email,
150
+ name: row.name,
151
+ roles: JSON.parse(row.roles),
152
+ oidcSubject: row.oidc_subject,
153
+ oidcIssuer: row.oidc_issuer,
154
+ isActive: row.is_active === 1,
155
+ createdAt: row.created_at,
156
+ updatedAt: row.updated_at,
157
+ };
158
+ }
159
+ //# sourceMappingURL=auth-adapter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth-adapter.js","sourceRoot":"","sources":["../src/auth-adapter.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ import type { CheckpointInfo, CheckpointTrigger, ICheckpointAdapter } from '@krantho/persistence';
2
+ import type Database from 'better-sqlite3';
3
+ /**
4
+ * SQLite 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 SqliteCheckpointAdapter implements ICheckpointAdapter {
12
+ private readonly db;
13
+ constructor(db: Database.Database);
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=checkpoints.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checkpoints.d.ts","sourceRoot":"","sources":["../src/checkpoints.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AACjG,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAA;AAoB1C;;;;;;;GAOG;AACH,qBAAa,uBAAwB,YAAW,kBAAkB;IAChE,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAmB;gBAE1B,EAAE,EAAE,QAAQ,CAAC,QAAQ;IAI3B,gBAAgB,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAAC,cAAc,CAAC;IA+BrF,eAAe,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC;IAO5C,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC;IAO9D,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;IAsE3D,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;CAI1D"}
@@ -0,0 +1,119 @@
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,
8
+ };
9
+ }
10
+ /**
11
+ * SQLite 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 SqliteCheckpointAdapter {
19
+ db;
20
+ constructor(db) {
21
+ this.db = db;
22
+ }
23
+ async createCheckpoint(name, trigger) {
24
+ const now = new Date().toISOString();
25
+ const triggerValue = trigger ?? 'manual';
26
+ const row = this.db.transaction(() => {
27
+ // Allocate next version number
28
+ const versionRow = this.db
29
+ .prepare('UPDATE checkpoint_counter SET next = next + 1 RETURNING next - 1 as version')
30
+ .get();
31
+ const insertRow = this.db
32
+ .prepare('INSERT INTO checkpoints (version, name, trigger, created_at) VALUES (?, ?, ?, ?) RETURNING id')
33
+ .get(versionRow.version, name ?? null, triggerValue, now);
34
+ return {
35
+ id: insertRow.id,
36
+ version: versionRow.version,
37
+ };
38
+ })();
39
+ return {
40
+ id: row.id,
41
+ version: row.version,
42
+ name: name ?? null,
43
+ trigger: triggerValue,
44
+ createdAt: now,
45
+ };
46
+ }
47
+ async listCheckpoints() {
48
+ const rows = this.db
49
+ .prepare('SELECT * FROM checkpoints ORDER BY version DESC')
50
+ .all();
51
+ return rows.map(rowToCheckpointInfo);
52
+ }
53
+ async getCheckpoint(version) {
54
+ const row = this.db.prepare('SELECT * FROM checkpoints WHERE version = ?').get(version);
55
+ return row ? rowToCheckpointInfo(row) : null;
56
+ }
57
+ async restoreCheckpoint(version) {
58
+ const checkpoint = await this.getCheckpoint(version);
59
+ if (!checkpoint) {
60
+ throw new Error(`Checkpoint version ${version} not found`);
61
+ }
62
+ // Create pre-restore checkpoint first
63
+ await this.createCheckpoint(`pre-restore-v${version}`, 'pre-restore');
64
+ const timestamp = checkpoint.createdAt;
65
+ // Restore all versioned tables to their state at checkpoint time
66
+ this.db.transaction(() => {
67
+ // --- Content ---
68
+ // Clear all current flags
69
+ this.db.exec('UPDATE content SET is_current = 0 WHERE is_current = 1');
70
+ // Re-activate the latest version of each entity that existed at checkpoint time
71
+ this.db
72
+ .prepare(`UPDATE content SET is_current = 1
73
+ WHERE id IN (
74
+ SELECT id FROM (
75
+ SELECT id, collection, slug, locale, created_at,
76
+ ROW_NUMBER() OVER (PARTITION BY collection, slug, locale ORDER BY version DESC) as rn
77
+ FROM content
78
+ WHERE created_at <= ?
79
+ AND (deleted_at IS NULL OR deleted_at > ?)
80
+ ) WHERE rn = 1
81
+ )`)
82
+ .run(timestamp, timestamp);
83
+ // --- Collection schemas ---
84
+ this.db.exec('UPDATE collection_schemas SET is_current = 0 WHERE is_current = 1');
85
+ this.db
86
+ .prepare(`UPDATE collection_schemas SET is_current = 1
87
+ WHERE row_id IN (
88
+ SELECT row_id FROM (
89
+ SELECT row_id, collection,
90
+ ROW_NUMBER() OVER (PARTITION BY collection ORDER BY version DESC) as rn
91
+ FROM collection_schemas
92
+ WHERE created_at <= ?
93
+ AND (deleted_at IS NULL OR deleted_at > ?)
94
+ ) WHERE rn = 1
95
+ )`)
96
+ .run(timestamp, timestamp);
97
+ // --- Media ---
98
+ this.db.exec('UPDATE media SET is_current = 0 WHERE is_current = 1');
99
+ this.db
100
+ .prepare(`UPDATE media SET is_current = 1
101
+ WHERE row_id IN (
102
+ SELECT row_id FROM (
103
+ SELECT row_id, id,
104
+ ROW_NUMBER() OVER (PARTITION BY id ORDER BY version DESC) as rn
105
+ FROM media
106
+ WHERE created_at <= ?
107
+ AND (deleted_at IS NULL OR deleted_at > ?)
108
+ ) WHERE rn = 1
109
+ )`)
110
+ .run(timestamp, timestamp);
111
+ })();
112
+ return checkpoint;
113
+ }
114
+ async deleteCheckpoint(version) {
115
+ const result = this.db.prepare('DELETE FROM checkpoints WHERE version = ?').run(version);
116
+ return result.changes > 0;
117
+ }
118
+ }
119
+ //# sourceMappingURL=checkpoints.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checkpoints.js","sourceRoot":"","sources":["../src/checkpoints.ts"],"names":[],"mappings":"AAWA,SAAS,mBAAmB,CAAC,GAAkB;IAC7C,OAAO;QACL,EAAE,EAAE,GAAG,CAAC,EAAE;QACV,OAAO,EAAE,GAAG,CAAC,OAAO;QACpB,IAAI,EAAE,GAAG,CAAC,IAAI;QACd,OAAO,EAAE,GAAG,CAAC,OAA4B;QACzC,SAAS,EAAE,GAAG,CAAC,UAAU;KAC1B,CAAA;AACH,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,OAAO,uBAAuB;IACjB,EAAE,CAAmB;IAEtC,YAAY,EAAqB;QAC/B,IAAI,CAAC,EAAE,GAAG,EAAE,CAAA;IACd,CAAC;IAED,KAAK,CAAC,gBAAgB,CAAC,IAAa,EAAE,OAA2B;QAC/D,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAA;QACpC,MAAM,YAAY,GAAG,OAAO,IAAI,QAAQ,CAAA;QAExC,MAAM,GAAG,GAAG,IAAI,CAAC,EAAE,CAAC,WAAW,CAAC,GAAG,EAAE;YACnC,+BAA+B;YAC/B,MAAM,UAAU,GAAG,IAAI,CAAC,EAAE;iBACvB,OAAO,CAAC,6EAA6E,CAAC;iBACtF,GAAG,EAAyB,CAAA;YAE/B,MAAM,SAAS,GAAG,IAAI,CAAC,EAAE;iBACtB,OAAO,CACN,+FAA+F,CAChG;iBACA,GAAG,CAAC,UAAU,CAAC,OAAO,EAAE,IAAI,IAAI,IAAI,EAAE,YAAY,EAAE,GAAG,CAAmB,CAAA;YAE7E,OAAO;gBACL,EAAE,EAAE,SAAS,CAAC,EAAE;gBAChB,OAAO,EAAE,UAAU,CAAC,OAAO;aAC5B,CAAA;QACH,CAAC,CAAC,EAAE,CAAA;QAEJ,OAAO;YACL,EAAE,EAAE,GAAG,CAAC,EAAE;YACV,OAAO,EAAE,GAAG,CAAC,OAAO;YACpB,IAAI,EAAE,IAAI,IAAI,IAAI;YAClB,OAAO,EAAE,YAAY;YACrB,SAAS,EAAE,GAAG;SACf,CAAA;IACH,CAAC;IAED,KAAK,CAAC,eAAe;QACnB,MAAM,IAAI,GAAG,IAAI,CAAC,EAAE;aACjB,OAAO,CAAC,iDAAiD,CAAC;aAC1D,GAAG,EAAqB,CAAA;QAC3B,OAAO,IAAI,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAA;IACtC,CAAC;IAED,KAAK,CAAC,aAAa,CAAC,OAAe;QACjC,MAAM,GAAG,GAAG,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,6CAA6C,CAAC,CAAC,GAAG,CAAC,OAAO,CAEzE,CAAA;QACb,OAAO,GAAG,CAAC,CAAC,CAAC,mBAAmB,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;IAC9C,CAAC;IAED,KAAK,CAAC,iBAAiB,CAAC,OAAe;QACrC,MAAM,UAAU,GAAG,MAAM,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC,CAAA;QACpD,IAAI,CAAC,UAAU,EAAE,CAAC;YAChB,MAAM,IAAI,KAAK,CAAC,sBAAsB,OAAO,YAAY,CAAC,CAAA;QAC5D,CAAC;QAED,sCAAsC;QACtC,MAAM,IAAI,CAAC,gBAAgB,CAAC,gBAAgB,OAAO,EAAE,EAAE,aAAa,CAAC,CAAA;QAErE,MAAM,SAAS,GAAG,UAAU,CAAC,SAAS,CAAA;QAEtC,iEAAiE;QACjE,IAAI,CAAC,EAAE,CAAC,WAAW,CAAC,GAAG,EAAE;YACvB,kBAAkB;YAClB,0BAA0B;YAC1B,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,wDAAwD,CAAC,CAAA;YACtE,gFAAgF;YAChF,IAAI,CAAC,EAAE;iBACJ,OAAO,CACN;;;;;;;;;aASG,CACJ;iBACA,GAAG,CAAC,SAAS,EAAE,SAAS,CAAC,CAAA;YAE5B,6BAA6B;YAC7B,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,mEAAmE,CAAC,CAAA;YACjF,IAAI,CAAC,EAAE;iBACJ,OAAO,CACN;;;;;;;;;aASG,CACJ;iBACA,GAAG,CAAC,SAAS,EAAE,SAAS,CAAC,CAAA;YAE5B,gBAAgB;YAChB,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,sDAAsD,CAAC,CAAA;YACpE,IAAI,CAAC,EAAE;iBACJ,OAAO,CACN;;;;;;;;;aASG,CACJ;iBACA,GAAG,CAAC,SAAS,EAAE,SAAS,CAAC,CAAA;QAC9B,CAAC,CAAC,EAAE,CAAA;QAEJ,OAAO,UAAU,CAAA;IACnB,CAAC;IAED,KAAK,CAAC,gBAAgB,CAAC,OAAe;QACpC,MAAM,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,2CAA2C,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;QACxF,OAAO,MAAM,CAAC,OAAO,GAAG,CAAC,CAAA;IAC3B,CAAC;CACF"}
@@ -0,0 +1,29 @@
1
+ import Database from 'better-sqlite3';
2
+ /** Connection tuning applied by {@link openDatabase} */
3
+ export interface SqliteConnectionOptions {
4
+ /** How long a statement waits for another connection's write lock before failing */
5
+ busyTimeoutMs: number;
6
+ /** Attempts to switch a brand-new database file into WAL mode */
7
+ walRetries: number;
8
+ /** Pause between WAL switch attempts */
9
+ walRetryDelayMs: number;
10
+ }
11
+ /**
12
+ * Tuning used when a caller supplies none.
13
+ *
14
+ * A deployment sets these through `persistence.sqlite` in the krantho
15
+ * configuration tree; the server passes the resolved values down. These
16
+ * literals are the standalone-library fallback and mirror that tree's
17
+ * defaults.
18
+ */
19
+ export declare const DEFAULT_SQLITE_CONNECTION_OPTIONS: SqliteConnectionOptions;
20
+ /**
21
+ * Open a connection to a SQLite database file.
22
+ *
23
+ * Several processes hold the same file open in normal operation: a server, a CLI,
24
+ * a framework that spawns one build worker per core. Opening is therefore written
25
+ * to tolerate a concurrent opener — the busy timeout is set before anything else
26
+ * so later statements wait for a writer rather than failing outright.
27
+ */
28
+ export declare function openDatabase(dbPath: string, options?: Partial<SqliteConnectionOptions>): Database.Database;
29
+ //# sourceMappingURL=connection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../src/connection.ts"],"names":[],"mappings":"AAAA,OAAO,QAAQ,MAAM,gBAAgB,CAAA;AAErC,wDAAwD;AACxD,MAAM,WAAW,uBAAuB;IACtC,oFAAoF;IACpF,aAAa,EAAE,MAAM,CAAA;IACrB,iEAAiE;IACjE,UAAU,EAAE,MAAM,CAAA;IAClB,wCAAwC;IACxC,eAAe,EAAE,MAAM,CAAA;CACxB;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,iCAAiC,EAAE,uBAI/C,CAAA;AAkDD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAC1B,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,OAAO,CAAC,uBAAuB,CAAM,GAC7C,QAAQ,CAAC,QAAQ,CAWnB"}
@@ -0,0 +1,77 @@
1
+ import Database from 'better-sqlite3';
2
+ /**
3
+ * Tuning used when a caller supplies none.
4
+ *
5
+ * A deployment sets these through `persistence.sqlite` in the krantho
6
+ * configuration tree; the server passes the resolved values down. These
7
+ * literals are the standalone-library fallback and mirror that tree's
8
+ * defaults.
9
+ */
10
+ export const DEFAULT_SQLITE_CONNECTION_OPTIONS = {
11
+ busyTimeoutMs: 10000,
12
+ walRetries: 10,
13
+ walRetryDelayMs: 50,
14
+ };
15
+ function isBusyError(err) {
16
+ return (typeof err === 'object' && err !== null && err.code === 'SQLITE_BUSY');
17
+ }
18
+ /** Block the thread. better-sqlite3 is synchronous, so there is nothing to await. */
19
+ function sleepSync(ms) {
20
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
21
+ }
22
+ /**
23
+ * Put the connection into WAL mode.
24
+ *
25
+ * Changing the journal mode needs an exclusive lock, and SQLite does not run the
26
+ * busy handler for it — a `busy_timeout` does not make this particular statement
27
+ * wait. Read the mode first so that only a connection finding a brand-new file
28
+ * attempts the write at all; every connection opened after the file is already in
29
+ * WAL skips it. The retry covers the remaining case of several connections opening
30
+ * the same new file at the same moment.
31
+ */
32
+ function enableWalMode(db, retries, retryDelayMs) {
33
+ for (let attempt = 1; attempt <= retries; attempt++) {
34
+ if (db.pragma('journal_mode', { simple: true }) === 'wal')
35
+ return;
36
+ try {
37
+ db.pragma('journal_mode = WAL');
38
+ return;
39
+ }
40
+ catch (err) {
41
+ if (!isBusyError(err) || attempt === retries)
42
+ throw err;
43
+ sleepSync(retryDelayMs);
44
+ }
45
+ }
46
+ }
47
+ /**
48
+ * Register `kr_lower`, the case fold content queries match with.
49
+ *
50
+ * SQLite's `lower()` folds ASCII only. Folding with JavaScript's `toLowerCase`, as the
51
+ * query's needle is folded, makes "ørret" find "Ørret". Functions belong to a connection,
52
+ * so every connection the adapter opens registers it here.
53
+ */
54
+ function registerFold(db) {
55
+ db.function('kr_lower', { deterministic: true }, (text) => typeof text === 'string' ? text.toLowerCase() : text);
56
+ }
57
+ /**
58
+ * Open a connection to a SQLite database file.
59
+ *
60
+ * Several processes hold the same file open in normal operation: a server, a CLI,
61
+ * a framework that spawns one build worker per core. Opening is therefore written
62
+ * to tolerate a concurrent opener — the busy timeout is set before anything else
63
+ * so later statements wait for a writer rather than failing outright.
64
+ */
65
+ export function openDatabase(dbPath, options = {}) {
66
+ const { busyTimeoutMs, walRetries, walRetryDelayMs } = {
67
+ ...DEFAULT_SQLITE_CONNECTION_OPTIONS,
68
+ ...options,
69
+ };
70
+ const db = new Database(dbPath);
71
+ db.pragma(`busy_timeout = ${busyTimeoutMs}`);
72
+ enableWalMode(db, walRetries, walRetryDelayMs);
73
+ db.pragma('foreign_keys = ON');
74
+ registerFold(db);
75
+ return db;
76
+ }
77
+ //# sourceMappingURL=connection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connection.js","sourceRoot":"","sources":["../src/connection.ts"],"names":[],"mappings":"AAAA,OAAO,QAAQ,MAAM,gBAAgB,CAAA;AAYrC;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,iCAAiC,GAA4B;IACxE,aAAa,EAAE,KAAK;IACpB,UAAU,EAAE,EAAE;IACd,eAAe,EAAE,EAAE;CACpB,CAAA;AAED,SAAS,WAAW,CAAC,GAAY;IAC/B,OAAO,CACL,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI,IAAK,GAAyB,CAAC,IAAI,KAAK,aAAa,CAC7F,CAAA;AACH,CAAC;AAED,qFAAqF;AACrF,SAAS,SAAS,CAAC,EAAU;IAC3B,OAAO,CAAC,IAAI,CAAC,IAAI,UAAU,CAAC,IAAI,iBAAiB,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAA;AAClE,CAAC;AAED;;;;;;;;;GASG;AACH,SAAS,aAAa,CAAC,EAAqB,EAAE,OAAe,EAAE,YAAoB;IACjF,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,IAAI,OAAO,EAAE,OAAO,EAAE,EAAE,CAAC;QACpD,IAAI,EAAE,CAAC,MAAM,CAAC,cAAc,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,KAAK,KAAK;YAAE,OAAM;QAEjE,IAAI,CAAC;YACH,EAAE,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAA;YAC/B,OAAM;QACR,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,IAAI,OAAO,KAAK,OAAO;gBAAE,MAAM,GAAG,CAAA;YACvD,SAAS,CAAC,YAAY,CAAC,CAAA;QACzB,CAAC;IACH,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,SAAS,YAAY,CAAC,EAAqB;IACzC,EAAE,CAAC,QAAQ,CAAC,UAAU,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,EAAE,CAAC,IAAI,EAAE,EAAE,CACxD,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,IAAI,CACrD,CAAA;AACH,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,YAAY,CAC1B,MAAc,EACd,UAA4C,EAAE;IAE9C,MAAM,EAAE,aAAa,EAAE,UAAU,EAAE,eAAe,EAAE,GAAG;QACrD,GAAG,iCAAiC;QACpC,GAAG,OAAO;KACX,CAAA;IACD,MAAM,EAAE,GAAG,IAAI,QAAQ,CAAC,MAAM,CAAC,CAAA;IAC/B,EAAE,CAAC,MAAM,CAAC,kBAAkB,aAAa,EAAE,CAAC,CAAA;IAC5C,aAAa,CAAC,EAAE,EAAE,UAAU,EAAE,eAAe,CAAC,CAAA;IAC9C,EAAE,CAAC,MAAM,CAAC,mBAAmB,CAAC,CAAA;IAC9B,YAAY,CAAC,EAAE,CAAC,CAAA;IAChB,OAAO,EAAE,CAAA;AACX,CAAC"}
@@ -0,0 +1,18 @@
1
+ import type { ConflictResolution, ContentConflict, ContentManifestRecord, IContentMigrationAdapter } from '@krantho/persistence';
2
+ import type Database from 'better-sqlite3';
3
+ /** SQLite content migration adapter */
4
+ export declare class SqliteContentMigrationAdapter implements IContentMigrationAdapter {
5
+ private readonly db;
6
+ constructor(db: Database.Database);
7
+ getContentManifestVersion(): Promise<number | null>;
8
+ getContentManifest(): Promise<ContentManifestRecord | null>;
9
+ saveContentManifest(manifest: ContentManifestRecord): Promise<void>;
10
+ listConflicts(): Promise<ContentConflict[]>;
11
+ resolveConflict(conflictId: number, resolution: ConflictResolution): Promise<boolean>;
12
+ recordConflict(conflict: Omit<ContentConflict, 'id' | 'resolved' | 'resolvedAt' | 'resolution'>): Promise<void>;
13
+ }
14
+ /** Compute SHA-256 checksum of content string */
15
+ export declare function computeContentChecksum(content: string): string;
16
+ export type { MigrationDecision } from '@krantho/persistence';
17
+ export { decideMigration } from '@krantho/persistence';
18
+ //# sourceMappingURL=content-migration.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content-migration.d.ts","sourceRoot":"","sources":["../src/content-migration.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,kBAAkB,EAClB,eAAe,EACf,qBAAqB,EACrB,wBAAwB,EACzB,MAAM,sBAAsB,CAAA;AAC7B,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAA;AAE1C,uCAAuC;AACvC,qBAAa,6BAA8B,YAAW,wBAAwB;IAC5E,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAmB;gBAE1B,EAAE,EAAE,QAAQ,CAAC,QAAQ;IAI3B,yBAAyB,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAQnD,kBAAkB,IAAI,OAAO,CAAC,qBAAqB,GAAG,IAAI,CAAC;IA0B3D,mBAAmB,CAAC,QAAQ,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC;IAanE,aAAa,IAAI,OAAO,CAAC,eAAe,EAAE,CAAC;IAgC3C,eAAe,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,kBAAkB,GAAG,OAAO,CAAC,OAAO,CAAC;IAUrF,cAAc,CAClB,QAAQ,EAAE,IAAI,CAAC,eAAe,EAAE,IAAI,GAAG,UAAU,GAAG,YAAY,GAAG,YAAY,CAAC,GAC/E,OAAO,CAAC,IAAI,CAAC;CAcjB;AAED,iDAAiD;AACjD,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAE9D;AAED,YAAY,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAA;AAE7D,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA"}
@@ -0,0 +1,68 @@
1
+ import { createHash } from 'node:crypto';
2
+ /** SQLite content migration adapter */
3
+ export class SqliteContentMigrationAdapter {
4
+ db;
5
+ constructor(db) {
6
+ this.db = db;
7
+ }
8
+ async getContentManifestVersion() {
9
+ const row = this.db
10
+ .prepare('SELECT MAX(repo_version) as max_version FROM content_manifest')
11
+ .get();
12
+ return row?.max_version ?? null;
13
+ }
14
+ async getContentManifest() {
15
+ const row = this.db
16
+ .prepare('SELECT id, repo_version, repo_commit, imported_at, file_checksums FROM content_manifest ORDER BY id DESC LIMIT 1')
17
+ .get();
18
+ if (!row)
19
+ return null;
20
+ return {
21
+ id: row.id,
22
+ repoVersion: row.repo_version,
23
+ repoCommit: row.repo_commit,
24
+ importedAt: row.imported_at,
25
+ fileChecksums: JSON.parse(row.file_checksums),
26
+ };
27
+ }
28
+ async saveContentManifest(manifest) {
29
+ this.db
30
+ .prepare('INSERT INTO content_manifest (repo_version, repo_commit, imported_at, file_checksums) VALUES (?, ?, ?, ?)')
31
+ .run(manifest.repoVersion, manifest.repoCommit, manifest.importedAt, JSON.stringify(manifest.fileChecksums));
32
+ }
33
+ async listConflicts() {
34
+ const rows = this.db
35
+ .prepare('SELECT id, migration_id, collection, slug, locale, repo_checksum, db_checksum, resolved, resolved_at, resolution FROM content_migration_conflicts WHERE resolved = 0 ORDER BY id DESC')
36
+ .all();
37
+ return rows.map((r) => ({
38
+ id: r.id,
39
+ migrationId: r.migration_id,
40
+ collection: r.collection,
41
+ slug: r.slug,
42
+ locale: r.locale,
43
+ repoChecksum: r.repo_checksum,
44
+ dbChecksum: r.db_checksum,
45
+ resolved: r.resolved === 1,
46
+ resolvedAt: r.resolved_at,
47
+ resolution: r.resolution,
48
+ }));
49
+ }
50
+ async resolveConflict(conflictId, resolution) {
51
+ const result = this.db
52
+ .prepare('UPDATE content_migration_conflicts SET resolved = 1, resolved_at = ?, resolution = ? WHERE id = ? AND resolved = 0')
53
+ .run(new Date().toISOString(), resolution, conflictId);
54
+ return result.changes > 0;
55
+ }
56
+ async recordConflict(conflict) {
57
+ this.db
58
+ .prepare('INSERT INTO content_migration_conflicts (migration_id, collection, slug, locale, repo_checksum, db_checksum) VALUES (?, ?, ?, ?, ?, ?)')
59
+ .run(conflict.migrationId, conflict.collection, conflict.slug, conflict.locale, conflict.repoChecksum, conflict.dbChecksum);
60
+ }
61
+ }
62
+ /** Compute SHA-256 checksum of content string */
63
+ export function computeContentChecksum(content) {
64
+ return createHash('sha256').update(content).digest('hex');
65
+ }
66
+ // Re-export shared content migration utilities for backward compatibility
67
+ export { decideMigration } from '@krantho/persistence';
68
+ //# sourceMappingURL=content-migration.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content-migration.js","sourceRoot":"","sources":["../src/content-migration.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AASxC,uCAAuC;AACvC,MAAM,OAAO,6BAA6B;IACvB,EAAE,CAAmB;IAEtC,YAAY,EAAqB;QAC/B,IAAI,CAAC,EAAE,GAAG,EAAE,CAAA;IACd,CAAC;IAED,KAAK,CAAC,yBAAyB;QAC7B,MAAM,GAAG,GAAG,IAAI,CAAC,EAAE;aAChB,OAAO,CAAC,+DAA+D,CAAC;aACxE,GAAG,EAAgD,CAAA;QAEtD,OAAO,GAAG,EAAE,WAAW,IAAI,IAAI,CAAA;IACjC,CAAC;IAED,KAAK,CAAC,kBAAkB;QACtB,MAAM,GAAG,GAAG,IAAI,CAAC,EAAE;aAChB,OAAO,CACN,kHAAkH,CACnH;aACA,GAAG,EAQO,CAAA;QAEb,IAAI,CAAC,GAAG;YAAE,OAAO,IAAI,CAAA;QAErB,OAAO;YACL,EAAE,EAAE,GAAG,CAAC,EAAE;YACV,WAAW,EAAE,GAAG,CAAC,YAAY;YAC7B,UAAU,EAAE,GAAG,CAAC,WAAW;YAC3B,UAAU,EAAE,GAAG,CAAC,WAAW;YAC3B,aAAa,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,cAAc,CAA2B;SACxE,CAAA;IACH,CAAC;IAED,KAAK,CAAC,mBAAmB,CAAC,QAA+B;QACvD,IAAI,CAAC,EAAE;aACJ,OAAO,CACN,2GAA2G,CAC5G;aACA,GAAG,CACF,QAAQ,CAAC,WAAW,EACpB,QAAQ,CAAC,UAAU,EACnB,QAAQ,CAAC,UAAU,EACnB,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,aAAa,CAAC,CACvC,CAAA;IACL,CAAC;IAED,KAAK,CAAC,aAAa;QACjB,MAAM,IAAI,GAAG,IAAI,CAAC,EAAE;aACjB,OAAO,CACN,uLAAuL,CACxL;aACA,GAAG,EAWJ,CAAA;QAEF,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YACtB,EAAE,EAAE,CAAC,CAAC,EAAE;YACR,WAAW,EAAE,CAAC,CAAC,YAAY;YAC3B,UAAU,EAAE,CAAC,CAAC,UAAU;YACxB,IAAI,EAAE,CAAC,CAAC,IAAI;YACZ,MAAM,EAAE,CAAC,CAAC,MAAM;YAChB,YAAY,EAAE,CAAC,CAAC,aAAa;YAC7B,UAAU,EAAE,CAAC,CAAC,WAAW;YACzB,QAAQ,EAAE,CAAC,CAAC,QAAQ,KAAK,CAAC;YAC1B,UAAU,EAAE,CAAC,CAAC,WAAW;YACzB,UAAU,EAAE,CAAC,CAAC,UAAuC;SACtD,CAAC,CAAC,CAAA;IACL,CAAC;IAED,KAAK,CAAC,eAAe,CAAC,UAAkB,EAAE,UAA8B;QACtE,MAAM,MAAM,GAAG,IAAI,CAAC,EAAE;aACnB,OAAO,CACN,oHAAoH,CACrH;aACA,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE,UAAU,EAAE,UAAU,CAAC,CAAA;QAExD,OAAO,MAAM,CAAC,OAAO,GAAG,CAAC,CAAA;IAC3B,CAAC;IAED,KAAK,CAAC,cAAc,CAClB,QAAgF;QAEhF,IAAI,CAAC,EAAE;aACJ,OAAO,CACN,wIAAwI,CACzI;aACA,GAAG,CACF,QAAQ,CAAC,WAAW,EACpB,QAAQ,CAAC,UAAU,EACnB,QAAQ,CAAC,IAAI,EACb,QAAQ,CAAC,MAAM,EACf,QAAQ,CAAC,YAAY,EACrB,QAAQ,CAAC,UAAU,CACpB,CAAA;IACL,CAAC;CACF;AAED,iDAAiD;AACjD,MAAM,UAAU,sBAAsB,CAAC,OAAe;IACpD,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;AAC3D,CAAC;AAGD,0EAA0E;AAC1E,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA"}
@@ -0,0 +1,16 @@
1
+ import type { ContentQuery, QueryPage, QueryProjection } from '@krantho/persistence';
2
+ import type Database from 'better-sqlite3';
3
+ /**
4
+ * Build the one statement a content query runs: the projection's columns, the rows of
5
+ * its state, the text match, the order and a page of `limit + 1` rows.
6
+ *
7
+ * Every name in the SQL is a whitelisted column; collections, locale, match text and
8
+ * paging are parameters.
9
+ */
10
+ export declare function buildContentQuery(spec: ContentQuery): {
11
+ sql: string;
12
+ params: unknown[];
13
+ };
14
+ /** Run a content query: one statement, one page */
15
+ export declare function runContentQuery<P extends QueryProjection>(db: Database.Database, spec: ContentQuery<P>): QueryPage<P>;
16
+ //# sourceMappingURL=content-query.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content-query.d.ts","sourceRoot":"","sources":["../src/content-query.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,YAAY,EAGZ,SAAS,EACT,eAAe,EAChB,MAAM,sBAAsB,CAAA;AAE7B,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAA;AAmG1C;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,YAAY,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,OAAO,EAAE,CAAA;CAAE,CA+BxF;AAaD,mDAAmD;AACnD,wBAAgB,eAAe,CAAC,CAAC,SAAS,eAAe,EACvD,EAAE,EAAE,QAAQ,CAAC,QAAQ,EACrB,IAAI,EAAE,YAAY,CAAC,CAAC,CAAC,GACpB,SAAS,CAAC,CAAC,CAAC,CAWd"}