@growth-labs/cms 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +106 -2
- package/dist/cli/migrations.d.ts +3 -0
- package/dist/cli/migrations.d.ts.map +1 -0
- package/dist/cli/migrations.js +73 -0
- package/dist/cli/migrations.js.map +1 -0
- package/dist/engine/index.d.ts +1 -0
- package/dist/engine/index.d.ts.map +1 -1
- package/dist/engine/index.js +2 -0
- package/dist/engine/index.js.map +1 -1
- package/dist/engine/reader-state.d.ts +127 -0
- package/dist/engine/reader-state.d.ts.map +1 -0
- package/dist/engine/reader-state.js +493 -0
- package/dist/engine/reader-state.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/migration-vendor-files.d.ts +17 -0
- package/dist/migration-vendor-files.d.ts.map +1 -0
- package/dist/migration-vendor-files.js +67 -0
- package/dist/migration-vendor-files.js.map +1 -0
- package/dist/migration-vendor.d.ts +26 -0
- package/dist/migration-vendor.d.ts.map +1 -0
- package/dist/migration-vendor.js +321 -0
- package/dist/migration-vendor.js.map +1 -0
- package/dist/schema/index.d.ts +1 -1
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/migrations.d.ts.map +1 -1
- package/dist/schema/migrations.js +37 -0
- package/dist/schema/migrations.js.map +1 -1
- package/dist/schema/tables.d.ts +1 -1
- package/dist/schema/tables.d.ts.map +1 -1
- package/dist/schema/tables.js +5 -2
- package/dist/schema/tables.js.map +1 -1
- package/dist/schema/types.d.ts +27 -0
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/schema/types.js.map +1 -1
- package/migrations/0019_reader_state.sql +34 -0
- package/package.json +8 -1
- package/src/cli/migrations.ts +85 -0
- package/src/engine/index.ts +38 -0
- package/src/engine/reader-state.ts +918 -0
- package/src/index.ts +4 -2
- package/src/migration-vendor-files.ts +100 -0
- package/src/migration-vendor.ts +450 -0
- package/src/schema/index.ts +2 -0
- package/src/schema/migrations.ts +37 -0
- package/src/schema/tables.ts +5 -2
- package/src/schema/types.ts +29 -0
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
export const READER_AUTOMATIC_COMPLETION_BPS = 9_000;
|
|
2
|
+
export const DEFAULT_READER_STATE_BATCH_SIZE = 50;
|
|
3
|
+
export const MAX_READER_STATE_BATCH_SIZE = 100;
|
|
4
|
+
const UUIDV4_PATTERN = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/;
|
|
5
|
+
const ULID_PATTERN = /^[0-9A-HJKMNP-TV-Z]{26}$/;
|
|
6
|
+
const SITE_ID_PATTERN = /^[a-z][a-z0-9_-]{1,63}$/;
|
|
7
|
+
const CONTENT_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:@/-]{0,127}$/;
|
|
8
|
+
const MUTATION_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:@/-]{0,127}$/;
|
|
9
|
+
const PREFERENCE_KEY_PATTERN = /^[A-Za-z][A-Za-z0-9._:-]{0,63}$/;
|
|
10
|
+
const CONTENT_TYPES = new Set(['article', 'video', 'podcast', 'newsletter', 'page']);
|
|
11
|
+
const MAX_PREFERENCE_KEYS = 50;
|
|
12
|
+
const MAX_PREFERENCES_JSON_BYTES = 8_192;
|
|
13
|
+
const MAX_EVENT_FUTURE_SKEW_MS = 5 * 60 * 1_000;
|
|
14
|
+
export class ReaderStateInputError extends Error {
|
|
15
|
+
constructor(message) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = 'ReaderStateInputError';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export class ReaderStateConflictError extends Error {
|
|
21
|
+
expectedVersion;
|
|
22
|
+
actualVersion;
|
|
23
|
+
constructor(expectedVersion, actualVersion) {
|
|
24
|
+
super(`@growth-labs/cms reader state version conflict: expected ${expectedVersion}, actual ${actualVersion ?? 'missing'}`);
|
|
25
|
+
this.expectedVersion = expectedVersion;
|
|
26
|
+
this.actualVersion = actualVersion;
|
|
27
|
+
this.name = 'ReaderStateConflictError';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export class ReaderStateDurableStateError extends Error {
|
|
31
|
+
constructor(message) {
|
|
32
|
+
super(message);
|
|
33
|
+
this.name = 'ReaderStateDurableStateError';
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export async function getReaderState(db, scope, key) {
|
|
37
|
+
const resolvedScope = resolveScope(scope);
|
|
38
|
+
const resolvedKey = resolveContentKey(key);
|
|
39
|
+
return loadReaderState(db, resolvedScope, resolvedKey);
|
|
40
|
+
}
|
|
41
|
+
/** Unlock is intentionally a read-only observation: it never creates or completes state. */
|
|
42
|
+
export async function recordReaderUnlock(db, scope, key) {
|
|
43
|
+
return getReaderState(db, scope, key);
|
|
44
|
+
}
|
|
45
|
+
export async function recordReaderOpen(db, scope, key, options) {
|
|
46
|
+
const resolvedScope = resolveScope(scope);
|
|
47
|
+
const resolvedKey = resolveContentKey(key);
|
|
48
|
+
if (resolveBehavioralConsent(options) === 'denied') {
|
|
49
|
+
return {
|
|
50
|
+
applied: false,
|
|
51
|
+
reason: 'consent_denied',
|
|
52
|
+
state: await loadReaderState(db, resolvedScope, resolvedKey),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
const mutation = resolveMutation(options);
|
|
56
|
+
const { occurredAt, expectedVersion } = mutation;
|
|
57
|
+
return executeMutation(db, resolvedScope, resolvedKey, occurredAt, expectedVersion, db
|
|
58
|
+
.prepare(`UPDATE cms_reader_state
|
|
59
|
+
SET first_opened_at = CASE
|
|
60
|
+
WHEN first_opened_at IS NULL OR first_opened_at > ? THEN ?
|
|
61
|
+
ELSE first_opened_at END,
|
|
62
|
+
last_opened_at = CASE
|
|
63
|
+
WHEN last_opened_at IS NULL OR last_opened_at < ? THEN ?
|
|
64
|
+
ELSE last_opened_at END,
|
|
65
|
+
version = version + 1,
|
|
66
|
+
updated_at = CASE WHEN updated_at < ? THEN ? ELSE updated_at END
|
|
67
|
+
WHERE identity_user_id = ? AND site_id = ? AND content_type = ? AND content_id = ?
|
|
68
|
+
AND (? IS NULL OR version = ?)
|
|
69
|
+
AND (first_opened_at IS NULL OR first_opened_at > ?
|
|
70
|
+
OR last_opened_at IS NULL OR last_opened_at < ?)
|
|
71
|
+
RETURNING *`)
|
|
72
|
+
.bind(occurredAt, occurredAt, occurredAt, occurredAt, occurredAt, occurredAt, resolvedScope.identityUserId, resolvedScope.siteId, resolvedKey.contentType, resolvedKey.contentId, expectedVersion ?? null, expectedVersion ?? null, occurredAt, occurredAt));
|
|
73
|
+
}
|
|
74
|
+
export async function recordReaderProgress(db, scope, key, options) {
|
|
75
|
+
const resolvedScope = resolveScope(scope);
|
|
76
|
+
const resolvedKey = resolveContentKey(key);
|
|
77
|
+
if (resolveBehavioralConsent(options) === 'denied') {
|
|
78
|
+
return {
|
|
79
|
+
applied: false,
|
|
80
|
+
reason: 'consent_denied',
|
|
81
|
+
state: await loadReaderState(db, resolvedScope, resolvedKey),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
const mutation = resolveMutation(options);
|
|
85
|
+
const progress = assertBasisPoints(options.progressBasisPoints);
|
|
86
|
+
const mediaPositionMs = optionalNonNegativeInteger(options.mediaPositionMs, 'mediaPositionMs');
|
|
87
|
+
const mediaDurationMs = optionalNonNegativeInteger(options.mediaDurationMs, 'mediaDurationMs');
|
|
88
|
+
if (mediaPositionMs !== undefined &&
|
|
89
|
+
mediaDurationMs !== undefined &&
|
|
90
|
+
mediaPositionMs > mediaDurationMs) {
|
|
91
|
+
throw new ReaderStateInputError('reader mediaPositionMs cannot exceed mediaDurationMs');
|
|
92
|
+
}
|
|
93
|
+
const { occurredAt, expectedVersion } = mutation;
|
|
94
|
+
return executeMutation(db, resolvedScope, resolvedKey, occurredAt, expectedVersion, db
|
|
95
|
+
.prepare(`UPDATE cms_reader_state
|
|
96
|
+
SET max_progress_bps = CASE
|
|
97
|
+
WHEN max_progress_bps < ? THEN ? ELSE max_progress_bps END,
|
|
98
|
+
media_position_ms = CASE
|
|
99
|
+
WHEN ? IS NOT NULL AND (progress_observed_at IS NULL OR progress_observed_at < ?)
|
|
100
|
+
THEN ? ELSE media_position_ms END,
|
|
101
|
+
media_duration_ms = CASE
|
|
102
|
+
WHEN ? IS NOT NULL AND (progress_observed_at IS NULL OR progress_observed_at < ?)
|
|
103
|
+
THEN ? ELSE media_duration_ms END,
|
|
104
|
+
progress_observed_at = CASE
|
|
105
|
+
WHEN progress_observed_at IS NULL OR progress_observed_at < ? THEN ?
|
|
106
|
+
ELSE progress_observed_at END,
|
|
107
|
+
automatic_completed_at = CASE
|
|
108
|
+
WHEN automatic_completed_at IS NULL
|
|
109
|
+
AND MAX(max_progress_bps, ?) >= ${READER_AUTOMATIC_COMPLETION_BPS}
|
|
110
|
+
THEN ? ELSE automatic_completed_at END,
|
|
111
|
+
version = version + 1,
|
|
112
|
+
updated_at = CASE WHEN updated_at < ? THEN ? ELSE updated_at END
|
|
113
|
+
WHERE identity_user_id = ? AND site_id = ? AND content_type = ? AND content_id = ?
|
|
114
|
+
AND (? IS NULL OR version = ?)
|
|
115
|
+
AND (max_progress_bps < ?
|
|
116
|
+
OR progress_observed_at IS NULL OR progress_observed_at < ?
|
|
117
|
+
OR (automatic_completed_at IS NULL AND ? >= ${READER_AUTOMATIC_COMPLETION_BPS}))
|
|
118
|
+
RETURNING *`)
|
|
119
|
+
.bind(progress, progress, mediaPositionMs ?? null, occurredAt, mediaPositionMs ?? null, mediaDurationMs ?? null, occurredAt, mediaDurationMs ?? null, occurredAt, occurredAt, progress, occurredAt, occurredAt, occurredAt, resolvedScope.identityUserId, resolvedScope.siteId, resolvedKey.contentType, resolvedKey.contentId, expectedVersion ?? null, expectedVersion ?? null, progress, occurredAt, progress));
|
|
120
|
+
}
|
|
121
|
+
export async function setManualReadState(db, scope, key, options) {
|
|
122
|
+
if (options?.state !== 'read' && options?.state !== 'unread') {
|
|
123
|
+
throw new ReaderStateInputError('manual reader state must be "read" or "unread"');
|
|
124
|
+
}
|
|
125
|
+
return writeManualState(db, scope, key, options.state, options);
|
|
126
|
+
}
|
|
127
|
+
export async function clearManualReadState(db, scope, key, options) {
|
|
128
|
+
return writeManualState(db, scope, key, null, options);
|
|
129
|
+
}
|
|
130
|
+
async function writeManualState(db, scope, key, state, options) {
|
|
131
|
+
const resolvedScope = resolveScope(scope);
|
|
132
|
+
const resolvedKey = resolveContentKey(key);
|
|
133
|
+
const mutation = resolveExplicitMutation(options);
|
|
134
|
+
return executeMutation(db, resolvedScope, resolvedKey, mutation.occurredAt, mutation.expectedVersion, db
|
|
135
|
+
.prepare(`UPDATE cms_reader_state
|
|
136
|
+
SET manual_read_state = ?, manual_state_at = ?, manual_mutation_id = ?,
|
|
137
|
+
version = version + 1,
|
|
138
|
+
updated_at = CASE WHEN updated_at < ? THEN ? ELSE updated_at END
|
|
139
|
+
WHERE identity_user_id = ? AND site_id = ? AND content_type = ? AND content_id = ?
|
|
140
|
+
AND (? IS NULL OR version = ?)
|
|
141
|
+
AND (manual_state_at IS NULL OR manual_state_at < ?
|
|
142
|
+
OR (manual_state_at = ? AND COALESCE(manual_mutation_id, '') < ?))
|
|
143
|
+
RETURNING *`)
|
|
144
|
+
.bind(state, mutation.occurredAt, options.mutationId, mutation.occurredAt, mutation.occurredAt, resolvedScope.identityUserId, resolvedScope.siteId, resolvedKey.contentType, resolvedKey.contentId, mutation.expectedVersion ?? null, mutation.expectedVersion ?? null, mutation.occurredAt, mutation.occurredAt, options.mutationId));
|
|
145
|
+
}
|
|
146
|
+
export async function setSavedState(db, scope, key, options) {
|
|
147
|
+
if (typeof options?.saved !== 'boolean') {
|
|
148
|
+
throw new ReaderStateInputError('saved reader state must be a boolean');
|
|
149
|
+
}
|
|
150
|
+
const resolvedScope = resolveScope(scope);
|
|
151
|
+
const resolvedKey = resolveContentKey(key);
|
|
152
|
+
const mutation = resolveExplicitMutation(options);
|
|
153
|
+
return executeMutation(db, resolvedScope, resolvedKey, mutation.occurredAt, mutation.expectedVersion, db
|
|
154
|
+
.prepare(`UPDATE cms_reader_state
|
|
155
|
+
SET saved = ?, saved_at = ?, saved_mutation_id = ?,
|
|
156
|
+
version = version + 1,
|
|
157
|
+
updated_at = CASE WHEN updated_at < ? THEN ? ELSE updated_at END
|
|
158
|
+
WHERE identity_user_id = ? AND site_id = ? AND content_type = ? AND content_id = ?
|
|
159
|
+
AND (? IS NULL OR version = ?)
|
|
160
|
+
AND (saved_at IS NULL OR saved_at < ?
|
|
161
|
+
OR (saved_at = ? AND COALESCE(saved_mutation_id, '') < ?))
|
|
162
|
+
RETURNING *`)
|
|
163
|
+
.bind(options.saved ? 1 : 0, mutation.occurredAt, options.mutationId, mutation.occurredAt, mutation.occurredAt, resolvedScope.identityUserId, resolvedScope.siteId, resolvedKey.contentType, resolvedKey.contentId, mutation.expectedVersion ?? null, mutation.expectedVersion ?? null, mutation.occurredAt, mutation.occurredAt, options.mutationId));
|
|
164
|
+
}
|
|
165
|
+
export async function setReaderPreferences(db, scope, key, options) {
|
|
166
|
+
const preferences = normalizePreferences(options?.preferences);
|
|
167
|
+
const resolvedScope = resolveScope(scope);
|
|
168
|
+
const resolvedKey = resolveContentKey(key);
|
|
169
|
+
const mutation = resolveExplicitMutation(options);
|
|
170
|
+
const preferencesJson = JSON.stringify(preferences);
|
|
171
|
+
return executeMutation(db, resolvedScope, resolvedKey, mutation.occurredAt, mutation.expectedVersion, db
|
|
172
|
+
.prepare(`UPDATE cms_reader_state
|
|
173
|
+
SET preferences_json = ?, preferences_at = ?, preferences_mutation_id = ?,
|
|
174
|
+
version = version + 1,
|
|
175
|
+
updated_at = CASE WHEN updated_at < ? THEN ? ELSE updated_at END
|
|
176
|
+
WHERE identity_user_id = ? AND site_id = ? AND content_type = ? AND content_id = ?
|
|
177
|
+
AND (? IS NULL OR version = ?)
|
|
178
|
+
AND (preferences_at IS NULL OR preferences_at < ?
|
|
179
|
+
OR (preferences_at = ? AND COALESCE(preferences_mutation_id, '') < ?))
|
|
180
|
+
RETURNING *`)
|
|
181
|
+
.bind(preferencesJson, mutation.occurredAt, options.mutationId, mutation.occurredAt, mutation.occurredAt, resolvedScope.identityUserId, resolvedScope.siteId, resolvedKey.contentType, resolvedKey.contentId, mutation.expectedVersion ?? null, mutation.expectedVersion ?? null, mutation.occurredAt, mutation.occurredAt, options.mutationId));
|
|
182
|
+
}
|
|
183
|
+
export async function getReaderStateBatch(db, scope, keys) {
|
|
184
|
+
const resolvedScope = resolveScope(scope);
|
|
185
|
+
if (!Array.isArray(keys)) {
|
|
186
|
+
throw new ReaderStateInputError('reader state batch keys must be an array');
|
|
187
|
+
}
|
|
188
|
+
if (keys.length > MAX_READER_STATE_BATCH_SIZE) {
|
|
189
|
+
throw new ReaderStateInputError(`reader state batches are limited to ${MAX_READER_STATE_BATCH_SIZE} declared content IDs`);
|
|
190
|
+
}
|
|
191
|
+
if (keys.length === 0)
|
|
192
|
+
return [];
|
|
193
|
+
const resolvedKeys = keys.map(resolveContentKey);
|
|
194
|
+
const declared = resolvedKeys.map(() => '(content_type = ? AND content_id = ?)').join(' OR ');
|
|
195
|
+
const statement = db.prepare(`SELECT * FROM cms_reader_state
|
|
196
|
+
WHERE identity_user_id = ? AND site_id = ? AND (${declared})
|
|
197
|
+
ORDER BY content_type, content_id`);
|
|
198
|
+
const bindings = [resolvedScope.identityUserId, resolvedScope.siteId];
|
|
199
|
+
for (const key of resolvedKeys)
|
|
200
|
+
bindings.push(key.contentType, key.contentId);
|
|
201
|
+
const result = await statement.bind(...bindings).all();
|
|
202
|
+
return (result.results ?? []).map(decodeReaderStateRow);
|
|
203
|
+
}
|
|
204
|
+
export async function listReaderState(db, scope, options = {}) {
|
|
205
|
+
const resolvedScope = resolveScope(scope);
|
|
206
|
+
return readReaderStatePage(db, resolvedScope, options, 'updated');
|
|
207
|
+
}
|
|
208
|
+
async function readReaderStatePage(db, scope, options, order) {
|
|
209
|
+
const limit = resolveLimit(options.limit);
|
|
210
|
+
const cursor = options.cursor ? decodeCursor(options.cursor, order) : null;
|
|
211
|
+
const orderColumn = order === 'updated' ? 'updated_at' : 'created_at';
|
|
212
|
+
let statement;
|
|
213
|
+
if (cursor) {
|
|
214
|
+
statement = db
|
|
215
|
+
.prepare(`SELECT * FROM cms_reader_state
|
|
216
|
+
WHERE identity_user_id = ? AND site_id = ?
|
|
217
|
+
AND (${orderColumn} < ?
|
|
218
|
+
OR (${orderColumn} = ? AND content_type > ?)
|
|
219
|
+
OR (${orderColumn} = ? AND content_type = ? AND content_id > ?))
|
|
220
|
+
ORDER BY ${orderColumn} DESC, content_type, content_id
|
|
221
|
+
LIMIT ?`)
|
|
222
|
+
.bind(scope.identityUserId, scope.siteId, cursor.orderedAt, cursor.orderedAt, cursor.contentType, cursor.orderedAt, cursor.contentType, cursor.contentId, limit + 1);
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
statement = db
|
|
226
|
+
.prepare(`SELECT * FROM cms_reader_state
|
|
227
|
+
WHERE identity_user_id = ? AND site_id = ?
|
|
228
|
+
ORDER BY ${orderColumn} DESC, content_type, content_id
|
|
229
|
+
LIMIT ?`)
|
|
230
|
+
.bind(scope.identityUserId, scope.siteId, limit + 1);
|
|
231
|
+
}
|
|
232
|
+
const result = await statement.all();
|
|
233
|
+
const decoded = (result.results ?? []).map(decodeReaderStateRow);
|
|
234
|
+
const hasMore = decoded.length > limit;
|
|
235
|
+
const items = hasMore ? decoded.slice(0, limit) : decoded;
|
|
236
|
+
const last = items.at(-1);
|
|
237
|
+
return {
|
|
238
|
+
items,
|
|
239
|
+
nextCursor: hasMore && last ? encodeCursor(last, order) : null,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
export async function exportReaderStatePage(db, scope, options = {}) {
|
|
243
|
+
const resolvedScope = resolveScope(scope);
|
|
244
|
+
const page = await readReaderStatePage(db, resolvedScope, options, 'created');
|
|
245
|
+
return {
|
|
246
|
+
...page,
|
|
247
|
+
scope: resolvedScope,
|
|
248
|
+
exportedAt: Date.now(),
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Consent-withdrawal/privacy clear: remove behavioral history while retaining
|
|
253
|
+
* explicit read/unread, save, and stated preference choices.
|
|
254
|
+
*/
|
|
255
|
+
export async function clearReaderHistory(db, scope) {
|
|
256
|
+
const resolvedScope = resolveScope(scope);
|
|
257
|
+
const now = Date.now();
|
|
258
|
+
const result = await db
|
|
259
|
+
.prepare(`UPDATE cms_reader_state
|
|
260
|
+
SET first_opened_at = NULL, last_opened_at = NULL,
|
|
261
|
+
max_progress_bps = 0, media_position_ms = NULL, media_duration_ms = NULL,
|
|
262
|
+
progress_observed_at = NULL, automatic_completed_at = NULL,
|
|
263
|
+
version = version + 1, updated_at = ?
|
|
264
|
+
WHERE identity_user_id = ? AND site_id = ?
|
|
265
|
+
AND (first_opened_at IS NOT NULL OR last_opened_at IS NOT NULL
|
|
266
|
+
OR max_progress_bps <> 0 OR media_position_ms IS NOT NULL
|
|
267
|
+
OR media_duration_ms IS NOT NULL OR progress_observed_at IS NOT NULL
|
|
268
|
+
OR automatic_completed_at IS NOT NULL)`)
|
|
269
|
+
.bind(now, resolvedScope.identityUserId, resolvedScope.siteId)
|
|
270
|
+
.run();
|
|
271
|
+
return resultChanges(result);
|
|
272
|
+
}
|
|
273
|
+
/** Delete every row for exactly one verified user/site scope. */
|
|
274
|
+
export async function deleteReaderState(db, scope) {
|
|
275
|
+
const resolvedScope = resolveScope(scope);
|
|
276
|
+
const result = await db
|
|
277
|
+
.prepare('DELETE FROM cms_reader_state WHERE identity_user_id = ? AND site_id = ?')
|
|
278
|
+
.bind(resolvedScope.identityUserId, resolvedScope.siteId)
|
|
279
|
+
.run();
|
|
280
|
+
return resultChanges(result);
|
|
281
|
+
}
|
|
282
|
+
async function executeMutation(db, scope, key, occurredAt, expectedVersion, update) {
|
|
283
|
+
const expected = expectedVersion ?? null;
|
|
284
|
+
const ensure = db
|
|
285
|
+
.prepare(`INSERT OR IGNORE INTO cms_reader_state
|
|
286
|
+
(identity_user_id, site_id, content_type, content_id, created_at, updated_at)
|
|
287
|
+
SELECT ?, ?, ?, ?, ?, ?
|
|
288
|
+
WHERE ? IS NULL OR ? = 0`)
|
|
289
|
+
.bind(scope.identityUserId, scope.siteId, key.contentType, key.contentId, occurredAt, occurredAt, expected, expected);
|
|
290
|
+
const [, updateResult] = await db.batch([ensure, update]);
|
|
291
|
+
const returnedRow = updateResult?.results?.[0];
|
|
292
|
+
if (returnedRow) {
|
|
293
|
+
return { applied: true, reason: 'applied', state: decodeReaderStateRow(returnedRow) };
|
|
294
|
+
}
|
|
295
|
+
if (resultChanges(updateResult) > 0) {
|
|
296
|
+
throw new ReaderStateDurableStateError('reader state mutation changed a row without returning its atomic state');
|
|
297
|
+
}
|
|
298
|
+
const state = await loadReaderState(db, scope, key);
|
|
299
|
+
if (!state) {
|
|
300
|
+
if (expectedVersion !== undefined) {
|
|
301
|
+
throw new ReaderStateConflictError(expectedVersion, null);
|
|
302
|
+
}
|
|
303
|
+
throw new ReaderStateDurableStateError('reader state mutation did not produce a durable row');
|
|
304
|
+
}
|
|
305
|
+
if (expectedVersion !== undefined && state.version !== expectedVersion) {
|
|
306
|
+
throw new ReaderStateConflictError(expectedVersion, state.version);
|
|
307
|
+
}
|
|
308
|
+
return { applied: false, reason: 'stale', state };
|
|
309
|
+
}
|
|
310
|
+
async function loadReaderState(db, scope, key) {
|
|
311
|
+
const row = await db
|
|
312
|
+
.prepare(`SELECT * FROM cms_reader_state
|
|
313
|
+
WHERE identity_user_id = ? AND site_id = ? AND content_type = ? AND content_id = ?`)
|
|
314
|
+
.bind(scope.identityUserId, scope.siteId, key.contentType, key.contentId)
|
|
315
|
+
.first();
|
|
316
|
+
return row ? decodeReaderStateRow(row) : null;
|
|
317
|
+
}
|
|
318
|
+
function decodeReaderStateRow(row) {
|
|
319
|
+
let preferences;
|
|
320
|
+
try {
|
|
321
|
+
preferences = normalizePreferences(JSON.parse(row.preferences_json));
|
|
322
|
+
}
|
|
323
|
+
catch (error) {
|
|
324
|
+
const detail = error instanceof Error ? `: ${error.message}` : '';
|
|
325
|
+
throw new ReaderStateDurableStateError(`reader state contains unreadable preferences${detail}`);
|
|
326
|
+
}
|
|
327
|
+
return {
|
|
328
|
+
identityUserId: row.identity_user_id,
|
|
329
|
+
siteId: row.site_id,
|
|
330
|
+
contentType: row.content_type,
|
|
331
|
+
contentId: row.content_id,
|
|
332
|
+
firstOpenedAt: row.first_opened_at,
|
|
333
|
+
lastOpenedAt: row.last_opened_at,
|
|
334
|
+
maxProgressBasisPoints: row.max_progress_bps,
|
|
335
|
+
mediaPositionMs: row.media_position_ms,
|
|
336
|
+
mediaDurationMs: row.media_duration_ms,
|
|
337
|
+
progressObservedAt: row.progress_observed_at,
|
|
338
|
+
automaticCompletedAt: row.automatic_completed_at,
|
|
339
|
+
manualReadState: row.manual_read_state,
|
|
340
|
+
manualStateAt: row.manual_state_at,
|
|
341
|
+
saved: row.saved === 1,
|
|
342
|
+
savedAt: row.saved_at,
|
|
343
|
+
preferences,
|
|
344
|
+
preferencesAt: row.preferences_at,
|
|
345
|
+
version: row.version,
|
|
346
|
+
effectiveRead: row.manual_read_state === 'read' ||
|
|
347
|
+
(row.manual_read_state === null && row.automatic_completed_at !== null),
|
|
348
|
+
createdAt: row.created_at,
|
|
349
|
+
updatedAt: row.updated_at,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
function resolveScope(scope) {
|
|
353
|
+
if (!scope || typeof scope !== 'object') {
|
|
354
|
+
throw new ReaderStateInputError('reader state requires a verified scope');
|
|
355
|
+
}
|
|
356
|
+
const identityUserId = scope.identityUserId;
|
|
357
|
+
if (typeof identityUserId !== 'string' ||
|
|
358
|
+
!(UUIDV4_PATTERN.test(identityUserId) || ULID_PATTERN.test(identityUserId))) {
|
|
359
|
+
throw new ReaderStateInputError('reader state requires a verified raw realm user id, never an analytics, local, email, or prefixed subject identifier');
|
|
360
|
+
}
|
|
361
|
+
if (typeof scope.siteId !== 'string' || !SITE_ID_PATTERN.test(scope.siteId)) {
|
|
362
|
+
throw new ReaderStateInputError('reader state requires a verified stable lowercase site id such as "fronts"');
|
|
363
|
+
}
|
|
364
|
+
return {
|
|
365
|
+
identityUserId: UUIDV4_PATTERN.test(identityUserId)
|
|
366
|
+
? identityUserId.toLowerCase()
|
|
367
|
+
: identityUserId,
|
|
368
|
+
siteId: scope.siteId,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
function resolveContentKey(key) {
|
|
372
|
+
if (!key || typeof key !== 'object' || !CONTENT_TYPES.has(key.contentType)) {
|
|
373
|
+
throw new ReaderStateInputError('reader state requires a supported CMS content type');
|
|
374
|
+
}
|
|
375
|
+
if (typeof key.contentId !== 'string' || !CONTENT_ID_PATTERN.test(key.contentId)) {
|
|
376
|
+
throw new ReaderStateInputError('reader state requires a stable CMS content id using URL-safe identifier characters');
|
|
377
|
+
}
|
|
378
|
+
return { contentType: key.contentType, contentId: key.contentId };
|
|
379
|
+
}
|
|
380
|
+
function resolveBehavioralConsent(options) {
|
|
381
|
+
if (!options || typeof options !== 'object') {
|
|
382
|
+
throw new ReaderStateInputError('reader behavioral writes require explicit consent state');
|
|
383
|
+
}
|
|
384
|
+
if (options.behavioralConsent !== 'granted' && options.behavioralConsent !== 'denied') {
|
|
385
|
+
throw new ReaderStateInputError('behavioralConsent must be "granted" or "denied"');
|
|
386
|
+
}
|
|
387
|
+
return options.behavioralConsent;
|
|
388
|
+
}
|
|
389
|
+
function resolveExplicitMutation(options) {
|
|
390
|
+
if (!options || typeof options !== 'object') {
|
|
391
|
+
throw new ReaderStateInputError('explicit reader state writes require mutation options');
|
|
392
|
+
}
|
|
393
|
+
if (typeof options.mutationId !== 'string' || !MUTATION_ID_PATTERN.test(options.mutationId)) {
|
|
394
|
+
throw new ReaderStateInputError('explicit reader state writes require a stable mutationId');
|
|
395
|
+
}
|
|
396
|
+
return resolveMutation(options);
|
|
397
|
+
}
|
|
398
|
+
function resolveMutation(options) {
|
|
399
|
+
const occurredAt = options.occurredAt ?? Date.now();
|
|
400
|
+
if (!Number.isSafeInteger(occurredAt) || occurredAt < 0) {
|
|
401
|
+
throw new ReaderStateInputError('reader state occurredAt must be a non-negative epoch millisecond');
|
|
402
|
+
}
|
|
403
|
+
if (occurredAt > Date.now() + MAX_EVENT_FUTURE_SKEW_MS) {
|
|
404
|
+
throw new ReaderStateInputError('reader state occurredAt exceeds the allowed five-minute clock skew');
|
|
405
|
+
}
|
|
406
|
+
if (options.expectedVersion !== undefined &&
|
|
407
|
+
(!Number.isSafeInteger(options.expectedVersion) || options.expectedVersion < 0)) {
|
|
408
|
+
throw new ReaderStateInputError('reader state expectedVersion must be a non-negative integer');
|
|
409
|
+
}
|
|
410
|
+
return { occurredAt, expectedVersion: options.expectedVersion };
|
|
411
|
+
}
|
|
412
|
+
function assertBasisPoints(value) {
|
|
413
|
+
if (!Number.isInteger(value) || value < 0 || value > 10_000) {
|
|
414
|
+
throw new ReaderStateInputError('reader progress must be an integer from 0 through 10000');
|
|
415
|
+
}
|
|
416
|
+
return value;
|
|
417
|
+
}
|
|
418
|
+
function optionalNonNegativeInteger(value, name) {
|
|
419
|
+
if (value === undefined)
|
|
420
|
+
return undefined;
|
|
421
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
422
|
+
throw new ReaderStateInputError(`reader ${name} must be a non-negative integer`);
|
|
423
|
+
}
|
|
424
|
+
return value;
|
|
425
|
+
}
|
|
426
|
+
function normalizePreferences(value) {
|
|
427
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
428
|
+
throw new ReaderStateInputError('reader preferences must be a plain object of stated values');
|
|
429
|
+
}
|
|
430
|
+
const prototype = Object.getPrototypeOf(value);
|
|
431
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
432
|
+
throw new ReaderStateInputError('reader preferences must be a plain object');
|
|
433
|
+
}
|
|
434
|
+
const entries = Object.entries(value);
|
|
435
|
+
if (entries.length > MAX_PREFERENCE_KEYS) {
|
|
436
|
+
throw new ReaderStateInputError(`reader preferences are limited to ${MAX_PREFERENCE_KEYS} keys`);
|
|
437
|
+
}
|
|
438
|
+
const normalized = Object.create(null);
|
|
439
|
+
for (const [key, preference] of entries.sort(([a], [b]) => a.localeCompare(b))) {
|
|
440
|
+
if (!PREFERENCE_KEY_PATTERN.test(key) ||
|
|
441
|
+
['__proto__', 'constructor', 'prototype'].includes(key)) {
|
|
442
|
+
throw new ReaderStateInputError('reader preference keys must use safe stable identifiers');
|
|
443
|
+
}
|
|
444
|
+
if (preference !== null &&
|
|
445
|
+
typeof preference !== 'string' &&
|
|
446
|
+
typeof preference !== 'boolean' &&
|
|
447
|
+
(typeof preference !== 'number' || !Number.isFinite(preference))) {
|
|
448
|
+
throw new ReaderStateInputError('reader preference values must be finite scalar values');
|
|
449
|
+
}
|
|
450
|
+
normalized[key] = preference;
|
|
451
|
+
}
|
|
452
|
+
if (new TextEncoder().encode(JSON.stringify(normalized)).byteLength > MAX_PREFERENCES_JSON_BYTES) {
|
|
453
|
+
throw new ReaderStateInputError(`reader preferences are limited to ${MAX_PREFERENCES_JSON_BYTES} encoded bytes`);
|
|
454
|
+
}
|
|
455
|
+
return normalized;
|
|
456
|
+
}
|
|
457
|
+
function resolveLimit(value) {
|
|
458
|
+
const limit = value ?? DEFAULT_READER_STATE_BATCH_SIZE;
|
|
459
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > MAX_READER_STATE_BATCH_SIZE) {
|
|
460
|
+
throw new ReaderStateInputError(`reader state list limits must be from 1 through ${MAX_READER_STATE_BATCH_SIZE}`);
|
|
461
|
+
}
|
|
462
|
+
return limit;
|
|
463
|
+
}
|
|
464
|
+
function encodeCursor(record, order) {
|
|
465
|
+
const orderedAt = order === 'updated' ? record.updatedAt : record.createdAt;
|
|
466
|
+
return encodeURIComponent(JSON.stringify([order, orderedAt, record.contentType, record.contentId]));
|
|
467
|
+
}
|
|
468
|
+
function decodeCursor(cursor, expectedOrder) {
|
|
469
|
+
try {
|
|
470
|
+
const parsed = JSON.parse(decodeURIComponent(cursor));
|
|
471
|
+
if (!Array.isArray(parsed) ||
|
|
472
|
+
parsed.length !== 4 ||
|
|
473
|
+
parsed[0] !== expectedOrder ||
|
|
474
|
+
!Number.isSafeInteger(parsed[1])) {
|
|
475
|
+
throw new Error('shape');
|
|
476
|
+
}
|
|
477
|
+
const key = resolveContentKey({ contentType: parsed[2], contentId: parsed[3] });
|
|
478
|
+
return { order: expectedOrder, orderedAt: parsed[1], ...key };
|
|
479
|
+
}
|
|
480
|
+
catch (error) {
|
|
481
|
+
if (error instanceof ReaderStateInputError)
|
|
482
|
+
throw error;
|
|
483
|
+
throw new ReaderStateInputError('reader state cursor is invalid');
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
function resultChanges(result) {
|
|
487
|
+
const changes = result?.meta?.changes;
|
|
488
|
+
if (typeof changes !== 'number' || !Number.isFinite(changes)) {
|
|
489
|
+
throw new ReaderStateDurableStateError('reader state mutation returned no finite change count');
|
|
490
|
+
}
|
|
491
|
+
return changes;
|
|
492
|
+
}
|
|
493
|
+
//# sourceMappingURL=reader-state.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reader-state.js","sourceRoot":"","sources":["../../src/engine/reader-state.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export * from './engine/index.js';
|
|
2
2
|
export * from './providers/index.js';
|
|
3
3
|
export type { CmsUser } from './routes/context.js';
|
|
4
|
-
export { type ArticleContentRow, type AuthorRow, applyMigrations, CMS_MIGRATION_SQL, CMS_MIGRATIONS_0002_PLUS, CMS_TABLES, type CmsActivityLogRow, type CmsApiKeyRow, type CmsNotificationRow, type CmsRole, type CmsTableName, type CmsWebhookRow, type ContentContributorRow, type ContentImportRow, type ContentItemRow, type ContentPublicationAttemptRow, type ContentPublicationAttemptState, type ContentPublicationOperation, type ContentRelationRow, type ContentRevisionRow, type ContentSlugRedirectRow, type ContentStatus, type ContentTagLinkRow, type ContentTagRow, type ContentType, type ContentVisibility, type FoundryCallbackEventRow, type MastheadUserRow, type MediaAssetRow, NON_CMS_TABLES, type PodcastContentRow, splitStatements, type VideoContentRow, type VideoProcessingState, type WorkspaceInviteRow, type WorkspaceMembershipRow, type WorkspaceSettingsRow, } from './schema/index.js';
|
|
4
|
+
export { type ArticleContentRow, type AuthorRow, applyMigrations, CMS_MIGRATION_SQL, CMS_MIGRATIONS_0002_PLUS, CMS_TABLES, type CmsActivityLogRow, type CmsApiKeyRow, type CmsNotificationRow, type CmsReaderStateRow, type CmsRole, type CmsTableName, type CmsWebhookRow, type ContentContributorRow, type ContentImportRow, type ContentItemRow, type ContentPublicationAttemptRow, type ContentPublicationAttemptState, type ContentPublicationOperation, type ContentRelationRow, type ContentRevisionRow, type ContentSlugRedirectRow, type ContentStatus, type ContentTagLinkRow, type ContentTagRow, type ContentType, type ContentVisibility, type FoundryCallbackEventRow, type MastheadUserRow, type MediaAssetRow, NON_CMS_TABLES, type PodcastContentRow, type ReaderManualReadState, splitStatements, type VideoContentRow, type VideoProcessingState, type WorkspaceInviteRow, type WorkspaceMembershipRow, type WorkspaceSettingsRow, } from './schema/index.js';
|
|
5
5
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAgCA,cAAc,mBAAmB,CAAA;AAGjC,cAAc,sBAAsB,CAAA;AAGpC,YAAY,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAElD,OAAO,EACN,KAAK,iBAAiB,EACtB,KAAK,SAAS,EACd,eAAe,EAEf,iBAAiB,EACjB,wBAAwB,EAExB,UAAU,EACV,KAAK,iBAAiB,EACtB,KAAK,YAAY,EACjB,KAAK,kBAAkB,EACvB,KAAK,OAAO,EACZ,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,KAAK,qBAAqB,EAC1B,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,4BAA4B,EACjC,KAAK,8BAA8B,EACnC,KAAK,2BAA2B,EAChC,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACvB,KAAK,sBAAsB,EAC3B,KAAK,aAAa,EAClB,KAAK,iBAAiB,EACtB,KAAK,aAAa,EAElB,KAAK,WAAW,EAChB,KAAK,iBAAiB,EACtB,KAAK,uBAAuB,EAC5B,KAAK,eAAe,EACpB,KAAK,aAAa,EAClB,cAAc,EACd,KAAK,iBAAiB,EACtB,eAAe,EACf,KAAK,eAAe,EACpB,KAAK,oBAAoB,EACzB,KAAK,kBAAkB,EACvB,KAAK,sBAAsB,EAC3B,KAAK,oBAAoB,GACzB,MAAM,mBAAmB,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAgCA,cAAc,mBAAmB,CAAA;AAGjC,cAAc,sBAAsB,CAAA;AAGpC,YAAY,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAElD,OAAO,EACN,KAAK,iBAAiB,EACtB,KAAK,SAAS,EACd,eAAe,EAEf,iBAAiB,EACjB,wBAAwB,EAExB,UAAU,EACV,KAAK,iBAAiB,EACtB,KAAK,YAAY,EACjB,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,KAAK,OAAO,EACZ,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,KAAK,qBAAqB,EAC1B,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,4BAA4B,EACjC,KAAK,8BAA8B,EACnC,KAAK,2BAA2B,EAChC,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACvB,KAAK,sBAAsB,EAC3B,KAAK,aAAa,EAClB,KAAK,iBAAiB,EACtB,KAAK,aAAa,EAElB,KAAK,WAAW,EAChB,KAAK,iBAAiB,EACtB,KAAK,uBAAuB,EAC5B,KAAK,eAAe,EACpB,KAAK,aAAa,EAClB,cAAc,EACd,KAAK,iBAAiB,EACtB,KAAK,qBAAqB,EAC1B,eAAe,EACf,KAAK,eAAe,EACpB,KAAK,oBAAoB,EACzB,KAAK,kBAAkB,EACvB,KAAK,sBAAsB,EAC3B,KAAK,oBAAoB,GACzB,MAAM,mBAAmB,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// @growth-labs/cms — the reusable Fronts-proven publishing/admin engine, as a
|
|
2
2
|
// package (NOT a shared engine). See docs/reference/cms-reuse-decision.md.
|
|
3
3
|
//
|
|
4
|
-
// This package ships the D1 schema contract (the 11-table
|
|
5
|
-
// content/revision/media/authors/foundry-callback set) + its TypeScript row
|
|
4
|
+
// This package ships the D1 schema contract (the original 11-table
|
|
5
|
+
// content/revision/media/authors/foundry-callback set plus additive migrations) + its TypeScript row
|
|
6
6
|
// types (WS7-04/06 foundation) AND the publishing engine — the content
|
|
7
7
|
// data-access core (WS7-05):
|
|
8
8
|
//
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,2EAA2E;AAC3E,EAAE;AACF,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,2EAA2E;AAC3E,EAAE;AACF,mEAAmE;AACnE,qGAAqG;AACrG,uEAAuE;AACvE,6BAA6B;AAC7B,EAAE;AACF,qEAAqE;AACrE,iEAAiE;AACjE,qEAAqE;AACrE,iDAAiD;AACjD,+EAA+E;AAC/E,gFAAgF;AAChF,6EAA6E;AAC7E,+EAA+E;AAC/E,oEAAoE;AACpE,2EAA2E;AAC3E,+EAA+E;AAC/E,qBAAqB;AACrB,oEAAoE;AACpE,wEAAwE;AACxE,4EAA4E;AAC5E,yEAAyE;AACzE,8EAA8E;AAC9E,gFAAgF;AAChF,0EAA0E;AAC1E,6EAA6E;AAC7E,iDAAiD;AAEjD,sEAAsE;AACtE,sDAAsD;AACtD,cAAc,mBAAmB,CAAA;AAEjC,mEAAmE;AACnE,cAAc,sBAAsB,CAAA;AAKpC,OAAO,EAGN,eAAe;AACf,qBAAqB;AACrB,iBAAiB,EACjB,wBAAwB;AACxB,iBAAiB;AACjB,UAAU,EA0BV,cAAc,EAGd,eAAe,GAMf,MAAM,mBAAmB,CAAA"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface StagedMigrationVendorFile {
|
|
2
|
+
tempPath: string;
|
|
3
|
+
targetPath: string;
|
|
4
|
+
}
|
|
5
|
+
export interface MigrationVendorFileOperations {
|
|
6
|
+
link(source: string, target: string): Promise<void>;
|
|
7
|
+
unlink(path: string): Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
export declare function rollbackNewFiles(paths: string[], operations?: MigrationVendorFileOperations): Promise<void>;
|
|
10
|
+
/**
|
|
11
|
+
* Link every pre-staged file without replacing an existing directory entry.
|
|
12
|
+
* A later failure removes all targets linked by this attempt and every temp.
|
|
13
|
+
*/
|
|
14
|
+
export declare function commitNewFilesAtomically(files: StagedMigrationVendorFile[], operations?: MigrationVendorFileOperations): Promise<string[]>;
|
|
15
|
+
/** Commit staged SQL, then run the receipt/final verification boundary. */
|
|
16
|
+
export declare function commitStagedMigrationBatch(files: StagedMigrationVendorFile[], finalize: () => Promise<void>, operations?: MigrationVendorFileOperations): Promise<string[]>;
|
|
17
|
+
//# sourceMappingURL=migration-vendor-files.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migration-vendor-files.d.ts","sourceRoot":"","sources":["../src/migration-vendor-files.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,yBAAyB;IACzC,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,6BAA6B;IAC7C,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACnD,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACnC;AAmBD,wBAAsB,gBAAgB,CACrC,KAAK,EAAE,MAAM,EAAE,EACf,UAAU,GAAE,6BAAkD,GAC5D,OAAO,CAAC,IAAI,CAAC,CAKf;AAED;;;GAGG;AACH,wBAAsB,wBAAwB,CAC7C,KAAK,EAAE,yBAAyB,EAAE,EAClC,UAAU,GAAE,6BAAkD,GAC5D,OAAO,CAAC,MAAM,EAAE,CAAC,CA8BnB;AAED,2EAA2E;AAC3E,wBAAsB,0BAA0B,CAC/C,KAAK,EAAE,yBAAyB,EAAE,EAClC,QAAQ,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,EAC7B,UAAU,GAAE,6BAAkD,GAC5D,OAAO,CAAC,MAAM,EAAE,CAAC,CAgBnB"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { link, unlink } from 'node:fs/promises';
|
|
2
|
+
const DEFAULT_OPERATIONS = { link, unlink };
|
|
3
|
+
async function removePaths(paths, operations) {
|
|
4
|
+
const errors = [];
|
|
5
|
+
for (const path of [...paths].reverse()) {
|
|
6
|
+
try {
|
|
7
|
+
await operations.unlink(path);
|
|
8
|
+
}
|
|
9
|
+
catch (error) {
|
|
10
|
+
if (error.code !== 'ENOENT')
|
|
11
|
+
errors.push(error);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return errors;
|
|
15
|
+
}
|
|
16
|
+
export async function rollbackNewFiles(paths, operations = DEFAULT_OPERATIONS) {
|
|
17
|
+
const errors = await removePaths(paths, operations);
|
|
18
|
+
if (errors.length > 0) {
|
|
19
|
+
throw new AggregateError(errors, 'failed to roll back newly linked CMS migration files');
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Link every pre-staged file without replacing an existing directory entry.
|
|
24
|
+
* A later failure removes all targets linked by this attempt and every temp.
|
|
25
|
+
*/
|
|
26
|
+
export async function commitNewFilesAtomically(files, operations = DEFAULT_OPERATIONS) {
|
|
27
|
+
const committed = [];
|
|
28
|
+
let failure;
|
|
29
|
+
try {
|
|
30
|
+
for (const file of files) {
|
|
31
|
+
await operations.link(file.tempPath, file.targetPath);
|
|
32
|
+
committed.push(file.targetPath);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
failure = error;
|
|
37
|
+
}
|
|
38
|
+
const cleanupErrors = await removePaths(files.map((file) => file.tempPath), operations);
|
|
39
|
+
if (failure) {
|
|
40
|
+
const rollbackErrors = await removePaths(committed, operations);
|
|
41
|
+
const errors = [failure, ...cleanupErrors, ...rollbackErrors].filter((error) => error !== undefined);
|
|
42
|
+
const primary = failure instanceof Error ? `: ${failure.message}` : '';
|
|
43
|
+
throw new AggregateError(errors, `CMS migration file commit failed${primary}`);
|
|
44
|
+
}
|
|
45
|
+
// A hard-linked target remains valid when unlinking only its staged temp
|
|
46
|
+
// fails. The caller's outer finally retries temp cleanup; rolling targets
|
|
47
|
+
// back here would turn a harmless leftover temp into data loss.
|
|
48
|
+
return committed;
|
|
49
|
+
}
|
|
50
|
+
/** Commit staged SQL, then run the receipt/final verification boundary. */
|
|
51
|
+
export async function commitStagedMigrationBatch(files, finalize, operations = DEFAULT_OPERATIONS) {
|
|
52
|
+
const committed = await commitNewFilesAtomically(files, operations);
|
|
53
|
+
try {
|
|
54
|
+
await finalize();
|
|
55
|
+
return committed;
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
try {
|
|
59
|
+
await rollbackNewFiles(committed, operations);
|
|
60
|
+
}
|
|
61
|
+
catch (rollbackError) {
|
|
62
|
+
throw new AggregateError([error, rollbackError], 'CMS migration batch finalization failed and rollback was incomplete');
|
|
63
|
+
}
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=migration-vendor-files.js.map
|