emdash 0.28.0 → 0.28.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/dist/api/route-utils.mjs +2 -2
- package/dist/api/schemas/index.d.mts +1 -1
- package/dist/{api-BFURZn1i.mjs → api-DxFSorgk.mjs} +2 -2
- package/dist/{api-BFURZn1i.mjs.map → api-DxFSorgk.mjs.map} +1 -1
- package/dist/{apply-Bu-8nYHL.mjs → apply-CDJqIjPU.mjs} +8 -8
- package/dist/{apply-Bu-8nYHL.mjs.map → apply-CDJqIjPU.mjs.map} +1 -1
- package/dist/astro/index.d.mts +1 -1
- package/dist/astro/index.mjs +1 -1
- package/dist/astro/middleware/auth.d.mts +1 -1
- package/dist/astro/middleware.mjs +15 -15
- package/dist/astro/routes/api/admin/bylines/_id_/index.mjs +1 -1
- package/dist/astro/routes/api/admin/bylines/_id_/translations.mjs +1 -1
- package/dist/astro/routes/api/admin/bylines/index.mjs +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs +6 -6
- package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs +6 -6
- package/dist/astro/routes/api/admin/plugins/_id_/index.mjs +6 -6
- package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs +6 -6
- package/dist/astro/routes/api/admin/plugins/_id_/update.mjs +6 -6
- package/dist/astro/routes/api/admin/plugins/index.mjs +6 -6
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs +6 -6
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs +6 -6
- package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs +6 -6
- package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs +6 -6
- package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs +7 -7
- package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs +6 -6
- package/dist/astro/routes/api/admin/plugins/registry/install.mjs +7 -7
- package/dist/astro/routes/api/admin/plugins/updates.mjs +6 -6
- package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs +6 -6
- package/dist/astro/routes/api/admin/themes/marketplace/index.mjs +6 -6
- package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs +1 -1
- package/dist/astro/routes/api/import/wordpress/execute.d.mts +1 -1
- package/dist/astro/routes/api/import/wordpress/execute.mjs +2 -2
- package/dist/astro/routes/api/import/wordpress/media.d.mts +1 -1
- package/dist/astro/routes/api/import/wordpress/prepare.mjs +1 -1
- package/dist/astro/routes/api/import/wordpress/rewrite-urls.d.mts +1 -1
- package/dist/astro/routes/api/import/wordpress/rewrite-urls.mjs +2 -2
- package/dist/astro/routes/api/import/wordpress-plugin/execute.d.mts +1 -1
- package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs +3 -3
- package/dist/astro/routes/api/manifest.mjs +1 -1
- package/dist/astro/routes/api/mcp.mjs +14 -14
- package/dist/astro/routes/api/plugins/_pluginId_/_...path_.d.mts.map +1 -1
- package/dist/astro/routes/api/plugins/_pluginId_/_...path_.mjs +2 -10
- package/dist/astro/routes/api/plugins/_pluginId_/_...path_.mjs.map +1 -1
- package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs +6 -6
- package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs +6 -6
- package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs +6 -6
- package/dist/astro/routes/api/schema/collections/_slug_/index.mjs +6 -6
- package/dist/astro/routes/api/schema/collections/index.mjs +6 -6
- package/dist/astro/routes/api/schema/index.mjs +2 -2
- package/dist/astro/routes/api/schema/orphans/_slug_.mjs +6 -6
- package/dist/astro/routes/api/schema/orphans/index.mjs +6 -6
- package/dist/astro/routes/api/setup/dev-bypass.mjs +3 -3
- package/dist/astro/routes/api/setup/index.mjs +3 -3
- package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs +2 -2
- package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs +2 -2
- package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs +2 -2
- package/dist/astro/routes/api/taxonomies/index.mjs +2 -2
- package/dist/astro/routes/api/typegen.mjs +2 -2
- package/dist/astro/types.d.mts +1 -1
- package/dist/{byline-fields-Cgrt9-aL.d.mts → byline-fields-CHE8UStY.d.mts} +3 -3
- package/dist/{byline-fields-Cgrt9-aL.d.mts.map → byline-fields-CHE8UStY.d.mts.map} +1 -1
- package/dist/{bylines-Woq87129.mjs → bylines-CAZxGSrm.mjs} +2 -2
- package/dist/{bylines-Woq87129.mjs.map → bylines-CAZxGSrm.mjs.map} +1 -1
- package/dist/cli/index.mjs +3 -3
- package/dist/{content-refresh-CA4uKwzH.mjs → content-refresh-Br4gMzh8.mjs} +13 -7
- package/dist/content-refresh-Br4gMzh8.mjs.map +1 -0
- package/dist/{context-CUsR9dXL.mjs → context-BRIpHARC.mjs} +2 -2
- package/dist/{context-CUsR9dXL.mjs.map → context-BRIpHARC.mjs.map} +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +11 -11
- package/dist/media/local-runtime.d.mts +1 -1
- package/dist/{menus-CsZ2ZEK2.mjs → menus-b2T4clbO.mjs} +2 -2
- package/dist/{menus-CsZ2ZEK2.mjs.map → menus-b2T4clbO.mjs.map} +1 -1
- package/dist/plugin-utils.d.mts +1 -1
- package/dist/plugins/adapt-sandbox-entry.d.mts +1 -1
- package/dist/{query-ByGq6ver.mjs → query-1Cp6U9oo.mjs} +5 -5
- package/dist/{query-ByGq6ver.mjs.map → query-1Cp6U9oo.mjs.map} +1 -1
- package/dist/{registry-B65mUtPa.mjs → registry-B57vp_X9.mjs} +2 -2
- package/dist/{registry-B65mUtPa.mjs.map → registry-B57vp_X9.mjs.map} +1 -1
- package/dist/runtime.d.mts +1 -1
- package/dist/{schema-Blhoxp3P.mjs → schema-CX9cfNkZ.mjs} +2 -2
- package/dist/{schema-Blhoxp3P.mjs.map → schema-CX9cfNkZ.mjs.map} +1 -1
- package/dist/seed/index.mjs +3 -3
- package/dist/{taxonomies-BGBd16Eu.mjs → taxonomies-Bs13SN-F.mjs} +2 -2
- package/dist/{taxonomies-BGBd16Eu.mjs.map → taxonomies-Bs13SN-F.mjs.map} +1 -1
- package/dist/{taxonomies-Cnb4F-Pb.mjs → taxonomies-kH9ABDUF.mjs} +2 -2
- package/dist/{taxonomies-Cnb4F-Pb.mjs.map → taxonomies-kH9ABDUF.mjs.map} +1 -1
- package/dist/{validation-c8WjhH8z.mjs → validation-0l8-RYj4.mjs} +3 -3
- package/dist/{validation-c8WjhH8z.mjs.map → validation-0l8-RYj4.mjs.map} +1 -1
- package/dist/version-qmeO1XZY.mjs +7 -0
- package/dist/{version-CaccU7DM.mjs.map → version-qmeO1XZY.mjs.map} +1 -1
- package/dist/{wxr-taxonomies-BC3OYpvx.mjs → wxr-taxonomies-CNFcsNCr.mjs} +2 -2
- package/dist/{wxr-taxonomies-BC3OYpvx.mjs.map → wxr-taxonomies-CNFcsNCr.mjs.map} +1 -1
- package/package.json +5 -5
- package/src/astro/routes/api/plugins/[pluginId]/[...path].ts +17 -15
- package/src/database/repositories/media-usage.ts +25 -3
- package/src/media/usage/content-repair.ts +206 -22
- package/src/media/usage/content-snapshots.ts +4 -0
- package/dist/content-refresh-CA4uKwzH.mjs.map +0 -1
- package/dist/version-CaccU7DM.mjs +0 -7
|
@@ -496,7 +496,7 @@ export class MediaUsageRepository {
|
|
|
496
496
|
.executeTakeFirst();
|
|
497
497
|
deleted = Number(result.numDeletedRows ?? 0) > 0;
|
|
498
498
|
if (!deleted) return;
|
|
499
|
-
await
|
|
499
|
+
await this.deleteSourceGenerationOccurrences(trx, sourceKey, expectedCurrentGeneration);
|
|
500
500
|
});
|
|
501
501
|
|
|
502
502
|
return {
|
|
@@ -518,7 +518,11 @@ export class MediaUsageRepository {
|
|
|
518
518
|
.executeTakeFirst();
|
|
519
519
|
deleted = Number(result.numDeletedRows ?? 0) > 0;
|
|
520
520
|
if (!deleted) return;
|
|
521
|
-
await
|
|
521
|
+
await this.deleteSourceGenerationOccurrences(
|
|
522
|
+
trx,
|
|
523
|
+
sourceKey,
|
|
524
|
+
expectedSource.currentGeneration,
|
|
525
|
+
);
|
|
522
526
|
});
|
|
523
527
|
|
|
524
528
|
return {
|
|
@@ -547,7 +551,11 @@ export class MediaUsageRepository {
|
|
|
547
551
|
.executeTakeFirst();
|
|
548
552
|
deleted = Number(result.numDeletedRows ?? 0) > 0;
|
|
549
553
|
if (!deleted) return;
|
|
550
|
-
await
|
|
554
|
+
await this.deleteSourceGenerationOccurrences(
|
|
555
|
+
trx,
|
|
556
|
+
sourceKey,
|
|
557
|
+
expectedSource.currentGeneration,
|
|
558
|
+
);
|
|
551
559
|
});
|
|
552
560
|
const contentPresent = deleted ? false : await this.contentRowExists(tableName, contentId);
|
|
553
561
|
|
|
@@ -903,6 +911,20 @@ export class MediaUsageRepository {
|
|
|
903
911
|
});
|
|
904
912
|
}
|
|
905
913
|
|
|
914
|
+
private async deleteSourceGenerationOccurrences(
|
|
915
|
+
db: DatabaseExecutor,
|
|
916
|
+
sourceKey: string,
|
|
917
|
+
generation: string,
|
|
918
|
+
): Promise<void> {
|
|
919
|
+
// Guarded source deletes remove only the generation that won the source CAS;
|
|
920
|
+
// unmatched generations become invisible orphans reclaimed by age-gated cleanup.
|
|
921
|
+
await db
|
|
922
|
+
.deleteFrom("_emdash_media_usage")
|
|
923
|
+
.where("source_key", "=", sourceKey)
|
|
924
|
+
.where("generation", "=", generation)
|
|
925
|
+
.execute();
|
|
926
|
+
}
|
|
927
|
+
|
|
906
928
|
private async insertOccurrences(
|
|
907
929
|
db: DatabaseExecutor,
|
|
908
930
|
sourceKey: string,
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
} from "./content-refresh.js";
|
|
21
21
|
import {
|
|
22
22
|
CONTENT_SOURCE_SCHEMA_VERSION,
|
|
23
|
+
type ContentMediaUsageSnapshot,
|
|
23
24
|
loadContentMediaUsageSnapshots,
|
|
24
25
|
} from "./content-snapshots.js";
|
|
25
26
|
import {
|
|
@@ -65,11 +66,48 @@ export interface ContentMediaUsageRepairCollectionResult {
|
|
|
65
66
|
completedAt: string | null;
|
|
66
67
|
}
|
|
67
68
|
|
|
69
|
+
export interface ContentMediaUsageRepairAllResult {
|
|
70
|
+
status: ContentMediaUsageRepairStatus;
|
|
71
|
+
collections: ContentMediaUsageRepairCollectionResult[];
|
|
72
|
+
indexedSourceCount: number;
|
|
73
|
+
failedSourceCount: number;
|
|
74
|
+
skippedSourceCount: number;
|
|
75
|
+
deletedSourceCount: number;
|
|
76
|
+
}
|
|
77
|
+
|
|
68
78
|
export interface ContentMediaUsageCollectionScan {
|
|
69
79
|
collectionSlug: string;
|
|
70
80
|
contentIds: string[];
|
|
71
81
|
}
|
|
72
82
|
|
|
83
|
+
interface ContentMediaUsageCollectionRecord {
|
|
84
|
+
id: string;
|
|
85
|
+
slug: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
interface ContentMediaUsageInitialCollectionResult {
|
|
89
|
+
collection: ContentMediaUsageCollectionRecord;
|
|
90
|
+
result: ContentMediaUsageRepairCollectionResult;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function repairContentMediaUsageAll(
|
|
94
|
+
db: Kysely<Database>,
|
|
95
|
+
): Promise<ContentMediaUsageRepairAllResult> {
|
|
96
|
+
const collections = await loadContentMediaUsageCollectionRecords(db);
|
|
97
|
+
const results: ContentMediaUsageInitialCollectionResult[] = [];
|
|
98
|
+
|
|
99
|
+
for (const collection of collections) {
|
|
100
|
+
results.push({
|
|
101
|
+
collection,
|
|
102
|
+
result: await repairContentMediaUsageCollectionSafely(db, collection.slug),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return aggregateContentMediaUsageRepairAll(
|
|
107
|
+
await filterExistingContentMediaUsageCollectionResults(db, results),
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
73
111
|
export async function scanContentMediaUsageCollection(
|
|
74
112
|
db: Kysely<Database>,
|
|
75
113
|
collectionSlug: string,
|
|
@@ -105,6 +143,116 @@ export async function repairContentMediaUsageCollection(
|
|
|
105
143
|
);
|
|
106
144
|
}
|
|
107
145
|
|
|
146
|
+
async function loadContentMediaUsageCollectionRecords(
|
|
147
|
+
db: Kysely<Database>,
|
|
148
|
+
): Promise<ContentMediaUsageCollectionRecord[]> {
|
|
149
|
+
return db
|
|
150
|
+
.selectFrom("_emdash_collections")
|
|
151
|
+
.select(["id", "slug"])
|
|
152
|
+
.orderBy("slug", "asc")
|
|
153
|
+
.execute();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function repairContentMediaUsageCollectionSafely(
|
|
157
|
+
db: Kysely<Database>,
|
|
158
|
+
collectionSlug: string,
|
|
159
|
+
): Promise<ContentMediaUsageRepairCollectionResult> {
|
|
160
|
+
try {
|
|
161
|
+
return await repairContentMediaUsageCollection(db, { collectionSlug });
|
|
162
|
+
} catch (error) {
|
|
163
|
+
console.error(`[media-usage] Failed to repair collection ${collectionSlug}:`, error);
|
|
164
|
+
const now = new Date().toISOString();
|
|
165
|
+
return {
|
|
166
|
+
scope: contentMediaUsageCollectionScope(collectionSlug),
|
|
167
|
+
status: "failed",
|
|
168
|
+
indexedSourceCount: 0,
|
|
169
|
+
failedSourceCount: 0,
|
|
170
|
+
skippedSourceCount: 0,
|
|
171
|
+
deletedSourceCount: 0,
|
|
172
|
+
lastErrorCode: CONTENT_MEDIA_USAGE_REPAIR_ERROR.CONTENT_USAGE_REPAIR_ERROR,
|
|
173
|
+
startedAt: now,
|
|
174
|
+
completedAt: now,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function filterExistingContentMediaUsageCollectionResults(
|
|
180
|
+
db: Kysely<Database>,
|
|
181
|
+
results: readonly ContentMediaUsageInitialCollectionResult[],
|
|
182
|
+
): Promise<ContentMediaUsageRepairCollectionResult[]> {
|
|
183
|
+
const currentCollections = await loadContentMediaUsageCollectionRecordsSafely(db);
|
|
184
|
+
const currentIdsBySlug = new Map(
|
|
185
|
+
currentCollections.map((collection) => [collection.slug, collection.id]),
|
|
186
|
+
);
|
|
187
|
+
const includedResults: ContentMediaUsageRepairCollectionResult[] = [];
|
|
188
|
+
const excludedResults: ContentMediaUsageRepairCollectionResult[] = [];
|
|
189
|
+
|
|
190
|
+
for (const { collection, result } of results) {
|
|
191
|
+
if (currentIdsBySlug.get(collection.slug) === collection.id) {
|
|
192
|
+
includedResults.push(result);
|
|
193
|
+
} else {
|
|
194
|
+
excludedResults.push(result);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (excludedResults.length > 0) {
|
|
199
|
+
const repo = new MediaUsageRepository(db);
|
|
200
|
+
for (const result of excludedResults) {
|
|
201
|
+
await repo.deleteIndexStatus(result.scope);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return includedResults;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function loadContentMediaUsageCollectionRecordsSafely(
|
|
209
|
+
db: Kysely<Database>,
|
|
210
|
+
): Promise<ContentMediaUsageCollectionRecord[]> {
|
|
211
|
+
try {
|
|
212
|
+
return await loadContentMediaUsageCollectionRecords(db);
|
|
213
|
+
} catch {
|
|
214
|
+
// Retry once before failing; returning unpruned results can over-report deleted collections.
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
return await loadContentMediaUsageCollectionRecords(db);
|
|
219
|
+
} catch (error) {
|
|
220
|
+
console.error("[media-usage] Failed to reconcile all-content repair collections:", error);
|
|
221
|
+
throw error;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function aggregateContentMediaUsageRepairAll(
|
|
226
|
+
collections: readonly ContentMediaUsageRepairCollectionResult[],
|
|
227
|
+
): ContentMediaUsageRepairAllResult {
|
|
228
|
+
return {
|
|
229
|
+
status: determineRepairAllStatus(collections),
|
|
230
|
+
collections: [...collections],
|
|
231
|
+
indexedSourceCount: sumCollectionRepairCount(collections, "indexedSourceCount"),
|
|
232
|
+
failedSourceCount: sumCollectionRepairCount(collections, "failedSourceCount"),
|
|
233
|
+
skippedSourceCount: sumCollectionRepairCount(collections, "skippedSourceCount"),
|
|
234
|
+
deletedSourceCount: sumCollectionRepairCount(collections, "deletedSourceCount"),
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function determineRepairAllStatus(
|
|
239
|
+
collections: readonly ContentMediaUsageRepairCollectionResult[],
|
|
240
|
+
): ContentMediaUsageRepairStatus {
|
|
241
|
+
if (collections.length === 0) return "complete";
|
|
242
|
+
if (collections.every((collection) => collection.status === "complete")) return "complete";
|
|
243
|
+
if (collections.some((collection) => collection.status === "stale")) return "stale";
|
|
244
|
+
if (collections.some((collection) => collection.status === "partial")) return "partial";
|
|
245
|
+
if (collections.every((collection) => collection.status === "failed")) return "failed";
|
|
246
|
+
return "partial";
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function sumCollectionRepairCount(
|
|
250
|
+
collections: readonly ContentMediaUsageRepairCollectionResult[],
|
|
251
|
+
key: "indexedSourceCount" | "failedSourceCount" | "skippedSourceCount" | "deletedSourceCount",
|
|
252
|
+
): number {
|
|
253
|
+
return collections.reduce((sum, collection) => sum + collection[key], 0);
|
|
254
|
+
}
|
|
255
|
+
|
|
108
256
|
async function repairContentMediaUsageCollectionUnlocked(
|
|
109
257
|
db: Kysely<Database>,
|
|
110
258
|
collectionSlug: string,
|
|
@@ -147,6 +295,7 @@ async function repairContentMediaUsageCollectionUnlocked(
|
|
|
147
295
|
skippedSourceCount: 0,
|
|
148
296
|
deletedSourceCount: 0,
|
|
149
297
|
lastErrorCode: CONTENT_MEDIA_USAGE_REPAIR_ERROR.COLLECTION_NOT_FOUND,
|
|
298
|
+
missingContentIds: new Set(),
|
|
150
299
|
},
|
|
151
300
|
status: "failed",
|
|
152
301
|
startedAt,
|
|
@@ -155,16 +304,14 @@ async function repairContentMediaUsageCollectionUnlocked(
|
|
|
155
304
|
}
|
|
156
305
|
const counts = await repairScannedContentSources(db, repo, scan);
|
|
157
306
|
const finalScan = await scanContentMediaUsageCollection(db, collectionSlug);
|
|
158
|
-
const scannedContentCount = finalScan?.contentIds.length ?? scan.contentIds.length;
|
|
159
307
|
if (!finalScan) {
|
|
160
308
|
counts.failedSourceCount++;
|
|
161
309
|
counts.lastErrorCode = CONTENT_MEDIA_USAGE_REPAIR_ERROR.COLLECTION_NOT_FOUND;
|
|
162
|
-
} else if (!sameContentIds(scan.contentIds, finalScan.contentIds)) {
|
|
163
|
-
counts
|
|
164
|
-
counts.lastErrorCode = CONTENT_MEDIA_USAGE_REPAIR_ERROR.CONTENT_USAGE_REPAIR_CONFLICT;
|
|
310
|
+
} else if (!sameContentIds(repairedContentIds(scan.contentIds, counts), finalScan.contentIds)) {
|
|
311
|
+
markRepairConflict(counts);
|
|
165
312
|
}
|
|
166
313
|
const completedAt = new Date().toISOString();
|
|
167
|
-
const status = determineRepairStatus(counts
|
|
314
|
+
const status = determineRepairStatus(counts);
|
|
168
315
|
return await finalizeRepairStatus(repo, {
|
|
169
316
|
...scope,
|
|
170
317
|
runToken,
|
|
@@ -191,6 +338,7 @@ async function repairContentMediaUsageCollectionUnlocked(
|
|
|
191
338
|
skippedSourceCount: 0,
|
|
192
339
|
deletedSourceCount: 0,
|
|
193
340
|
lastErrorCode,
|
|
341
|
+
missingContentIds: new Set(),
|
|
194
342
|
},
|
|
195
343
|
status: "failed",
|
|
196
344
|
startedAt,
|
|
@@ -205,6 +353,7 @@ interface RepairCounts {
|
|
|
205
353
|
skippedSourceCount: number;
|
|
206
354
|
deletedSourceCount: number;
|
|
207
355
|
lastErrorCode: ContentMediaUsageRepairErrorCode | null;
|
|
356
|
+
missingContentIds: Set<string>;
|
|
208
357
|
}
|
|
209
358
|
|
|
210
359
|
async function repairScannedContentSources(
|
|
@@ -218,6 +367,7 @@ async function repairScannedContentSources(
|
|
|
218
367
|
skippedSourceCount: 0,
|
|
219
368
|
deletedSourceCount: 0,
|
|
220
369
|
lastErrorCode: null,
|
|
370
|
+
missingContentIds: new Set(),
|
|
221
371
|
};
|
|
222
372
|
|
|
223
373
|
const fieldDiscovery = await loadContentMediaUsageFields(db, scan.collectionSlug);
|
|
@@ -256,7 +406,16 @@ async function repairContentSource(
|
|
|
256
406
|
fieldDiscovery,
|
|
257
407
|
);
|
|
258
408
|
if (!snapshotsResult.success) {
|
|
409
|
+
if (snapshotsResult.error === CONTENT_MEDIA_USAGE_REPAIR_ERROR.CONTENT_NOT_FOUND) {
|
|
410
|
+
markRepairConflict(counts);
|
|
411
|
+
counts.missingContentIds.add(contentId);
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
259
414
|
counts.lastErrorCode = snapshotsResult.error;
|
|
415
|
+
if (snapshotsResult.snapshots) {
|
|
416
|
+
// Partial snapshot failures keep absent variants in place; the attempted source is marked below.
|
|
417
|
+
await repairSnapshotSources(repo, snapshotsResult.snapshots, observedSources, counts);
|
|
418
|
+
}
|
|
260
419
|
if (snapshotsResult.source) {
|
|
261
420
|
const result = await repo.markSourceAttemptedIfMatching(
|
|
262
421
|
{
|
|
@@ -269,8 +428,7 @@ async function repairContentSource(
|
|
|
269
428
|
if (result.attempted) {
|
|
270
429
|
counts.failedSourceCount++;
|
|
271
430
|
} else {
|
|
272
|
-
counts
|
|
273
|
-
counts.lastErrorCode = CONTENT_MEDIA_USAGE_REPAIR_ERROR.CONTENT_USAGE_REPAIR_CONFLICT;
|
|
431
|
+
markRepairConflict(counts);
|
|
274
432
|
}
|
|
275
433
|
return;
|
|
276
434
|
}
|
|
@@ -279,8 +437,29 @@ async function repairContentSource(
|
|
|
279
437
|
return;
|
|
280
438
|
}
|
|
281
439
|
|
|
440
|
+
const expectedSourceKeys = await repairSnapshotSources(
|
|
441
|
+
repo,
|
|
442
|
+
snapshotsResult.snapshots,
|
|
443
|
+
observedSources,
|
|
444
|
+
counts,
|
|
445
|
+
);
|
|
446
|
+
|
|
447
|
+
for (const sourceKey of sourceKeys) {
|
|
448
|
+
if (expectedSourceKeys.has(sourceKey)) continue;
|
|
449
|
+
const observedSource = observedSources.get(sourceKey);
|
|
450
|
+
if (!observedSource) continue;
|
|
451
|
+
await deleteObservedSource(repo, sourceKey, observedSource, counts);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
async function repairSnapshotSources(
|
|
456
|
+
repo: MediaUsageRepository,
|
|
457
|
+
snapshots: readonly ContentMediaUsageSnapshot[],
|
|
458
|
+
observedSources: Map<string, MediaUsageSource>,
|
|
459
|
+
counts: RepairCounts,
|
|
460
|
+
): Promise<Set<string>> {
|
|
282
461
|
const expectedSourceKeys = new Set<string>();
|
|
283
|
-
for (const snapshot of
|
|
462
|
+
for (const snapshot of snapshots) {
|
|
284
463
|
expectedSourceKeys.add(snapshot.source.sourceKey);
|
|
285
464
|
const result = await repo.replaceSourceIfMatching(
|
|
286
465
|
snapshot.source,
|
|
@@ -290,17 +469,15 @@ async function repairContentSource(
|
|
|
290
469
|
if (result.replaced) {
|
|
291
470
|
counts.indexedSourceCount++;
|
|
292
471
|
} else {
|
|
293
|
-
counts
|
|
294
|
-
counts.lastErrorCode = CONTENT_MEDIA_USAGE_REPAIR_ERROR.CONTENT_USAGE_REPAIR_CONFLICT;
|
|
472
|
+
markRepairConflict(counts);
|
|
295
473
|
}
|
|
296
474
|
}
|
|
475
|
+
return expectedSourceKeys;
|
|
476
|
+
}
|
|
297
477
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
if (!observedSource) continue;
|
|
302
|
-
await deleteObservedSource(repo, sourceKey, observedSource, counts);
|
|
303
|
-
}
|
|
478
|
+
function markRepairConflict(counts: RepairCounts): void {
|
|
479
|
+
counts.skippedSourceCount++;
|
|
480
|
+
counts.lastErrorCode ??= CONTENT_MEDIA_USAGE_REPAIR_ERROR.CONTENT_USAGE_REPAIR_CONFLICT;
|
|
304
481
|
}
|
|
305
482
|
|
|
306
483
|
async function reconcileOrphanedContentSources(
|
|
@@ -370,8 +547,7 @@ async function deleteObservedSourceIfContentAbsent(
|
|
|
370
547
|
}
|
|
371
548
|
if (result.contentPresent) return;
|
|
372
549
|
if (result.source) {
|
|
373
|
-
counts
|
|
374
|
-
counts.lastErrorCode = CONTENT_MEDIA_USAGE_REPAIR_ERROR.CONTENT_USAGE_REPAIR_CONFLICT;
|
|
550
|
+
markRepairConflict(counts);
|
|
375
551
|
}
|
|
376
552
|
}
|
|
377
553
|
|
|
@@ -387,8 +563,7 @@ async function deleteObservedSource(
|
|
|
387
563
|
return;
|
|
388
564
|
}
|
|
389
565
|
if (result.source) {
|
|
390
|
-
counts
|
|
391
|
-
counts.lastErrorCode = CONTENT_MEDIA_USAGE_REPAIR_ERROR.CONTENT_USAGE_REPAIR_CONFLICT;
|
|
566
|
+
markRepairConflict(counts);
|
|
392
567
|
}
|
|
393
568
|
}
|
|
394
569
|
|
|
@@ -439,11 +614,12 @@ async function finalizeRepairStatus(
|
|
|
439
614
|
|
|
440
615
|
function determineRepairStatus(
|
|
441
616
|
counts: RepairCounts,
|
|
442
|
-
scannedContentCount: number,
|
|
443
617
|
): Exclude<ContentMediaUsageRepairStatus, "stale"> {
|
|
444
618
|
if (counts.failedSourceCount === 0 && counts.skippedSourceCount === 0) return "complete";
|
|
445
619
|
const trustedProgress = counts.indexedSourceCount + counts.deletedSourceCount;
|
|
446
|
-
if (
|
|
620
|
+
if (counts.failedSourceCount > 0 && trustedProgress === 0) {
|
|
621
|
+
return "failed";
|
|
622
|
+
}
|
|
447
623
|
return "partial";
|
|
448
624
|
}
|
|
449
625
|
|
|
@@ -477,6 +653,14 @@ function sameContentIds(left: readonly string[], right: readonly string[]): bool
|
|
|
477
653
|
return left.every((id) => rightIds.has(id));
|
|
478
654
|
}
|
|
479
655
|
|
|
656
|
+
function repairedContentIds(
|
|
657
|
+
contentIds: readonly string[],
|
|
658
|
+
counts: Pick<RepairCounts, "missingContentIds">,
|
|
659
|
+
): string[] {
|
|
660
|
+
if (counts.missingContentIds.size === 0) return [...contentIds];
|
|
661
|
+
return contentIds.filter((contentId) => !counts.missingContentIds.has(contentId));
|
|
662
|
+
}
|
|
663
|
+
|
|
480
664
|
function contentMediaUsageCollectionScope(collectionSlug: string): ContentMediaUsageRepairScope {
|
|
481
665
|
return {
|
|
482
666
|
adapterId: CONTENT_MEDIA_USAGE_ADAPTER_ID,
|
|
@@ -46,6 +46,7 @@ export type LoadContentMediaUsageSnapshotsResult =
|
|
|
46
46
|
| "DRAFT_REVISION_MISMATCH"
|
|
47
47
|
| "DRAFT_REVISION_INVALID";
|
|
48
48
|
source?: MediaUsageSourceInput;
|
|
49
|
+
snapshots?: ContentMediaUsageSnapshot[];
|
|
49
50
|
};
|
|
50
51
|
|
|
51
52
|
export interface ContentMediaUsageSnapshot {
|
|
@@ -116,6 +117,7 @@ export async function loadContentMediaUsageSnapshots(
|
|
|
116
117
|
success: false,
|
|
117
118
|
error: "DRAFT_REVISION_NOT_FOUND",
|
|
118
119
|
source: attemptedDraftSource,
|
|
120
|
+
snapshots,
|
|
119
121
|
};
|
|
120
122
|
}
|
|
121
123
|
if (!revisionResult.success) {
|
|
@@ -123,6 +125,7 @@ export async function loadContentMediaUsageSnapshots(
|
|
|
123
125
|
success: false,
|
|
124
126
|
error: "DRAFT_REVISION_INVALID",
|
|
125
127
|
source: attemptedDraftSource,
|
|
128
|
+
snapshots,
|
|
126
129
|
};
|
|
127
130
|
}
|
|
128
131
|
const revision = revisionResult.revision;
|
|
@@ -131,6 +134,7 @@ export async function loadContentMediaUsageSnapshots(
|
|
|
131
134
|
success: false,
|
|
132
135
|
error: "DRAFT_REVISION_MISMATCH",
|
|
133
136
|
source: attemptedDraftSource,
|
|
137
|
+
snapshots,
|
|
134
138
|
};
|
|
135
139
|
}
|
|
136
140
|
|