Skip to content

Commit 5964e5e

Browse files
committed
fix(ci,site): map uploaded dev imageAsset IDs onto dev documents and use sync mutations
1 parent 2920009 commit 5964e5e

2 files changed

Lines changed: 55 additions & 88 deletions

File tree

‎.github/workflows/nightly-dataset-sync.yml‎

Lines changed: 52 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,8 @@ jobs:
133133
}
134134
}
135135
136+
const prodToDevAssetMap = new Map();
137+
136138
function sanitizeDoc(obj) {
137139
if (!obj || typeof obj !== "object") return obj;
138140
if (Array.isArray(obj)) {
@@ -143,7 +145,6 @@ jobs:
143145
if (item && typeof item === "object" && typeof item._key === "string") {
144146
if (usedKeys.has(item._key)) {
145147
if (typeof item._ref === "string") {
146-
// Duplicate reference key in array: skip duplicate entry
147148
continue;
148149
}
149150
item._key = `${item._key}_${usedKeys.size}`;
@@ -160,6 +161,9 @@ jobs:
160161
copy[k] = sanitizeDoc(v);
161162
}
162163
if (typeof copy._ref === "string") {
164+
if (prodToDevAssetMap.has(copy._ref)) {
165+
copy._ref = prodToDevAssetMap.get(copy._ref);
166+
}
163167
if (allTargetIds.has(copy._ref)) {
164168
delete copy._weak;
165169
delete copy._strengthenOnPublish;
@@ -170,14 +174,11 @@ jobs:
170174
return copy;
171175
}
172176
173-
const sanitized = contentDocs.map(sanitizeDoc);
174-
console.log(`Prepared ${sanitized.length} content documents and ${syndicationStubs.size} syndication stubs for dataset "dev"...`);
175-
176177
let activeToken = tokens[0];
177178
async function mutateBatch(mutations, dataset = "dev") {
178179
for (let attempt = 0; attempt < 5; attempt++) {
179180
const token = activeToken || tokens[attempt % tokens.length];
180-
const res = await fetch(`https://${projectId}.api.sanity.io/v2021-06-07/data/mutate/${dataset}?visibility=deferred&autoGenerateArrayKeys=true`, {
181+
const res = await fetch(`https://${projectId}.api.sanity.io/v2021-06-07/data/mutate/${dataset}?visibility=sync&autoGenerateArrayKeys=true`, {
181182
method: "POST",
182183
headers: {
183184
Authorization: `Bearer ${token}`,
@@ -213,7 +214,7 @@ jobs:
213214
const MAX_BYTES = 2500000;
214215
215216
for (const item of items) {
216-
const mutation = { [op]: item };
217+
const mutation = op === "raw" ? item : { [op]: item };
217218
const itemBytes = Buffer.byteLength(JSON.stringify(mutation), "utf8");
218219
if (batch.length > 0 && (batch.length >= MAX_ITEMS || batchBytes + itemBytes > MAX_BYTES)) {
219220
await mutateBatch(batch);
@@ -235,19 +236,29 @@ jobs:
235236
}
236237
237238
async function uploadMissingDevImages() {
238-
const q = encodeURIComponent(`*[_type == "sanity.imageAsset" && path match "images/${projectId}/dev/*"]._id`);
239-
let devUploadedIds = new Set();
239+
const q = encodeURIComponent(`*[_type == "sanity.imageAsset" && string::startsWith(path, "images/${projectId}/dev/")]{_id, label}`);
240+
let devAssetDocs = [];
240241
for (const token of tokens) {
241242
const res = await fetch(`https://${projectId}.api.sanity.io/v2025-02-19/data/query/dev?query=${q}`, {
242243
headers: { Authorization: `Bearer ${token}` },
243244
});
244245
if (res.ok) {
245246
const json = await res.json();
246-
devUploadedIds = new Set(json.result || []);
247+
devAssetDocs = json.result || [];
247248
break;
248249
}
249250
}
250-
console.log(`Found ${devUploadedIds.size} image binaries already in 'dev' CDN bucket.`);
251+
252+
const devUploadedIds = new Set();
253+
for (const d of devAssetDocs) {
254+
devUploadedIds.add(d._id);
255+
allTargetIds.add(d._id);
256+
prodToDevAssetMap.set(d._id, d._id);
257+
if (typeof d.label === "string" && d.label.startsWith("image-")) {
258+
prodToDevAssetMap.set(d.label, d._id);
259+
}
260+
}
261+
console.log(`Found ${devUploadedIds.size} image binaries in 'dev' CDN bucket (${prodToDevAssetMap.size} mapped IDs).`);
251262
252263
const activeTypes = new Set(["post", "podcast", "guest", "author", "sponsor", "page", "settings", "podcastType"]);
253264
const priorityRefs = new Set();
@@ -271,79 +282,19 @@ jobs:
271282
}
272283
273284
const orderedRefs = [...new Set([...priorityRefs, ...inlineRefs])].filter(
274-
(id) => !devUploadedIds.has(id) && imageAssetMap.has(id)
285+
(id) => !prodToDevAssetMap.has(id) && imageAssetMap.has(id)
275286
);
276287
if (orderedRefs.length === 0) {
277-
console.log("All active image binaries are already present in 'dev' dataset.");
288+
console.log("All active image binaries are already present and mapped in 'dev' dataset.");
278289
return devUploadedIds;
279290
}
280291
281-
// Check if any of orderedRefs exist in 'dev' as metadata-only docs pointing to 'production' bucket
282-
// (Sanity's /assets/images/dev endpoint skips writing the binary if the document _id already exists in 'dev')
283-
const prodPathQuery = encodeURIComponent(`*[_type == "sanity.imageAsset" && path match "images/${projectId}/production/*"]._id`);
284-
let metadataOnlyIds = new Set();
285-
for (const token of tokens) {
286-
const res = await fetch(`https://${projectId}.api.sanity.io/v2025-02-19/data/query/dev?query=${prodPathQuery}`, {
287-
headers: { Authorization: `Bearer ${token}` },
288-
});
289-
if (res.ok) {
290-
const json = await res.json();
291-
metadataOnlyIds = new Set(json.result || []);
292-
break;
293-
}
294-
}
295-
296-
const toDeleteBeforeUpload = orderedRefs.filter((id) => metadataOnlyIds.has(id));
297-
if (toDeleteBeforeUpload.length > 0) {
298-
const toDeleteSet = new Set(toDeleteBeforeUpload);
299-
console.log(`Replacing ${toDeleteBeforeUpload.length} metadata-only asset docs with real 'dev' binaries...`);
300-
301-
function weakenTargetRefs(obj) {
302-
if (!obj || typeof obj !== "object") return { val: obj, hit: false };
303-
if (Array.isArray(obj)) {
304-
let anyHit = false;
305-
const arr = obj.map((item) => {
306-
const r = weakenTargetRefs(item);
307-
if (r.hit) anyHit = true;
308-
return r.val;
309-
});
310-
return { val: arr, hit: anyHit };
311-
}
312-
let anyHit = false;
313-
const copy = {};
314-
for (const [k, v] of Object.entries(obj)) {
315-
const r = weakenTargetRefs(v);
316-
if (r.hit) anyHit = true;
317-
copy[k] = r.val;
318-
}
319-
if (typeof copy._ref === "string" && toDeleteSet.has(copy._ref)) {
320-
copy._weak = true;
321-
anyHit = true;
322-
}
323-
return { val: copy, hit: anyHit };
324-
}
325-
326-
const weakenedDocs = [];
327-
for (const doc of sanitized) {
328-
const { val, hit } = weakenTargetRefs(doc);
329-
if (hit) weakenedDocs.push(val);
330-
}
331-
if (weakenedDocs.length > 0) {
332-
await sendChunked(weakenedDocs, "Pre-upload: Temporarily Weaken Asset Refs", "createOrReplace");
333-
}
334-
335-
for (let i = 0; i < toDeleteBeforeUpload.length; i += 200) {
336-
const delBatch = toDeleteBeforeUpload.slice(i, i + 200).map((id) => ({ delete: { id } }));
337-
await mutateBatch(delBatch, "dev");
338-
}
339-
console.log(`Deleted ${toDeleteBeforeUpload.length} metadata-only asset docs; ready for binary upload.`);
340-
}
341-
342-
console.log(`Uploading ${orderedRefs.length} missing active image binaries to 'dev' dataset...`);
292+
console.log(`Uploading/mapping ${orderedRefs.length} active image binaries to 'dev' dataset...`);
343293
344294
let uploaded = 0;
345295
let idx = 0;
346296
const CONCURRENCY = 20;
297+
const labelPatches = [];
347298
348299
async function worker() {
349300
while (idx < orderedRefs.length) {
@@ -359,7 +310,8 @@ jobs:
359310
const buf = Buffer.from(await imgRes.arrayBuffer());
360311
const token = activeToken || tokens[attempt % tokens.length];
361312
const filename = encodeURIComponent(assetDoc.originalFilename || `${assetId}.${assetDoc.extension || "jpg"}`);
362-
const upRes = await fetch(`https://${projectId}.api.sanity.io/v2021-06-07/assets/images/dev?filename=${filename}`, {
313+
const labelParam = encodeURIComponent(assetId);
314+
const upRes = await fetch(`https://${projectId}.api.sanity.io/v2021-06-07/assets/images/dev?filename=${filename}&label=${labelParam}`, {
363315
method: "POST",
364316
headers: {
365317
Authorization: `Bearer ${token}`,
@@ -369,10 +321,19 @@ jobs:
369321
});
370322
if (upRes.ok) {
371323
activeToken = token;
372-
devUploadedIds.add(assetId);
324+
const upJson = await upRes.json();
325+
const devId = upJson?.document?._id;
326+
if (devId) {
327+
devUploadedIds.add(devId);
328+
allTargetIds.add(devId);
329+
prodToDevAssetMap.set(assetId, devId);
330+
if (upJson.document.label !== assetId) {
331+
labelPatches.push({ patch: { id: devId, set: { label: assetId } } });
332+
}
333+
}
373334
uploaded++;
374335
if (uploaded % 100 === 0 || uploaded === orderedRefs.length) {
375-
console.log(`[Image Binaries] Uploaded ${uploaded} / ${orderedRefs.length}...`);
336+
console.log(`[Image Binaries] Mapped/uploaded ${uploaded} / ${orderedRefs.length}...`);
376337
}
377338
break;
378339
}
@@ -393,7 +354,12 @@ jobs:
393354
}
394355
395356
await Promise.all(Array.from({ length: CONCURRENCY }, () => worker()));
396-
console.log(`[Image Binaries] Finished uploading ${uploaded} image binaries to 'dev'.`);
357+
console.log(`[Image Binaries] Finished mapping/uploading ${uploaded} image binaries to 'dev'.`);
358+
359+
if (labelPatches.length > 0) {
360+
await sendChunked(labelPatches, "Image Asset Label Mapping Patches", "raw");
361+
}
362+
397363
return devUploadedIds;
398364
}
399365
@@ -408,25 +374,29 @@ jobs:
408374
}
409375
}
410376
411-
// Upload missing active image binaries to dev (creates real sanity.imageAsset docs with dev bucket paths)
377+
// 1. Upload/map missing active image binaries to dev (populates prodToDevAssetMap & allTargetIds)
412378
const devUploadedIds = await uploadMissingDevImages();
413379
414-
// Import remaining asset metadata docs (only those not already uploaded as dev binaries) so all asset refs resolve
380+
// 2. Now sanitize content documents so all active image _refs are rewritten to their real dev binary asset IDs
381+
const sanitized = contentDocs.map(sanitizeDoc);
382+
console.log(`Prepared ${sanitized.length} content documents and ${syndicationStubs.size} syndication stubs for dataset "dev"...`);
383+
384+
// 3. Import remaining asset metadata docs (for unreferenced/legacy assets) so all asset refs resolve
415385
const remainingAssetDocs = assetDocs
416386
.filter((d) => !devUploadedIds.has(d._id))
417387
.map(({ _rev, ...rest }) => rest);
418388
if (remainingAssetDocs.length > 0) {
419389
await sendChunked(remainingAssetDocs, "Pass 1a: Remaining Asset Metadata", "createIfNotExists");
420390
}
421391
422-
// Create content + syndication stubs so all internal document references resolve as strong references
392+
// 4. Create content + syndication stubs so all internal document references resolve as strong references
423393
const stubs = [
424394
...sanitized.map((d) => ({ _id: d._id, _type: d._type })),
425395
...Array.from(syndicationStubs.values()),
426396
];
427397
await sendChunked(stubs, "Pass 1b: Document & Syndication Stubs", "createIfNotExists");
428398
429-
// Populate full content documents (with strong coverImage.asset & syndications references and unique _keys)
399+
// 5. Populate full content documents (with strong coverImage.asset & syndications references and unique _keys)
430400
await sendChunked(sanitized, "Pass 2: Content Docs", "createOrReplace");
431401
432402
console.log("Dataset sync completed successfully!");

‎apps/site/src/lib/sanity/image.ts‎

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,11 @@ type Builder = ReturnType<typeof createImageUrlBuilder>;
99

1010
const builders = new Map<string, Builder>();
1111

12-
function builderFor({ projectId }: SanityConfig): Builder {
13-
// Image binary files synced from `production` to `dev` remain stored under
14-
// the `production` dataset path in Sanity's CDN (`images/<projectId>/production/...`).
15-
const imageDataset = "production";
16-
const key = `${projectId}:${imageDataset}`;
12+
function builderFor({ projectId, dataset }: SanityConfig): Builder {
13+
const key = `${projectId}:${dataset}`;
1714
let b = builders.get(key);
1815
if (!b) {
19-
b = createImageUrlBuilder({ projectId, dataset: imageDataset });
16+
b = createImageUrlBuilder({ projectId, dataset });
2017
builders.set(key, b);
2118
}
2219
return b;

0 commit comments

Comments
 (0)