@@ -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!");
0 commit comments