contentful-export 8.3.0 → 8.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -389,6 +389,23 @@ If the source space has no ExO entities, or lacks the `exoM1` entitlement, each
389
389
 
390
390
  Requires the `exoM1` entitlement on the source space's organization. [contentful-cli](https://github.com/contentful/contentful-cli)'s `space export` command doesn't expose this option at all yet, so ExO export isn't reachable through that separate CLI regardless of default.
391
391
 
392
+ ### Optimization Variants
393
+
394
+ Experiences and Experience Fragments each support **Optimization Variants** — alternate versions used for personalization. Variants are a separate opt-in on top of `includeExperienceOrchestration`, defaulting to `false`:
395
+
396
+ ```javascript
397
+ const options = {
398
+ spaceId: '<space_id>',
399
+ managementToken: '<content_management_api_key>',
400
+ includeExperienceOrchestration: true,
401
+ includeExoVariants: true
402
+ }
403
+
404
+ await contentfulExport(options)
405
+ ```
406
+
407
+ Unlike the six entity types above, variants are **not** exported as a seventh top-level array — they're nested onto their parent as `experience.optimizationVariants` / `experienceFragment.optimizationVariants`, because a variant has no globally-unique `sys.id` of its own (the API's variant response reuses the parent's `sys.id`; the variant is identified by `sys.variant`/`sys.variantType`/`sys.variantDimension` instead). When `includeExoVariants` is omitted or `false`, the `optimizationVariants` field is absent entirely — not an empty array — so default export output is unaffected. See [`docs/exo-export.md`](./docs/exo-export.md#optimization-variants) for the full shape, the ADR behind the nested-storage decision, and the corresponding `contentful-import` behavior.
408
+
392
409
  ### Round-tripping into `contentful-import`
393
410
 
394
411
  The ExO entities exported here are designed to be fed directly into [`contentful-import`](https://github.com/contentful/contentful-import), which preserves source IDs, applies dependency ordering (a topological sort for Components and Experience Fragments, since either can reference others of the same type), and upgrades entities from older, pre-rename export files automatically. See `contentful-import`'s README "Experience Orchestration (ExO) entities" section for the import-side details.
package/dist/index.js CHANGED
@@ -82,6 +82,7 @@ function runContentfulExport(params) {
82
82
  skipTags: options.skipTags,
83
83
  stripTags: options.stripTags,
84
84
  includeExperienceOrchestration: options.includeExperienceOrchestration,
85
+ includeExoVariants: options.includeExoVariants,
85
86
  listrOptions,
86
87
  queryEntries: options.queryEntries,
87
88
  queryAssets: options.queryAssets
@@ -131,6 +132,7 @@ function runContentfulExport(params) {
131
132
  }).then(ctx => {
132
133
  const resultTypes = Object.keys(ctx.data);
133
134
  if (resultTypes.length) {
135
+ var _ctx$data$experiences, _ctx$optimizationVari, _ctx$data$experienceF, _ctx$optimizationVari2;
134
136
  const resultTable = new _cliTable.default(tableOptions);
135
137
  resultTable.push([{
136
138
  colSpan: 2,
@@ -139,6 +141,33 @@ function runContentfulExport(params) {
139
141
  resultTypes.forEach(type => {
140
142
  resultTable.push([(0, _lodash.default)(type), ctx.data[type].length]);
141
143
  });
144
+
145
+ // Optimization Variants are nested onto their parent Experience/Experience
146
+ // Fragment rather than exported as their own top-level field, so their count
147
+ // isn't free from resultTypes above and is computed here instead. A per-parent
148
+ // fetch failure falls back to an empty optimizationVariants array, which is
149
+ // indistinguishable from a parent that genuinely has none -- so failures are
150
+ // surfaced as their own row rather than folded silently into the count.
151
+ if ((_ctx$data$experiences = ctx.data.experiences) !== null && _ctx$data$experiences !== void 0 && _ctx$data$experiences.some(experience => experience.optimizationVariants)) {
152
+ const count = ctx.data.experiences.reduce((sum, experience) => {
153
+ var _experience$optimizat, _experience$optimizat2;
154
+ return sum + ((_experience$optimizat = (_experience$optimizat2 = experience.optimizationVariants) === null || _experience$optimizat2 === void 0 ? void 0 : _experience$optimizat2.length) !== null && _experience$optimizat !== void 0 ? _experience$optimizat : 0);
155
+ }, 0);
156
+ resultTable.push(['Experience Optimization Variants', count]);
157
+ }
158
+ if ((_ctx$optimizationVari = ctx.optimizationVariantFailures) !== null && _ctx$optimizationVari !== void 0 && _ctx$optimizationVari.experiences) {
159
+ resultTable.push(['Experience Optimization Variant Fetch Failures', ctx.optimizationVariantFailures.experiences]);
160
+ }
161
+ if ((_ctx$data$experienceF = ctx.data.experienceFragments) !== null && _ctx$data$experienceF !== void 0 && _ctx$data$experienceF.some(experienceFragment => experienceFragment.optimizationVariants)) {
162
+ const count = ctx.data.experienceFragments.reduce((sum, experienceFragment) => {
163
+ var _experienceFragment$o, _experienceFragment$o2;
164
+ return sum + ((_experienceFragment$o = (_experienceFragment$o2 = experienceFragment.optimizationVariants) === null || _experienceFragment$o2 === void 0 ? void 0 : _experienceFragment$o2.length) !== null && _experienceFragment$o !== void 0 ? _experienceFragment$o : 0);
165
+ }, 0);
166
+ resultTable.push(['Experience Fragment Optimization Variants', count]);
167
+ }
168
+ if ((_ctx$optimizationVari2 = ctx.optimizationVariantFailures) !== null && _ctx$optimizationVari2 !== void 0 && _ctx$optimizationVari2.experienceFragments) {
169
+ resultTable.push(['Experience Fragment Optimization Variant Fetch Failures', ctx.optimizationVariantFailures.experienceFragments]);
170
+ }
142
171
  console.log(resultTable.toString());
143
172
  } else {
144
173
  console.log('No data was exported');
@@ -32,6 +32,7 @@ function parseOptions(params) {
32
32
  stripTags: false,
33
33
  maxAllowedLimit: 1000,
34
34
  includeExperienceOrchestration: true,
35
+ includeExoVariants: false,
35
36
  saveFile: true,
36
37
  useVerboseRenderer: false,
37
38
  rawProxy: false
@@ -38,6 +38,7 @@ function getFullSourceSpace({
38
38
  includeArchived,
39
39
  maxAllowedLimit,
40
40
  includeExperienceOrchestration,
41
+ includeExoVariants,
41
42
  listrOptions,
42
43
  queryEntries,
43
44
  queryAssets
@@ -260,6 +261,40 @@ function getFullSourceSpace({
260
261
  }
261
262
  }),
262
263
  skip: () => !includeExperienceOrchestration
264
+ }, {
265
+ title: 'Fetching Experience Optimization Variants data',
266
+ task: (0, _contentfulBatchLibs.wrapTask)(async ctx => {
267
+ const experienceFailureCount = await attachOptimizationVariants({
268
+ parents: ctx.data.experiences,
269
+ fetchFn: (parentId, query) => client.experienceVariant.getMany({
270
+ spaceId,
271
+ environmentId,
272
+ experienceId: parentId,
273
+ query
274
+ }),
275
+ entityLabel: 'Experience Optimization Variants'
276
+ });
277
+ ctx.optimizationVariantFailures = ctx.optimizationVariantFailures || {};
278
+ ctx.optimizationVariantFailures.experiences = experienceFailureCount;
279
+ }),
280
+ skip: () => !includeExperienceOrchestration || !includeExoVariants
281
+ }, {
282
+ title: 'Fetching Experience Fragment Optimization Variants data',
283
+ task: (0, _contentfulBatchLibs.wrapTask)(async ctx => {
284
+ const experienceFragmentFailureCount = await attachOptimizationVariants({
285
+ parents: ctx.data.experienceFragments,
286
+ fetchFn: (parentId, query) => client.experienceFragmentVariant.getMany({
287
+ spaceId,
288
+ environmentId,
289
+ experienceFragmentId: parentId,
290
+ query
291
+ }),
292
+ entityLabel: 'Experience Fragment Optimization Variants'
293
+ });
294
+ ctx.optimizationVariantFailures = ctx.optimizationVariantFailures || {};
295
+ ctx.optimizationVariantFailures.experienceFragments = experienceFragmentFailureCount;
296
+ }),
297
+ skip: () => !includeExperienceOrchestration || !includeExoVariants
263
298
  }], listrOptions);
264
299
  }
265
300
  function getEditorInterfaces(client, spaceId, environmentId, contentTypes) {
@@ -305,6 +340,59 @@ async function cursorPagedGet(fetchFn, entityLabel) {
305
340
  return allItems;
306
341
  }
307
342
 
343
+ /**
344
+ * Fetches and attaches Optimization Variants onto each of a parent entity type's
345
+ * items (Experience or ExperienceFragment), mutating them in place as
346
+ * `parent.optimizationVariants`. Variants are nested onto their parent rather than
347
+ * exported as their own top-level field because a variant's `sys.id` is borrowed
348
+ * from its parent (not unique to the variant), so a flat array would collide the
349
+ * way none of the other ExO entity types do.
350
+ *
351
+ * Unlike the top-level ExO entities, the optimization_variants list endpoint is NOT
352
+ * cursor-paginated — it takes no limit/pageNext query params and its response has no
353
+ * total/pages fields, just { sys: { type: 'Array' }, items }. So this is a single
354
+ * request per parent, not a paging loop.
355
+ *
356
+ * The API always leads the list with a `sys.variantType: 'default'` item representing
357
+ * the parent's own base view (either a real record, or the baseline Experience/Fragment
358
+ * itself if none exists) — not a real personalization variant. That entity is already
359
+ * exported as its own top-level Experience/ExperienceFragment, so it's filtered out here
360
+ * to avoid double-counting it as a variant.
361
+ *
362
+ * Runs with bounded concurrency (mirrors getEditorInterfaces' pattern) since this
363
+ * issues one request per parent entity (N+1).
364
+ *
365
+ * Returns the count of parents whose variant fetch failed, so callers can
366
+ * surface that distinctly from a parent that genuinely has zero variants
367
+ * (both look identical as an empty `optimizationVariants` array otherwise).
368
+ */
369
+ async function attachOptimizationVariants({
370
+ parents,
371
+ fetchFn,
372
+ entityLabel
373
+ }) {
374
+ if (!(parents !== null && parents !== void 0 && parents.length)) {
375
+ return 0;
376
+ }
377
+ let totalFetched = 0;
378
+ let failureCount = 0;
379
+ await _bluebird.default.map(parents, async parent => {
380
+ try {
381
+ const response = await fetchFn(parent.sys.id, {});
382
+ parent.optimizationVariants = response.items.filter(item => item.sys.variantType !== 'default');
383
+ totalFetched += parent.optimizationVariants.length;
384
+ _contentfulBatchLibs.logEmitter.emit('info', `Fetched ${totalFetched} ${entityLabel} items`);
385
+ } catch (err) {
386
+ _contentfulBatchLibs.logEmitter.emit('warning', `Skipping ${entityLabel} export for ${parent.sys.id}: ${err.message}`);
387
+ parent.optimizationVariants = [];
388
+ failureCount++;
389
+ }
390
+ }, {
391
+ concurrency: 6
392
+ });
393
+ return failureCount;
394
+ }
395
+
308
396
  /**
309
397
  * Gets all roles. Roles are scheduled to switch from skip/limit to
310
398
  * cursor-based (pages.next/pagePrev) pagination on Feb 15, 2027:
@@ -111,6 +111,10 @@ var _default = exports.default = _yargs.default.version(_package.default.version
111
111
  describe: 'Include Experience Orchestration entities (Components, Experience Templates, Data Assemblies, Experience Fragments, Experiences, Design Tokens). Requires exo_m1 entitlement on the source space.',
112
112
  type: 'boolean',
113
113
  default: true
114
+ }).option('include-exo-variants', {
115
+ describe: 'Include Optimization Variants, nested onto their parent Experience/Experience Fragment as `optimizationVariants`. Requires include-experience-orchestration. Only has an effect if the Contentful Personalization app is installed on the source space.',
116
+ type: 'boolean',
117
+ default: false
114
118
  }).option('header', {
115
119
  alias: 'H',
116
120
  type: 'string',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "contentful-export",
3
- "version": "8.3.0",
3
+ "version": "8.4.0",
4
4
  "description": "this tool allows you to export a space to a JSON dump",
5
5
  "main": "dist/index.js",
6
6
  "types": "types.d.ts",
@@ -52,7 +52,7 @@
52
52
  "cli-table3": "^0.6.0",
53
53
  "contentful": "^11.5.10",
54
54
  "contentful-batch-libs": "^12.0.0",
55
- "contentful-management": "^12.14.0",
55
+ "contentful-management": "^12.17.0",
56
56
  "date-fns": "^4.1.0",
57
57
  "figures": "^3.2.0",
58
58
  "jsonwebtoken": "^9.0.0",