@se-studio/project-build 1.0.68 → 1.0.71
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/CHANGELOG.md +18 -0
- package/package.json +2 -4
- package/skills/se-marketing-sites/create-collection/SKILL.md +14 -10
- package/skills/se-marketing-sites/create-component/SKILL.md +1 -1
- package/skills/se-marketing-sites/handling-media/SKILL.md +19 -0
- package/skills/se-marketing-sites/lib-cms-structure/SKILL.md +8 -6
- package/skills/se-marketing-sites/register-cms-features/SKILL.md +15 -15
- package/skills/se-marketing-sites/styling-system/SKILL.md +1 -1
- package/dist/management/check-target.d.ts +0 -2
- package/dist/management/check-target.d.ts.map +0 -1
- package/dist/management/check-target.js +0 -40
- package/dist/management/check-target.js.map +0 -1
- package/dist/management/count-brightline.d.ts +0 -2
- package/dist/management/count-brightline.d.ts.map +0 -1
- package/dist/management/count-brightline.js +0 -37
- package/dist/management/count-brightline.js.map +0 -1
- package/dist/management/cross-check-migration.d.ts +0 -2
- package/dist/management/cross-check-migration.d.ts.map +0 -1
- package/dist/management/cross-check-migration.js +0 -212
- package/dist/management/cross-check-migration.js.map +0 -1
- package/dist/management/discovery-brightlife.d.ts +0 -2
- package/dist/management/discovery-brightlife.d.ts.map +0 -1
- package/dist/management/discovery-brightlife.js +0 -78
- package/dist/management/discovery-brightlife.js.map +0 -1
- package/dist/management/discovery-brightline.d.ts +0 -2
- package/dist/management/discovery-brightline.d.ts.map +0 -1
- package/dist/management/discovery-brightline.js +0 -68
- package/dist/management/discovery-brightline.js.map +0 -1
- package/dist/management/generateVisibilityJson.d.ts +0 -3
- package/dist/management/generateVisibilityJson.d.ts.map +0 -1
- package/dist/management/generateVisibilityJson.js +0 -149
- package/dist/management/generateVisibilityJson.js.map +0 -1
- package/dist/management/inspect-target.d.ts +0 -2
- package/dist/management/inspect-target.d.ts.map +0 -1
- package/dist/management/inspect-target.js +0 -48
- package/dist/management/inspect-target.js.map +0 -1
- package/dist/management/migrate-brightlife.d.ts +0 -2
- package/dist/management/migrate-brightlife.d.ts.map +0 -1
- package/dist/management/migrate-brightlife.js +0 -475
- package/dist/management/migrate-brightlife.js.map +0 -1
- package/dist/management/migrate-brightline.d.ts +0 -2
- package/dist/management/migrate-brightline.d.ts.map +0 -1
- package/dist/management/migrate-brightline.js +0 -1085
- package/dist/management/migrate-brightline.js.map +0 -1
|
@@ -1,1085 +0,0 @@
|
|
|
1
|
-
import { isDeepStrictEqual } from 'node:util';
|
|
2
|
-
import contentfulManagement from 'contentful-management';
|
|
3
|
-
import dotenv from 'dotenv';
|
|
4
|
-
dotenv.config();
|
|
5
|
-
const SPACE_ID = '96gdpqkm7elu';
|
|
6
|
-
const SOURCE_ENV_ID = 'cleanup';
|
|
7
|
-
const TARGET_ENV_ID = 'production-2026';
|
|
8
|
-
const DRY_RUN = process.argv.includes('--dry-run');
|
|
9
|
-
const stats = {
|
|
10
|
-
assets: { created: 0, skipped: 0, errors: 0 },
|
|
11
|
-
entries: { created: 0, skipped: 0, errors: 0 },
|
|
12
|
-
};
|
|
13
|
-
// ID Mapping: Old ID -> New ID
|
|
14
|
-
const idMap = new Map();
|
|
15
|
-
async function getEnvironment(spaceId, envId) {
|
|
16
|
-
const accessToken = process.env.CONTENTFUL_MANAGEMENT_TOKEN;
|
|
17
|
-
if (!accessToken) {
|
|
18
|
-
throw new Error('CONTENTFUL_MANAGEMENT_TOKEN is not set');
|
|
19
|
-
}
|
|
20
|
-
const client = contentfulManagement.createClient({ accessToken });
|
|
21
|
-
const space = await client.getSpace(spaceId);
|
|
22
|
-
return space.getEnvironment(envId);
|
|
23
|
-
}
|
|
24
|
-
// --- Helpers ---
|
|
25
|
-
// Cache for valid values to avoid repeated API calls
|
|
26
|
-
const validValuesCache = {};
|
|
27
|
-
async function ensureValidValue(environment, contentTypeId, fieldId, value) {
|
|
28
|
-
if (!value)
|
|
29
|
-
return;
|
|
30
|
-
const cacheKey = `${contentTypeId}:${fieldId}`;
|
|
31
|
-
// Initial Cache Population
|
|
32
|
-
if (!validValuesCache[cacheKey]) {
|
|
33
|
-
try {
|
|
34
|
-
const contentType = await environment.getContentType(contentTypeId);
|
|
35
|
-
const field = contentType.fields.find((f) => f.id === fieldId);
|
|
36
|
-
const validations = field?.validations || [];
|
|
37
|
-
const inValidation = validations.find((v) => v.in);
|
|
38
|
-
validValuesCache[cacheKey] = (inValidation ? inValidation.in : []);
|
|
39
|
-
}
|
|
40
|
-
catch (e) {
|
|
41
|
-
console.warn(`Could not fetch validation for ${cacheKey}`, e);
|
|
42
|
-
validValuesCache[cacheKey] = [];
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
const currentValues = validValuesCache[cacheKey];
|
|
46
|
-
if (currentValues && !currentValues.includes(value)) {
|
|
47
|
-
if (DRY_RUN) {
|
|
48
|
-
console.log(`[Dry Run] Would update ${contentTypeId}.${fieldId} validations to include: ${value}`);
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
console.log(`Updating ${contentTypeId}.${fieldId} validations to include: ${value}`);
|
|
52
|
-
try {
|
|
53
|
-
let contentType = await environment.getContentType(contentTypeId);
|
|
54
|
-
const field = contentType.fields.find((f) => f.id === fieldId);
|
|
55
|
-
const inValidation = field?.validations?.find((v) => v.in);
|
|
56
|
-
// Double check if value is already there (race condition or cache stale)
|
|
57
|
-
if (inValidation?.in?.includes(value)) {
|
|
58
|
-
validValuesCache[cacheKey].push(value);
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
if (inValidation && inValidation.in) {
|
|
62
|
-
inValidation.in.push(value);
|
|
63
|
-
contentType = await contentType.update();
|
|
64
|
-
await contentType.publish();
|
|
65
|
-
validValuesCache[cacheKey].push(value);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
catch (e) {
|
|
69
|
-
console.error(`Failed to update validation for ${cacheKey}:`, e);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
function getWrapperId(assetId) {
|
|
74
|
-
return `visual-${assetId}`;
|
|
75
|
-
}
|
|
76
|
-
async function ensureWrapperEntry(targetEnv, assetId, title = 'Visual Wrapper', targetAssetsMap) {
|
|
77
|
-
const wrapperId = getWrapperId(assetId);
|
|
78
|
-
// Check if Asset exists in Target (Fail-Fast)
|
|
79
|
-
if (targetAssetsMap) {
|
|
80
|
-
const targetAsset = targetAssetsMap.get(assetId);
|
|
81
|
-
// We also check if it is published?
|
|
82
|
-
// The map might contain drafts.
|
|
83
|
-
// But if we ran `migrateAssets`, we ensured publication.
|
|
84
|
-
if (!targetAsset) {
|
|
85
|
-
throw new Error(`Cannot create wrapper ${wrapperId}. Asset ${assetId} not found in target.`);
|
|
86
|
-
}
|
|
87
|
-
// If we want to be strict about publication:
|
|
88
|
-
if (!targetAsset.sys.publishedVersion && !targetAsset.sys.archivedVersion) {
|
|
89
|
-
// It might have just been created and published in `migrateAssets`,
|
|
90
|
-
// but the map object might be stale if we didn't update it with the published version?
|
|
91
|
-
// `createAssetWithId` returns the created asset. We updated the map.
|
|
92
|
-
// `publish()` returns the published asset. We didn't update the map with that result in previous logic.
|
|
93
|
-
// However, simple existence check is better than failing if we aren't sure.
|
|
94
|
-
// The error `notResolvable` comes from Contentful validation.
|
|
95
|
-
// Let's assume `migrateAssets` did its job.
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
if (DRY_RUN) {
|
|
99
|
-
console.log(`[Dry Run] Would ensure wrapper entry ${wrapperId} for asset ${assetId}`);
|
|
100
|
-
idMap.set(wrapperId, wrapperId);
|
|
101
|
-
return wrapperId;
|
|
102
|
-
}
|
|
103
|
-
try {
|
|
104
|
-
try {
|
|
105
|
-
await targetEnv.getEntry(wrapperId);
|
|
106
|
-
}
|
|
107
|
-
catch (e) {
|
|
108
|
-
console.log(`Creating wrapper entry ${wrapperId} for asset ${assetId}`);
|
|
109
|
-
const entry = await targetEnv.createEntryWithId('media', wrapperId, {
|
|
110
|
-
fields: {
|
|
111
|
-
name: { 'en-US': title },
|
|
112
|
-
asset: { 'en-US': { sys: createLink(assetId, 'Asset').sys } },
|
|
113
|
-
},
|
|
114
|
-
});
|
|
115
|
-
await entry.publish();
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
catch (error) {
|
|
119
|
-
console.error(`Failed to ensure wrapper ${wrapperId}:`, error);
|
|
120
|
-
throw error; // Fail fast
|
|
121
|
-
}
|
|
122
|
-
return wrapperId;
|
|
123
|
-
}
|
|
124
|
-
// Helper to get or create Article Type 'Blog'
|
|
125
|
-
async function getBlogArticleTypeId(env) {
|
|
126
|
-
// Try to find the Blog article type by name "Blog"
|
|
127
|
-
try {
|
|
128
|
-
const entries = await env.getEntries({
|
|
129
|
-
content_type: 'articleType',
|
|
130
|
-
'fields.name[match]': 'Blog',
|
|
131
|
-
limit: 1,
|
|
132
|
-
});
|
|
133
|
-
if (entries.items.length > 0) {
|
|
134
|
-
console.log(`Found existing Blog Article Type: ${entries.items[0].sys.id}`);
|
|
135
|
-
return entries.items[0].sys.id;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
catch (e) {
|
|
139
|
-
console.warn('Error searching for Blog Article Type:', e);
|
|
140
|
-
}
|
|
141
|
-
// Create if not found
|
|
142
|
-
console.log('Creating Blog Article Type...');
|
|
143
|
-
try {
|
|
144
|
-
const newEntry = await env.createEntry('articleType', {
|
|
145
|
-
fields: {
|
|
146
|
-
name: { 'en-US': 'Blog' },
|
|
147
|
-
cmsLabel: { 'en-US': 'Blog' },
|
|
148
|
-
slug: { 'en-US': 'blogs' },
|
|
149
|
-
indexPageTitle: { 'en-US': 'Blog' },
|
|
150
|
-
indexPageDescription: { 'en-US': 'Blog Index' },
|
|
151
|
-
indexed: { 'en-US': true },
|
|
152
|
-
hidden: { 'en-US': false },
|
|
153
|
-
},
|
|
154
|
-
});
|
|
155
|
-
console.log(`Created Blog Article Type: ${newEntry.sys.id}`);
|
|
156
|
-
return newEntry.sys.id;
|
|
157
|
-
}
|
|
158
|
-
catch (e) {
|
|
159
|
-
console.error('Failed to create Blog Article Type:', e);
|
|
160
|
-
return '';
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
// Helper to deep compare Contentful fields while ignoring sys metadata in links
|
|
164
|
-
function isEqualFields(obj1, obj2) {
|
|
165
|
-
const normalize = (obj) => {
|
|
166
|
-
if (!obj || typeof obj !== 'object')
|
|
167
|
-
return obj;
|
|
168
|
-
if (Array.isArray(obj))
|
|
169
|
-
return obj.map(normalize);
|
|
170
|
-
if (obj.sys && obj.sys.type === 'Link') {
|
|
171
|
-
return {
|
|
172
|
-
sys: {
|
|
173
|
-
type: 'Link',
|
|
174
|
-
linkType: obj.sys.linkType,
|
|
175
|
-
id: obj.sys.id,
|
|
176
|
-
},
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
const res = {};
|
|
180
|
-
for (const key of Object.keys(obj).sort()) {
|
|
181
|
-
if (obj[key] === undefined)
|
|
182
|
-
continue;
|
|
183
|
-
res[key] = normalize(obj[key]);
|
|
184
|
-
}
|
|
185
|
-
return res;
|
|
186
|
-
};
|
|
187
|
-
return isDeepStrictEqual(normalize(obj1), normalize(obj2));
|
|
188
|
-
}
|
|
189
|
-
// Helper to update entry with retry logic
|
|
190
|
-
async function updateEntryWithRetry(env, id, contentType, fields, retries = 3) {
|
|
191
|
-
try {
|
|
192
|
-
let entry;
|
|
193
|
-
try {
|
|
194
|
-
entry = await env.getEntry(id);
|
|
195
|
-
if (isEqualFields(entry.fields, fields)) {
|
|
196
|
-
// console.log(`Entry ${id} is up to date. Skipping update.`);
|
|
197
|
-
return entry;
|
|
198
|
-
}
|
|
199
|
-
console.log(`Entry ${id} exists. Updating...`);
|
|
200
|
-
entry.fields = fields;
|
|
201
|
-
return await entry.update();
|
|
202
|
-
}
|
|
203
|
-
catch (e) {
|
|
204
|
-
if (e.name === 'NotFound') {
|
|
205
|
-
console.log(`Creating ${contentType} (${id})`);
|
|
206
|
-
return await env.createEntryWithId(contentType, id, { fields });
|
|
207
|
-
}
|
|
208
|
-
throw e;
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
catch (error) {
|
|
212
|
-
if (error.name === 'VersionMismatch' && retries > 0) {
|
|
213
|
-
console.warn(`VersionMismatch for ${id}. Retrying... (${retries} left)`);
|
|
214
|
-
return updateEntryWithRetry(env, id, contentType, fields, retries - 1);
|
|
215
|
-
}
|
|
216
|
-
throw error; // Re-throw to fail fast
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
// --- Asset Migration ---
|
|
220
|
-
async function migrateAssets(sourceAssets, targetEnv, targetAssetsMap) {
|
|
221
|
-
console.log('\n--- Migrating Assets ---');
|
|
222
|
-
console.log(`Processing ${sourceAssets.length} source assets (reachable only).`);
|
|
223
|
-
for (const asset of sourceAssets) {
|
|
224
|
-
const assetId = asset.sys.id;
|
|
225
|
-
try {
|
|
226
|
-
const targetAsset = targetAssetsMap.get(assetId);
|
|
227
|
-
if (targetAsset) {
|
|
228
|
-
// Asset exists. Should we update?
|
|
229
|
-
// Simple check: is it published?
|
|
230
|
-
if (!targetAsset.sys.publishedVersion || !!targetAsset.sys.archivedVersion) {
|
|
231
|
-
console.log(`Asset ${assetId} exists but not published. Publishing...`);
|
|
232
|
-
if (!DRY_RUN) {
|
|
233
|
-
await targetAsset.publish();
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
else {
|
|
237
|
-
// Already published. Skip update for speed.
|
|
238
|
-
}
|
|
239
|
-
idMap.set(assetId, assetId);
|
|
240
|
-
continue;
|
|
241
|
-
}
|
|
242
|
-
if (DRY_RUN) {
|
|
243
|
-
console.log(`[Dry Run] Would create asset: ${assetId}`);
|
|
244
|
-
idMap.set(assetId, assetId);
|
|
245
|
-
stats.assets.created++;
|
|
246
|
-
continue;
|
|
247
|
-
}
|
|
248
|
-
console.log(`Creating asset: ${assetId}`);
|
|
249
|
-
const newAsset = await targetEnv.createAssetWithId(assetId, {
|
|
250
|
-
fields: asset.fields,
|
|
251
|
-
metadata: asset.metadata,
|
|
252
|
-
});
|
|
253
|
-
await newAsset.publish(); // Ensure published so wrappers can link
|
|
254
|
-
// Update our map so subsequent calls know it exists
|
|
255
|
-
targetAssetsMap.set(assetId, newAsset);
|
|
256
|
-
idMap.set(assetId, assetId);
|
|
257
|
-
stats.assets.created++;
|
|
258
|
-
}
|
|
259
|
-
catch (error) {
|
|
260
|
-
console.error(`Failed to migrate asset ${assetId}:`, error);
|
|
261
|
-
throw error; // Fail fast
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
// --- Mapping Logic ---
|
|
266
|
-
// Helper to get localized value (assuming 'en-US' or generic)
|
|
267
|
-
function getVal(field, locale = 'en-US') {
|
|
268
|
-
if (!field)
|
|
269
|
-
return undefined;
|
|
270
|
-
return field[locale] || Object.values(field)[0];
|
|
271
|
-
}
|
|
272
|
-
function slugify(text) {
|
|
273
|
-
return text
|
|
274
|
-
.toString()
|
|
275
|
-
.toLowerCase()
|
|
276
|
-
.trim()
|
|
277
|
-
.replace(/\s+/g, '-') // Replace spaces with -
|
|
278
|
-
.replace(/[^\w-]+/g, '') // Remove all non-word chars
|
|
279
|
-
.replace(/--+/g, '-'); // Replace multiple - with single -
|
|
280
|
-
}
|
|
281
|
-
// Dependency walker for single ID mode
|
|
282
|
-
function collectReferences(obj, refs = { entries: new Set(), assets: new Set() }) {
|
|
283
|
-
if (!obj || typeof obj !== 'object')
|
|
284
|
-
return refs;
|
|
285
|
-
if (Array.isArray(obj)) {
|
|
286
|
-
obj.forEach((item) => collectReferences(item, refs));
|
|
287
|
-
return refs;
|
|
288
|
-
}
|
|
289
|
-
if (obj.sys && obj.sys.type === 'Link') {
|
|
290
|
-
if (obj.sys.linkType === 'Entry') {
|
|
291
|
-
refs.entries.add(obj.sys.id);
|
|
292
|
-
}
|
|
293
|
-
else if (obj.sys.linkType === 'Asset') {
|
|
294
|
-
refs.assets.add(obj.sys.id);
|
|
295
|
-
}
|
|
296
|
-
return refs;
|
|
297
|
-
}
|
|
298
|
-
for (const key in obj) {
|
|
299
|
-
collectReferences(obj[key], refs);
|
|
300
|
-
}
|
|
301
|
-
return refs;
|
|
302
|
-
}
|
|
303
|
-
function getEntryReferences(entry) {
|
|
304
|
-
const refs = { entries: new Set(), assets: new Set() };
|
|
305
|
-
for (const field of Object.values(entry.fields)) {
|
|
306
|
-
const val = getVal(field);
|
|
307
|
-
collectReferences(val, refs);
|
|
308
|
-
}
|
|
309
|
-
return refs;
|
|
310
|
-
}
|
|
311
|
-
async function getReachableContent(rootEntries, allEntriesMap) {
|
|
312
|
-
const reachableEntries = new Set();
|
|
313
|
-
const reachableAssets = new Set();
|
|
314
|
-
const queue = [...rootEntries];
|
|
315
|
-
// Mark roots as reachable
|
|
316
|
-
for (const root of rootEntries) {
|
|
317
|
-
reachableEntries.add(root.sys.id);
|
|
318
|
-
}
|
|
319
|
-
while (queue.length > 0) {
|
|
320
|
-
const entry = queue.shift();
|
|
321
|
-
const refs = getEntryReferences(entry);
|
|
322
|
-
// Add Assets
|
|
323
|
-
for (const assetId of refs.assets) {
|
|
324
|
-
reachableAssets.add(assetId);
|
|
325
|
-
}
|
|
326
|
-
// Add Entries
|
|
327
|
-
for (const entryId of refs.entries) {
|
|
328
|
-
if (!reachableEntries.has(entryId)) {
|
|
329
|
-
reachableEntries.add(entryId);
|
|
330
|
-
const linkedEntry = allEntriesMap.get(entryId);
|
|
331
|
-
if (linkedEntry) {
|
|
332
|
-
queue.push(linkedEntry);
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
return { entries: reachableEntries, assets: reachableAssets };
|
|
338
|
-
}
|
|
339
|
-
function createLink(id, linkType) {
|
|
340
|
-
return {
|
|
341
|
-
sys: {
|
|
342
|
-
type: 'Link',
|
|
343
|
-
linkType,
|
|
344
|
-
id,
|
|
345
|
-
},
|
|
346
|
-
};
|
|
347
|
-
}
|
|
348
|
-
function transformRichText(document) {
|
|
349
|
-
if (!document)
|
|
350
|
-
return undefined;
|
|
351
|
-
// If input is a string, wrap it in a basic Rich Text document
|
|
352
|
-
if (typeof document === 'string') {
|
|
353
|
-
return {
|
|
354
|
-
nodeType: 'document',
|
|
355
|
-
data: {},
|
|
356
|
-
content: [
|
|
357
|
-
{
|
|
358
|
-
nodeType: 'paragraph',
|
|
359
|
-
data: {},
|
|
360
|
-
content: [
|
|
361
|
-
{
|
|
362
|
-
nodeType: 'text',
|
|
363
|
-
value: document,
|
|
364
|
-
marks: [],
|
|
365
|
-
data: {},
|
|
366
|
-
},
|
|
367
|
-
],
|
|
368
|
-
},
|
|
369
|
-
],
|
|
370
|
-
};
|
|
371
|
-
}
|
|
372
|
-
if (!document.nodeType)
|
|
373
|
-
return document;
|
|
374
|
-
const newDoc = JSON.parse(JSON.stringify(document));
|
|
375
|
-
function traverse(node) {
|
|
376
|
-
if (node.data && node.data.target && node.data.target.sys) {
|
|
377
|
-
const oldId = node.data.target.sys.id;
|
|
378
|
-
if (idMap.has(oldId)) {
|
|
379
|
-
node.data.target.sys.id = idMap.get(oldId);
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
if (node.content) {
|
|
383
|
-
node.content.forEach(traverse);
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
traverse(newDoc);
|
|
387
|
-
return newDoc;
|
|
388
|
-
}
|
|
389
|
-
// Helper to pick best resolution image
|
|
390
|
-
function getBestImage(candidates, assetsMap, sourceEntriesMap) {
|
|
391
|
-
let bestAsset = undefined;
|
|
392
|
-
let maxPixels = 0;
|
|
393
|
-
for (const item of candidates) {
|
|
394
|
-
if (!item)
|
|
395
|
-
continue;
|
|
396
|
-
let assetId = '';
|
|
397
|
-
// Resolve Entry wrappers (imageAndAltText)
|
|
398
|
-
if (item.sys.linkType === 'Entry') {
|
|
399
|
-
const entryId = item.sys.id;
|
|
400
|
-
const linkedEntry = sourceEntriesMap?.get(entryId);
|
|
401
|
-
if (linkedEntry && linkedEntry.sys.contentType.sys.id === 'imageAndAltText') {
|
|
402
|
-
const wrappedAsset = getVal(linkedEntry.fields.image);
|
|
403
|
-
if (wrappedAsset)
|
|
404
|
-
assetId = wrappedAsset.sys.id;
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
else if (item.sys.linkType === 'Asset') {
|
|
408
|
-
assetId = item.sys.id;
|
|
409
|
-
}
|
|
410
|
-
if (assetId && assetsMap) {
|
|
411
|
-
const asset = assetsMap.get(assetId);
|
|
412
|
-
if (asset && asset.fields.file) {
|
|
413
|
-
const file = getVal(asset.fields.file);
|
|
414
|
-
if (file && file.details && file.details.image) {
|
|
415
|
-
const pixels = (file.details.image.width || 0) * (file.details.image.height || 0);
|
|
416
|
-
if (pixels > maxPixels) {
|
|
417
|
-
maxPixels = pixels;
|
|
418
|
-
// Return the Asset Link.
|
|
419
|
-
bestAsset = createLink(assetId, 'Asset');
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
// Fallback: if no dimensions found (e.g. SVG) or no assetsMap, return first resolvable asset
|
|
426
|
-
if (!bestAsset && candidates.length > 0) {
|
|
427
|
-
const item = candidates.find(c => c);
|
|
428
|
-
if (item) {
|
|
429
|
-
if (item.sys.linkType === 'Entry') {
|
|
430
|
-
const entryId = item.sys.id;
|
|
431
|
-
const linkedEntry = sourceEntriesMap?.get(entryId);
|
|
432
|
-
if (linkedEntry && linkedEntry.sys.contentType.sys.id === 'imageAndAltText') {
|
|
433
|
-
const wrappedAsset = getVal(linkedEntry.fields.image);
|
|
434
|
-
if (wrappedAsset)
|
|
435
|
-
return createLink(wrappedAsset.sys.id, 'Asset');
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
else {
|
|
439
|
-
return item; // Assume Asset Link
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
return bestAsset ? { sys: bestAsset.sys } : undefined;
|
|
444
|
-
}
|
|
445
|
-
function mapEntry(entry, sourceEntriesMap, sourceAssetsMap) {
|
|
446
|
-
const contentTypeId = entry.sys.contentType.sys.id;
|
|
447
|
-
const fields = {};
|
|
448
|
-
const locale = 'en-US';
|
|
449
|
-
const set = (key, value) => {
|
|
450
|
-
if (value !== undefined)
|
|
451
|
-
fields[key] = { [locale]: value };
|
|
452
|
-
};
|
|
453
|
-
const link = (fieldVal, type = 'Entry', forceArray = false) => {
|
|
454
|
-
if (!fieldVal)
|
|
455
|
-
return undefined;
|
|
456
|
-
if (Array.isArray(fieldVal)) {
|
|
457
|
-
return fieldVal.map((item) => {
|
|
458
|
-
const oldId = item.sys.id;
|
|
459
|
-
return createLink(idMap.get(oldId) || oldId, type);
|
|
460
|
-
});
|
|
461
|
-
}
|
|
462
|
-
const oldId = fieldVal.sys.id;
|
|
463
|
-
const result = createLink(idMap.get(oldId) || oldId, type);
|
|
464
|
-
return forceArray ? [result] : result;
|
|
465
|
-
};
|
|
466
|
-
const linkVisual = (fieldVal) => {
|
|
467
|
-
if (!fieldVal)
|
|
468
|
-
return undefined;
|
|
469
|
-
const assetId = fieldVal.sys.id;
|
|
470
|
-
return createLink(getWrapperId(assetId), 'Entry');
|
|
471
|
-
};
|
|
472
|
-
const linkAsset = (fieldVal) => {
|
|
473
|
-
if (!fieldVal)
|
|
474
|
-
return undefined;
|
|
475
|
-
const assetId = fieldVal.sys.id;
|
|
476
|
-
return createLink(assetId, 'Asset');
|
|
477
|
-
};
|
|
478
|
-
// --- Pages ---
|
|
479
|
-
if (contentTypeId === 'contentPage' || contentTypeId === 'page') {
|
|
480
|
-
set('title', getVal(entry.fields.title));
|
|
481
|
-
set('slug', getVal(entry.fields.path) || getVal(entry.fields.slug));
|
|
482
|
-
set('description', getVal(entry.fields.description));
|
|
483
|
-
set('cmsLabel', getVal(entry.fields.adminLabel) || getVal(entry.fields.title));
|
|
484
|
-
// Featured Image (Direct Asset Link)
|
|
485
|
-
const socialImage = getVal(entry.fields.socialImage);
|
|
486
|
-
if (socialImage) {
|
|
487
|
-
if (socialImage.sys.linkType === 'Entry') {
|
|
488
|
-
// If it links to an entry (likely imageAndAltText), try to resolve the asset
|
|
489
|
-
const entryId = socialImage.sys.id;
|
|
490
|
-
const linkedEntry = sourceEntriesMap?.get(entryId);
|
|
491
|
-
if (linkedEntry && linkedEntry.sys.contentType.sys.id === 'imageAndAltText') {
|
|
492
|
-
const asset = getVal(linkedEntry.fields.image);
|
|
493
|
-
if (asset) {
|
|
494
|
-
set('featuredImage', link(asset, 'Asset'));
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
else {
|
|
499
|
-
set('featuredImage', link(socialImage, 'Asset'));
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
// Contents (Flatten pageSection if present? For now assume contents array is main)
|
|
503
|
-
set('content', link(getVal(entry.fields.contents), 'Entry', true));
|
|
504
|
-
set('indexed', getVal(entry.fields.indexed) ?? true);
|
|
505
|
-
set('hidden', false);
|
|
506
|
-
return { contentType: 'page', fields };
|
|
507
|
-
}
|
|
508
|
-
// --- Articles ---
|
|
509
|
-
if (contentTypeId === 'blogPost' || contentTypeId === 'resourcePost') {
|
|
510
|
-
set('title', getVal(entry.fields.title));
|
|
511
|
-
set('subtitle', getVal(entry.fields.teaserTitle));
|
|
512
|
-
set('slug', getVal(entry.fields.slug));
|
|
513
|
-
set('date', getVal(entry.fields.publishDate) || new Date().toISOString());
|
|
514
|
-
set('cmsLabel', getVal(entry.fields.title));
|
|
515
|
-
// Resolve Meta Content for Description and extra Image candidate
|
|
516
|
-
const metaContentLink = getVal(entry.fields.metaContent);
|
|
517
|
-
let metaDescription;
|
|
518
|
-
let metaImage;
|
|
519
|
-
if (metaContentLink && sourceEntriesMap) {
|
|
520
|
-
const metaEntry = sourceEntriesMap.get(metaContentLink.sys.id);
|
|
521
|
-
if (metaEntry) {
|
|
522
|
-
metaDescription = getVal(metaEntry.fields.metaDescription);
|
|
523
|
-
metaImage = getVal(metaEntry.fields.metaImage);
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
// Set Description from Meta Content if available
|
|
527
|
-
if (metaDescription) {
|
|
528
|
-
set('description', metaDescription);
|
|
529
|
-
}
|
|
530
|
-
// Hardcode Article Type to 'Blog'
|
|
531
|
-
set('__assign_blog_type', true);
|
|
532
|
-
// Author (Link to Person)
|
|
533
|
-
const author = getVal(entry.fields.author);
|
|
534
|
-
if (author) {
|
|
535
|
-
set('author', link(author, 'Entry'));
|
|
536
|
-
}
|
|
537
|
-
// Featured Image Logic: Best of featuredImage, image, thumbnail, metaImage
|
|
538
|
-
const candidateImages = [
|
|
539
|
-
getVal(entry.fields.image),
|
|
540
|
-
getVal(entry.fields.thumbnail),
|
|
541
|
-
getVal(entry.fields.featuredImage),
|
|
542
|
-
metaImage
|
|
543
|
-
];
|
|
544
|
-
const bestImage = getBestImage(candidateImages, sourceAssetsMap, sourceEntriesMap);
|
|
545
|
-
if (bestImage) {
|
|
546
|
-
set('featuredImage', { sys: bestImage.sys });
|
|
547
|
-
}
|
|
548
|
-
// Body (Rich Text) -> Component?
|
|
549
|
-
set('__body_rich_text', getVal(entry.fields.body));
|
|
550
|
-
set('indexed', true);
|
|
551
|
-
set('hidden', false);
|
|
552
|
-
return { contentType: 'article', fields };
|
|
553
|
-
}
|
|
554
|
-
// --- Components ---
|
|
555
|
-
if (contentTypeId === 'ctaCard') {
|
|
556
|
-
// Map Card Type to Component Type
|
|
557
|
-
const cardType = getVal(entry.fields.cardType);
|
|
558
|
-
set('componentType', cardType || 'CtaCard'); // Use actual cardType value
|
|
559
|
-
set('heading', getVal(entry.fields.title));
|
|
560
|
-
set('preHeading', getVal(entry.fields.eyebrow));
|
|
561
|
-
set('postHeading', getVal(entry.fields.subtitle));
|
|
562
|
-
set('body', transformRichText(getVal(entry.fields.copy)));
|
|
563
|
-
set('cmsLabel', getVal(entry.fields.adminLabel) || 'CTA Card');
|
|
564
|
-
set('showHeading', !!getVal(entry.fields.title));
|
|
565
|
-
set('anchor', getVal(entry.fields.anchor));
|
|
566
|
-
// set('backgroundColour', getVal(entry.fields.backgroundColour));
|
|
567
|
-
// set('textColour', getVal(entry.fields.textColour));
|
|
568
|
-
const media = getVal(entry.fields.media); // Asset
|
|
569
|
-
const image = getVal(entry.fields.image); // Entry (imageAndAltText)
|
|
570
|
-
if (media) {
|
|
571
|
-
set('visual', linkVisual(media));
|
|
572
|
-
}
|
|
573
|
-
else if (image) {
|
|
574
|
-
// imageAndAltText mapped to media entry, so we link to that media entry
|
|
575
|
-
set('visual', link(image));
|
|
576
|
-
}
|
|
577
|
-
set('links', link(getVal(entry.fields.buttons), 'Entry', true));
|
|
578
|
-
return { contentType: 'component', fields };
|
|
579
|
-
}
|
|
580
|
-
if (contentTypeId === 'hero') {
|
|
581
|
-
set('componentType', 'Hero');
|
|
582
|
-
set('heading', getVal(entry.fields.title)); // RichText in source, Symbol in target?
|
|
583
|
-
// If target heading is Symbol, we might need to extract text or use 'body'.
|
|
584
|
-
// Target 'heading' is Symbol. Source 'title' is RichText.
|
|
585
|
-
// We'll put source title into 'body' if it's rich, or extract string.
|
|
586
|
-
// Let's assume 'body' for rich title.
|
|
587
|
-
set('body', transformRichText(getVal(entry.fields.title)));
|
|
588
|
-
set('cmsLabel', getVal(entry.fields.adminLabel) || 'Hero');
|
|
589
|
-
set('showHeading', false); // Since we put title in body
|
|
590
|
-
const heroImage = getVal(entry.fields.heroImage);
|
|
591
|
-
if (heroImage) {
|
|
592
|
-
set('visual', link(heroImage)); // It's an Entry (imageAndAltText)
|
|
593
|
-
}
|
|
594
|
-
return { contentType: 'component', fields };
|
|
595
|
-
}
|
|
596
|
-
if (contentTypeId === 'videoModule') {
|
|
597
|
-
set('componentType', 'Video');
|
|
598
|
-
set('heading', getVal(entry.fields.title));
|
|
599
|
-
set('cmsLabel', getVal(entry.fields.adminTitle) || 'Video');
|
|
600
|
-
set('showHeading', !!getVal(entry.fields.title));
|
|
601
|
-
set('body', transformRichText(getVal(entry.fields.copy)));
|
|
602
|
-
const videoUrl = getVal(entry.fields.videoUrl);
|
|
603
|
-
if (videoUrl) {
|
|
604
|
-
set('__external_video', {
|
|
605
|
-
url: videoUrl,
|
|
606
|
-
width: getVal(entry.fields.videoWidth),
|
|
607
|
-
title: getVal(entry.fields.title) || getVal(entry.fields.adminTitle) || 'External Video',
|
|
608
|
-
});
|
|
609
|
-
}
|
|
610
|
-
else {
|
|
611
|
-
const videoAsset = getVal(entry.fields.videoAsset);
|
|
612
|
-
if (videoAsset)
|
|
613
|
-
set('visual', linkVisual(videoAsset));
|
|
614
|
-
}
|
|
615
|
-
return { contentType: 'component', fields };
|
|
616
|
-
}
|
|
617
|
-
if (contentTypeId === 'quote') {
|
|
618
|
-
set('componentType', 'Quote');
|
|
619
|
-
set('body', transformRichText(getVal(entry.fields.copy)));
|
|
620
|
-
set('cmsLabel', getVal(entry.fields.adminLabel) || 'Quote');
|
|
621
|
-
set('showHeading', false);
|
|
622
|
-
set('heading', getVal(entry.fields.authorName));
|
|
623
|
-
set('postHeading', getVal(entry.fields.authorTitle));
|
|
624
|
-
// set('backgroundColour', getVal(entry.fields.backgroundColour));
|
|
625
|
-
// set('textColour', getVal(entry.fields.textColour));
|
|
626
|
-
const image = getVal(entry.fields.image);
|
|
627
|
-
if (image) {
|
|
628
|
-
if (image.sys.linkType === 'Asset') {
|
|
629
|
-
set('visual', linkVisual(image));
|
|
630
|
-
}
|
|
631
|
-
else {
|
|
632
|
-
// Probably imageAndAltText
|
|
633
|
-
set('visual', link(image));
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
return { contentType: 'component', fields };
|
|
637
|
-
}
|
|
638
|
-
if (contentTypeId === 'textSection') {
|
|
639
|
-
set('componentType', 'Text');
|
|
640
|
-
set('body', transformRichText(getVal(entry.fields.copy)));
|
|
641
|
-
set('cmsLabel', getVal(entry.fields.adminLabel) || 'Text Section');
|
|
642
|
-
set('showHeading', false);
|
|
643
|
-
return { contentType: 'component', fields };
|
|
644
|
-
}
|
|
645
|
-
if (contentTypeId === 'itemGrid') {
|
|
646
|
-
set('componentType', 'Logos');
|
|
647
|
-
set('heading', getVal(entry.fields.title));
|
|
648
|
-
set('cmsLabel', getVal(entry.fields.adminLabel) || 'Logos');
|
|
649
|
-
set('showHeading', !!getVal(entry.fields.title));
|
|
650
|
-
set('body', transformRichText(getVal(entry.fields.copy)));
|
|
651
|
-
set('additionalCopy', transformRichText(getVal(entry.fields.footnote)));
|
|
652
|
-
// set('backgroundColour', getVal(entry.fields.backgroundColour));
|
|
653
|
-
const items = getVal(entry.fields.items);
|
|
654
|
-
if (items && Array.isArray(items)) {
|
|
655
|
-
const otherMedia = items.map((item) => {
|
|
656
|
-
if (item.sys.linkType === 'Asset') {
|
|
657
|
-
return linkAsset(item);
|
|
658
|
-
}
|
|
659
|
-
// 2. If it's an Entry, check if it's an 'imageAndAltText'
|
|
660
|
-
if (item.sys.linkType === 'Entry') {
|
|
661
|
-
const entryId = item.sys.id;
|
|
662
|
-
const linkedEntry = sourceEntriesMap?.get(entryId);
|
|
663
|
-
if (linkedEntry && linkedEntry.sys.contentType.sys.id === 'imageAndAltText') {
|
|
664
|
-
const asset = getVal(linkedEntry.fields.image);
|
|
665
|
-
if (asset) {
|
|
666
|
-
// Found the asset inside imageAndAltText, link it as a Visual
|
|
667
|
-
return linkAsset(asset);
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
// 3. Fallback to standard entry link
|
|
672
|
-
return link(item);
|
|
673
|
-
});
|
|
674
|
-
set('otherMedia', otherMedia);
|
|
675
|
-
}
|
|
676
|
-
return { contentType: 'component', fields };
|
|
677
|
-
}
|
|
678
|
-
if (contentTypeId === 'imageAndAltText') {
|
|
679
|
-
// Map to 'media' Entry, not 'component'
|
|
680
|
-
// 'media' has 'name' and 'asset'
|
|
681
|
-
const name = getVal(entry.fields.adminLabel) || 'Media';
|
|
682
|
-
const asset = getVal(entry.fields.image);
|
|
683
|
-
// We are creating a 'media' entry directly here.
|
|
684
|
-
// fields: name, asset
|
|
685
|
-
fields['name'] = { [locale]: name };
|
|
686
|
-
if (asset)
|
|
687
|
-
fields['asset'] = { [locale]: { sys: createLink(asset.sys.id, 'Asset').sys } };
|
|
688
|
-
return { contentType: 'media', fields };
|
|
689
|
-
}
|
|
690
|
-
// --- Collections ---
|
|
691
|
-
if (['cards', 'carousel', 'teamMemberGrid', 'featuredPostSection'].includes(contentTypeId)) {
|
|
692
|
-
let collectionType = 'Collection';
|
|
693
|
-
if (contentTypeId === 'cards') {
|
|
694
|
-
// Map Cards Type to Collection Type
|
|
695
|
-
collectionType = getVal(entry.fields.type) || 'Card Collection';
|
|
696
|
-
}
|
|
697
|
-
else {
|
|
698
|
-
collectionType =
|
|
699
|
-
contentTypeId === 'carousel'
|
|
700
|
-
? 'Carousel'
|
|
701
|
-
: contentTypeId === 'teamMemberGrid'
|
|
702
|
-
? 'Team Grid'
|
|
703
|
-
: 'Featured Posts';
|
|
704
|
-
}
|
|
705
|
-
set('collectionType', collectionType);
|
|
706
|
-
set('cmsLabel', getVal(entry.fields.adminLabel) || 'Collection');
|
|
707
|
-
set('heading', getVal(entry.fields.title));
|
|
708
|
-
set('showHeading', !!getVal(entry.fields.title));
|
|
709
|
-
// Common mappings
|
|
710
|
-
// set('backgroundColour', getVal(entry.fields.backgroundColour));
|
|
711
|
-
// Type Specific Mappings
|
|
712
|
-
if (contentTypeId === 'cards') {
|
|
713
|
-
set('body', transformRichText(getVal(entry.fields.copy)));
|
|
714
|
-
set('preHeading', getVal(entry.fields.eyebrow));
|
|
715
|
-
set('postHeading', getVal(entry.fields.subtitle));
|
|
716
|
-
set('additionalCopy', transformRichText(getVal(entry.fields.footnote)));
|
|
717
|
-
}
|
|
718
|
-
if (contentTypeId === 'carousel') {
|
|
719
|
-
const media = getVal(entry.fields.media);
|
|
720
|
-
if (media)
|
|
721
|
-
set('visual', linkVisual(media));
|
|
722
|
-
}
|
|
723
|
-
const contents = getVal(entry.fields.cards) ||
|
|
724
|
-
getVal(entry.fields.items) ||
|
|
725
|
-
getVal(entry.fields.teamMembers) ||
|
|
726
|
-
getVal(entry.fields.posts) ||
|
|
727
|
-
getVal(entry.fields.images) || // Added
|
|
728
|
-
getVal(entry.fields.media); // Added
|
|
729
|
-
if (contents) {
|
|
730
|
-
set('contents', link(contents, 'Entry', true));
|
|
731
|
-
}
|
|
732
|
-
set('links', link(getVal(entry.fields.links), 'Entry', true));
|
|
733
|
-
return { contentType: 'collection', fields };
|
|
734
|
-
}
|
|
735
|
-
// --- People ---
|
|
736
|
-
if (contentTypeId === 'teamMember' ||
|
|
737
|
-
contentTypeId === 'author' ||
|
|
738
|
-
contentTypeId === 'therapist') {
|
|
739
|
-
const name = getVal(entry.fields.name);
|
|
740
|
-
set('name', name);
|
|
741
|
-
// set('cmsLabel', getVal(entry.fields.name) || getVal(entry.fields.adminLabel)); // Person has no cmsLabel
|
|
742
|
-
if (name) {
|
|
743
|
-
set('slug', slugify(name));
|
|
744
|
-
}
|
|
745
|
-
set('indexed', true);
|
|
746
|
-
set('hidden', false);
|
|
747
|
-
if (contentTypeId === 'teamMember') {
|
|
748
|
-
set('jobTitle', getVal(entry.fields.role));
|
|
749
|
-
const bylines = getVal(entry.fields.bylines);
|
|
750
|
-
let bioText = transformRichText(getVal(entry.fields.bio));
|
|
751
|
-
// Append bylines to bio if present
|
|
752
|
-
if (bylines && bylines.length > 0) {
|
|
753
|
-
// Construct a simple paragraph for bylines
|
|
754
|
-
// Since we can't easily append to RichText structure without helpers,
|
|
755
|
-
// let's assume if we have bio, we keep it. If we have bylines, we might want to add them.
|
|
756
|
-
// For simplicity, if bio is empty, use bylines as text.
|
|
757
|
-
if (!bioText) {
|
|
758
|
-
bioText = transformRichText(bylines.join('\n'));
|
|
759
|
-
}
|
|
760
|
-
}
|
|
761
|
-
set('bio', bioText);
|
|
762
|
-
const photo = getVal(entry.fields.photo) || getVal(entry.fields.portrait) || getVal(entry.fields.portraitImage); // therapist uses portrait
|
|
763
|
-
if (photo)
|
|
764
|
-
set('media', link(photo, 'Asset'));
|
|
765
|
-
}
|
|
766
|
-
if (contentTypeId === 'author') {
|
|
767
|
-
set('bio', transformRichText(getVal(entry.fields.bio)));
|
|
768
|
-
const image = getVal(entry.fields.image);
|
|
769
|
-
if (image)
|
|
770
|
-
set('media', link(image, 'Asset'));
|
|
771
|
-
}
|
|
772
|
-
if (contentTypeId === 'therapist') {
|
|
773
|
-
set('jobTitle', getVal(entry.fields.qualifications));
|
|
774
|
-
set('bio', transformRichText(getVal(entry.fields.bio)));
|
|
775
|
-
const portrait = getVal(entry.fields.portrait);
|
|
776
|
-
if (portrait)
|
|
777
|
-
set('media', link(portrait, 'Asset'));
|
|
778
|
-
}
|
|
779
|
-
return { contentType: 'person', fields };
|
|
780
|
-
}
|
|
781
|
-
// --- Links ---
|
|
782
|
-
if (contentTypeId === 'button') {
|
|
783
|
-
set('linkText', getVal(entry.fields.label));
|
|
784
|
-
set('name', getVal(entry.fields.adminLabel) || 'Button');
|
|
785
|
-
set('useName', false);
|
|
786
|
-
set('variant', 'button');
|
|
787
|
-
// Handle internal destination - could be array, could be Asset or Entry
|
|
788
|
-
let internalDest = getVal(entry.fields.internalDestination);
|
|
789
|
-
if (Array.isArray(internalDest)) {
|
|
790
|
-
internalDest = internalDest[0];
|
|
791
|
-
}
|
|
792
|
-
if (internalDest) {
|
|
793
|
-
// Check if it's an Asset or Entry
|
|
794
|
-
if (internalDest.sys.linkType === 'Asset') {
|
|
795
|
-
set('downloadAsset', link(internalDest, 'Asset'));
|
|
796
|
-
}
|
|
797
|
-
else {
|
|
798
|
-
set('internal', link(internalDest, 'Entry'));
|
|
799
|
-
}
|
|
800
|
-
}
|
|
801
|
-
set('external', getVal(entry.fields.externalDestination));
|
|
802
|
-
return { contentType: 'link', fields };
|
|
803
|
-
}
|
|
804
|
-
// --- External Components ---
|
|
805
|
-
if (contentTypeId === 'form') {
|
|
806
|
-
set('externalComponentType', 'form');
|
|
807
|
-
set('cmsLabel', getVal(entry.fields.name) || getVal(entry.fields.adminLabel) || 'Form');
|
|
808
|
-
set('data', { formName: getVal(entry.fields.formName) || getVal(entry.fields.name) });
|
|
809
|
-
return { contentType: 'externalComponent', fields };
|
|
810
|
-
}
|
|
811
|
-
if (contentTypeId === 'genericModule') {
|
|
812
|
-
set('externalComponentType', 'generic');
|
|
813
|
-
const codeId = getVal(entry.fields.codeId);
|
|
814
|
-
set('cmsLabel', codeId || getVal(entry.fields.adminLabel) || 'Generic Module');
|
|
815
|
-
set('data', { codeId });
|
|
816
|
-
return { contentType: 'externalComponent', fields };
|
|
817
|
-
}
|
|
818
|
-
return null;
|
|
819
|
-
}
|
|
820
|
-
// --- Priority Sorting ---
|
|
821
|
-
function getPriority(contentTypeId) {
|
|
822
|
-
// Level 1: Leafs (Dependencies)
|
|
823
|
-
if (['author', 'teamMember', 'therapist', 'imageAndAltText', 'button'].includes(contentTypeId)) {
|
|
824
|
-
return 1;
|
|
825
|
-
}
|
|
826
|
-
// Level 2: Components
|
|
827
|
-
if (['ctaCard', 'quote', 'videoModule', 'textSection', 'hero', 'form', 'genericModule', 'itemGrid'].includes(contentTypeId)) {
|
|
828
|
-
return 2;
|
|
829
|
-
}
|
|
830
|
-
// Level 3: Collections
|
|
831
|
-
if (['cards', 'carousel', 'teamMemberGrid', 'featuredPostSection'].includes(contentTypeId)) {
|
|
832
|
-
return 3;
|
|
833
|
-
}
|
|
834
|
-
// Level 4: Roots (Pages, Articles)
|
|
835
|
-
if (['contentPage', 'page', 'blogPost', 'resourcePost'].includes(contentTypeId)) {
|
|
836
|
-
return 4;
|
|
837
|
-
}
|
|
838
|
-
// Default to last
|
|
839
|
-
return 5;
|
|
840
|
-
}
|
|
841
|
-
// Helper to fetch all entries with pagination (Published Only)
|
|
842
|
-
async function fetchAllEntries(env, query = {}) {
|
|
843
|
-
const limit = 1000;
|
|
844
|
-
let skip = 0;
|
|
845
|
-
let allItems = [];
|
|
846
|
-
while (true) {
|
|
847
|
-
const response = await env.getPublishedEntries({ ...query, limit, skip });
|
|
848
|
-
allItems = allItems.concat(response.items);
|
|
849
|
-
skip += limit;
|
|
850
|
-
if (response.items.length < limit)
|
|
851
|
-
break;
|
|
852
|
-
console.log(`Fetched ${allItems.length} published entries so far...`);
|
|
853
|
-
}
|
|
854
|
-
return allItems;
|
|
855
|
-
}
|
|
856
|
-
// Helper to fetch all assets with pagination
|
|
857
|
-
async function fetchAllAssets(env, query = {}, publishedOnly = true) {
|
|
858
|
-
const limit = 1000;
|
|
859
|
-
let skip = 0;
|
|
860
|
-
let allItems = [];
|
|
861
|
-
while (true) {
|
|
862
|
-
const response = await (publishedOnly
|
|
863
|
-
? env.getPublishedAssets({ ...query, limit, skip })
|
|
864
|
-
: env.getAssets({ ...query, limit, skip }));
|
|
865
|
-
allItems = allItems.concat(response.items);
|
|
866
|
-
skip += limit;
|
|
867
|
-
if (response.items.length < limit)
|
|
868
|
-
break;
|
|
869
|
-
console.log(`Fetched ${allItems.length} ${publishedOnly ? 'published' : 'total'} assets so far...`);
|
|
870
|
-
}
|
|
871
|
-
return allItems;
|
|
872
|
-
}
|
|
873
|
-
// --- Main Execution ---
|
|
874
|
-
async function run() {
|
|
875
|
-
console.log(`Starting migration... DRY_RUN=${DRY_RUN}`);
|
|
876
|
-
const targetAssetsMap = new Map();
|
|
877
|
-
const sourceEnv = await getEnvironment(SPACE_ID, SOURCE_ENV_ID);
|
|
878
|
-
const targetEnv = await getEnvironment(SPACE_ID, TARGET_ENV_ID);
|
|
879
|
-
// Fetch existing target assets to avoid recreation errors
|
|
880
|
-
console.log('Fetching existing target assets...');
|
|
881
|
-
const targetAssetsItems = await fetchAllAssets(targetEnv, {}, false);
|
|
882
|
-
for (const asset of targetAssetsItems) {
|
|
883
|
-
targetAssetsMap.set(asset.sys.id, asset);
|
|
884
|
-
}
|
|
885
|
-
console.log(`Found ${targetAssetsMap.size} existing target assets.`);
|
|
886
|
-
const singleId = process.argv.find((arg) => arg.startsWith('--single-id='))?.split('=')[1];
|
|
887
|
-
const scopeArg = process.argv.find((arg) => arg.startsWith('--scope='))?.split('=')[1];
|
|
888
|
-
// Pre-fetch/Ensure Blog Article Type
|
|
889
|
-
let blogTypeId = '';
|
|
890
|
-
if (!DRY_RUN) {
|
|
891
|
-
blogTypeId = await getBlogArticleTypeId(targetEnv);
|
|
892
|
-
}
|
|
893
|
-
else {
|
|
894
|
-
blogTypeId = 'article-type-blog';
|
|
895
|
-
}
|
|
896
|
-
// 1. Fetch ALL Published Entries and Assets from Source
|
|
897
|
-
console.log('Fetching ALL published entries from source...');
|
|
898
|
-
const allSourceEntries = await fetchAllEntries(sourceEnv);
|
|
899
|
-
const allSourceAssets = await fetchAllAssets(sourceEnv);
|
|
900
|
-
const sourceEntriesMap = new Map();
|
|
901
|
-
for (const entry of allSourceEntries) {
|
|
902
|
-
sourceEntriesMap.set(entry.sys.id, entry);
|
|
903
|
-
}
|
|
904
|
-
const sourceAssetsMap = new Map();
|
|
905
|
-
for (const asset of allSourceAssets) {
|
|
906
|
-
sourceAssetsMap.set(asset.sys.id, asset);
|
|
907
|
-
}
|
|
908
|
-
// 2. Determine Root Entries
|
|
909
|
-
let rootEntries = [];
|
|
910
|
-
if (singleId) {
|
|
911
|
-
console.log(`Running in Single ID mode for: ${singleId}`);
|
|
912
|
-
const root = sourceEntriesMap.get(singleId);
|
|
913
|
-
if (root) {
|
|
914
|
-
rootEntries.push(root);
|
|
915
|
-
}
|
|
916
|
-
else {
|
|
917
|
-
console.warn(`Single ID ${singleId} not found in published entries.`);
|
|
918
|
-
}
|
|
919
|
-
}
|
|
920
|
-
else {
|
|
921
|
-
// Default roots: Pages and Posts
|
|
922
|
-
rootEntries = allSourceEntries.filter(entry => ['contentPage', 'page', 'blogPost', 'resourcePost'].includes(entry.sys.contentType.sys.id));
|
|
923
|
-
console.log(`Found ${rootEntries.length} root entries (Pages/Posts).`);
|
|
924
|
-
}
|
|
925
|
-
// 3. Calculate Reachable Content
|
|
926
|
-
console.log('Calculating reachable content...');
|
|
927
|
-
const reachable = await getReachableContent(rootEntries, sourceEntriesMap);
|
|
928
|
-
console.log(`Reachable: ${reachable.entries.size} entries, ${reachable.assets.size} assets.`);
|
|
929
|
-
// 4. Filter Source Items
|
|
930
|
-
const sourceEntriesItems = allSourceEntries.filter(e => reachable.entries.has(e.sys.id));
|
|
931
|
-
const sourceAssetsItems = allSourceAssets.filter(a => reachable.assets.has(a.sys.id));
|
|
932
|
-
// Sort by priority
|
|
933
|
-
sourceEntriesItems.sort((a, b) => {
|
|
934
|
-
return getPriority(a.sys.contentType.sys.id) - getPriority(b.sys.contentType.sys.id);
|
|
935
|
-
});
|
|
936
|
-
// 5. Migrate Assets
|
|
937
|
-
await migrateAssets(sourceAssetsItems, targetEnv, targetAssetsMap);
|
|
938
|
-
// 6. Process Entries
|
|
939
|
-
console.log(`Processing ${sourceEntriesItems.length} entries.`);
|
|
940
|
-
console.log('\n--- Processing Entries (Phase 1: Dependencies) ---');
|
|
941
|
-
for (const entry of sourceEntriesItems) {
|
|
942
|
-
if (getPriority(entry.sys.contentType.sys.id) !== 1)
|
|
943
|
-
continue;
|
|
944
|
-
await processEntry(entry, targetEnv, blogTypeId, sourceEntriesMap);
|
|
945
|
-
}
|
|
946
|
-
console.log('\n--- Processing Entries (Phase 2a: Content) ---');
|
|
947
|
-
for (const entry of sourceEntriesItems) {
|
|
948
|
-
const priority = getPriority(entry.sys.contentType.sys.id);
|
|
949
|
-
if (priority === 1 || priority === 4)
|
|
950
|
-
continue;
|
|
951
|
-
await processEntry(entry, targetEnv, blogTypeId, sourceEntriesMap);
|
|
952
|
-
}
|
|
953
|
-
console.log('\n--- Processing Entries (Phase 2b: Pages & Articles Sequential) ---');
|
|
954
|
-
for (const entry of sourceEntriesItems) {
|
|
955
|
-
if (getPriority(entry.sys.contentType.sys.id) !== 4)
|
|
956
|
-
continue;
|
|
957
|
-
const type = entry.sys.contentType.sys.id;
|
|
958
|
-
const isArticle = ['blogPost', 'resourcePost'].includes(type);
|
|
959
|
-
const isPage = ['contentPage', 'page'].includes(type);
|
|
960
|
-
if (scopeArg === 'articles' && !isArticle)
|
|
961
|
-
continue;
|
|
962
|
-
if (scopeArg === 'pages' && !isPage)
|
|
963
|
-
continue;
|
|
964
|
-
// Process sequentially with delay
|
|
965
|
-
await processEntry(entry, targetEnv, blogTypeId, sourceEntriesMap);
|
|
966
|
-
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
967
|
-
}
|
|
968
|
-
async function processEntry(entry, targetEnv, blogTypeId, sourceEntriesMap) {
|
|
969
|
-
// Skip if not in our target list
|
|
970
|
-
if (![
|
|
971
|
-
'contentPage',
|
|
972
|
-
'page', // Added
|
|
973
|
-
'blogPost',
|
|
974
|
-
'resourcePost', // Added
|
|
975
|
-
'ctaCard',
|
|
976
|
-
'cards',
|
|
977
|
-
'carousel',
|
|
978
|
-
'itemGrid', // Added
|
|
979
|
-
'teamMemberGrid', // Added
|
|
980
|
-
'featuredPostSection', // Added
|
|
981
|
-
'quote',
|
|
982
|
-
'videoModule',
|
|
983
|
-
'hero', // Added
|
|
984
|
-
'textSection', // Added
|
|
985
|
-
'button',
|
|
986
|
-
'imageAndAltText',
|
|
987
|
-
'teamMember',
|
|
988
|
-
'author',
|
|
989
|
-
'therapist',
|
|
990
|
-
'form',
|
|
991
|
-
'genericModule',
|
|
992
|
-
].includes(entry.sys.contentType.sys.id)) {
|
|
993
|
-
return;
|
|
994
|
-
}
|
|
995
|
-
const mapped = mapEntry(entry, sourceEntriesMap, targetAssetsMap);
|
|
996
|
-
if (!mapped) {
|
|
997
|
-
console.log(`Skipping ${entry.sys.id} (${entry.sys.contentType.sys.id}) - No mapping`);
|
|
998
|
-
return;
|
|
999
|
-
}
|
|
1000
|
-
const newId = entry.sys.id;
|
|
1001
|
-
idMap.set(entry.sys.id, newId);
|
|
1002
|
-
// Ensure types are valid
|
|
1003
|
-
if (mapped.contentType === 'component' && mapped.fields.componentType) {
|
|
1004
|
-
await ensureValidValue(targetEnv, 'component', 'componentType', mapped.fields.componentType['en-US']);
|
|
1005
|
-
}
|
|
1006
|
-
if (mapped.contentType === 'collection' && mapped.fields.collectionType) {
|
|
1007
|
-
await ensureValidValue(targetEnv, 'collection', 'collectionType', mapped.fields.collectionType['en-US']);
|
|
1008
|
-
}
|
|
1009
|
-
if (mapped.contentType === 'externalComponent' && mapped.fields.externalComponentType) {
|
|
1010
|
-
await ensureValidValue(targetEnv, 'externalComponent', 'externalComponentType', mapped.fields.externalComponentType['en-US']);
|
|
1011
|
-
}
|
|
1012
|
-
// --- Special Handling: Create External Video for Video Components ---
|
|
1013
|
-
if (mapped.fields.__external_video) {
|
|
1014
|
-
const extVideoData = mapped.fields.__external_video['en-US'];
|
|
1015
|
-
const extVideoId = `${newId}-ext-video`;
|
|
1016
|
-
const extVideoEntry = {
|
|
1017
|
-
name: { 'en-US': extVideoData.title },
|
|
1018
|
-
url: { 'en-US': extVideoData.url },
|
|
1019
|
-
width: extVideoData.width ? { 'en-US': extVideoData.width } : undefined,
|
|
1020
|
-
};
|
|
1021
|
-
if (DRY_RUN) {
|
|
1022
|
-
console.log(`[Dry Run] Would create External Video ${extVideoId}`);
|
|
1023
|
-
}
|
|
1024
|
-
else {
|
|
1025
|
-
await updateEntryWithRetry(targetEnv, extVideoId, 'externalVideo', extVideoEntry);
|
|
1026
|
-
}
|
|
1027
|
-
mapped.fields.visual = { 'en-US': { sys: createLink(extVideoId, 'Entry').sys } };
|
|
1028
|
-
delete mapped.fields.__external_video;
|
|
1029
|
-
}
|
|
1030
|
-
// ------------------------------------------------------------
|
|
1031
|
-
// --- Special Handling: Create Body Component for Articles ---
|
|
1032
|
-
if (mapped.fields.__body_rich_text) {
|
|
1033
|
-
const bodyId = `${newId}-body`;
|
|
1034
|
-
const bodyComponent = {
|
|
1035
|
-
componentType: { 'en-US': 'Blog rich text' },
|
|
1036
|
-
cmsLabel: { 'en-US': `Body for ${newId}` },
|
|
1037
|
-
showHeading: { 'en-US': false },
|
|
1038
|
-
body: mapped.fields.__body_rich_text,
|
|
1039
|
-
};
|
|
1040
|
-
if (DRY_RUN) {
|
|
1041
|
-
console.log(`[Dry Run] Would create Body Component ${bodyId}`);
|
|
1042
|
-
}
|
|
1043
|
-
else {
|
|
1044
|
-
await updateEntryWithRetry(targetEnv, bodyId, 'component', bodyComponent);
|
|
1045
|
-
}
|
|
1046
|
-
mapped.fields.content = { 'en-US': [{ sys: createLink(bodyId, 'Entry').sys }] };
|
|
1047
|
-
delete mapped.fields.__body_rich_text;
|
|
1048
|
-
}
|
|
1049
|
-
// Assign Blog Article Type
|
|
1050
|
-
if (mapped.fields.__assign_blog_type) {
|
|
1051
|
-
if (blogTypeId) {
|
|
1052
|
-
mapped.fields.articleType = { 'en-US': { sys: createLink(blogTypeId, 'Entry').sys } };
|
|
1053
|
-
}
|
|
1054
|
-
delete mapped.fields.__assign_blog_type;
|
|
1055
|
-
}
|
|
1056
|
-
// ------------------------------------------------------------
|
|
1057
|
-
// Ensure visual wrappers
|
|
1058
|
-
for (const [key, val] of Object.entries(mapped.fields)) {
|
|
1059
|
-
if (key === 'visual') {
|
|
1060
|
-
const linkObj = val['en-US'];
|
|
1061
|
-
if (linkObj && linkObj.sys.linkType === 'Entry' && linkObj.sys.id.startsWith('visual-')) {
|
|
1062
|
-
const wrapperId = linkObj.sys.id;
|
|
1063
|
-
const assetId = wrapperId.replace('visual-', '');
|
|
1064
|
-
await ensureWrapperEntry(targetEnv, assetId, `Wrapper for ${assetId}`, targetAssetsMap);
|
|
1065
|
-
}
|
|
1066
|
-
}
|
|
1067
|
-
}
|
|
1068
|
-
if (DRY_RUN) {
|
|
1069
|
-
console.log(`[Dry Run] Would create ${mapped.contentType} (${newId}) from ${entry.sys.contentType.sys.id}`);
|
|
1070
|
-
stats.entries.created++;
|
|
1071
|
-
return;
|
|
1072
|
-
}
|
|
1073
|
-
try {
|
|
1074
|
-
await updateEntryWithRetry(targetEnv, newId, mapped.contentType, mapped.fields);
|
|
1075
|
-
stats.entries.created++;
|
|
1076
|
-
}
|
|
1077
|
-
catch (error) {
|
|
1078
|
-
console.error(`Failed to create/update entry ${newId}:`, error);
|
|
1079
|
-
stats.entries.errors++;
|
|
1080
|
-
}
|
|
1081
|
-
}
|
|
1082
|
-
console.log(JSON.stringify(stats, null, 2));
|
|
1083
|
-
}
|
|
1084
|
-
run().catch(console.error);
|
|
1085
|
-
//# sourceMappingURL=migrate-brightline.js.map
|