contentful-export 8.2.2 → 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
@@ -18,9 +18,8 @@ var _formatDistance = require("date-fns/formatDistance");
18
18
  var _contentfulBatchLibs = require("contentful-batch-libs");
19
19
  var _downloadAssets = _interopRequireDefault(require("./tasks/download-assets"));
20
20
  var _getSpaceData = _interopRequireDefault(require("./tasks/get-space-data"));
21
- var _initClient = _interopRequireWildcard(require("./tasks/init-client"));
21
+ var _initClient = _interopRequireDefault(require("./tasks/init-client"));
22
22
  var _parseOptions = _interopRequireDefault(require("./parseOptions"));
23
- function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
24
23
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
25
24
  const accessP = _bluebird.default.promisify(_fs.access);
26
25
  const tableOptions = {
@@ -54,7 +53,6 @@ function runContentfulExport(params) {
54
53
  try {
55
54
  // CMA client
56
55
  ctx.client = (0, _initClient.default)(options);
57
- ctx.plainClient = (0, _initClient.initPlainClient)(options);
58
56
  if (options.deliveryToken && !options.includeDrafts) {
59
57
  // CDA client for fetching only public entries
60
58
  ctx.cdaClient = (0, _initClient.default)(options, true);
@@ -69,7 +67,6 @@ function runContentfulExport(params) {
69
67
  task: ctx => {
70
68
  return (0, _getSpaceData.default)({
71
69
  client: ctx.client,
72
- plainClient: ctx.plainClient,
73
70
  cdaClient: ctx.cdaClient,
74
71
  spaceId: options.spaceId,
75
72
  environmentId: options.environmentId,
@@ -85,6 +82,7 @@ function runContentfulExport(params) {
85
82
  skipTags: options.skipTags,
86
83
  stripTags: options.stripTags,
87
84
  includeExperienceOrchestration: options.includeExperienceOrchestration,
85
+ includeExoVariants: options.includeExoVariants,
88
86
  listrOptions,
89
87
  queryEntries: options.queryEntries,
90
88
  queryAssets: options.queryAssets
@@ -134,6 +132,7 @@ function runContentfulExport(params) {
134
132
  }).then(ctx => {
135
133
  const resultTypes = Object.keys(ctx.data);
136
134
  if (resultTypes.length) {
135
+ var _ctx$data$experiences, _ctx$optimizationVari, _ctx$data$experienceF, _ctx$optimizationVari2;
137
136
  const resultTable = new _cliTable.default(tableOptions);
138
137
  resultTable.push([{
139
138
  colSpan: 2,
@@ -142,6 +141,33 @@ function runContentfulExport(params) {
142
141
  resultTypes.forEach(type => {
143
142
  resultTable.push([(0, _lodash.default)(type), ctx.data[type].length]);
144
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
+ }
145
171
  console.log(resultTable.toString());
146
172
  } else {
147
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
@@ -9,6 +9,11 @@ var _contentfulBatchLibs = require("contentful-batch-libs");
9
9
  var _listr = _interopRequireDefault(require("listr"));
10
10
  var _listrVerboseRenderer = _interopRequireDefault(require("listr-verbose-renderer"));
11
11
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
12
+ function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
13
+ function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
14
+ function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
15
+ function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; }
16
+ function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
12
17
  const MAX_ALLOWED_LIMIT = 1000;
13
18
  let pageLimit = MAX_ALLOWED_LIMIT;
14
19
 
@@ -18,7 +23,6 @@ let pageLimit = MAX_ALLOWED_LIMIT;
18
23
  */
19
24
  function getFullSourceSpace({
20
25
  client,
21
- plainClient,
22
26
  cdaClient,
23
27
  spaceId,
24
28
  environmentId = 'master',
@@ -34,6 +38,7 @@ function getFullSourceSpace({
34
38
  includeArchived,
35
39
  maxAllowedLimit,
36
40
  includeExperienceOrchestration,
41
+ includeExoVariants,
37
42
  listrOptions,
38
43
  queryEntries,
39
44
  queryAssets
@@ -43,22 +48,32 @@ function getFullSourceSpace({
43
48
  renderer: _listrVerboseRenderer.default
44
49
  };
45
50
  return new _listr.default([{
46
- title: 'Connecting to space',
47
- task: (0, _contentfulBatchLibs.wrapTask)(ctx => {
48
- return client.getSpace(spaceId).then(space => {
49
- ctx.space = space;
50
- return space.getEnvironment(environmentId);
51
- }).then(environment => {
52
- ctx.environment = environment;
53
- });
51
+ title: 'Connecting to space/environment',
52
+ task: (0, _contentfulBatchLibs.wrapTask)(async () => {
53
+ try {
54
+ await client.space.get({
55
+ spaceId
56
+ });
57
+ } catch (err) {
58
+ throw new Error(`Unable to retrieve space ${spaceId}, please ensure the space exists and the token is valid. (${err.message})`);
59
+ }
60
+ try {
61
+ await client.environment.get({
62
+ spaceId,
63
+ environmentId
64
+ });
65
+ } catch (err) {
66
+ throw new Error(`Unable to retrieve environment ${environmentId}, please ensure the environment exists and the token is valid. (${err.message})`);
67
+ }
54
68
  })
55
69
  }, {
56
70
  title: 'Fetching content types data',
57
71
  task: (0, _contentfulBatchLibs.wrapTask)(ctx => {
58
- return pagedGet({
59
- source: ctx.environment,
60
- method: 'getContentTypes'
61
- }).then(extractItems).then(items => {
72
+ return pagedGet(query => client.contentType.getMany({
73
+ spaceId,
74
+ environmentId,
75
+ query
76
+ })).then(extractItems).then(items => {
62
77
  ctx.data.contentTypes = items;
63
78
  });
64
79
  }),
@@ -66,10 +81,11 @@ function getFullSourceSpace({
66
81
  }, {
67
82
  title: 'Fetching tags data',
68
83
  task: (0, _contentfulBatchLibs.wrapTask)(ctx => {
69
- return pagedGet({
70
- source: ctx.environment,
71
- method: 'getTags'
72
- }).then(extractItems).then(items => {
84
+ return pagedGet(query => client.tag.getMany({
85
+ spaceId,
86
+ environmentId,
87
+ query
88
+ })).then(extractItems).then(items => {
73
89
  ctx.data.tags = items;
74
90
  }).catch(err => {
75
91
  _contentfulBatchLibs.logEmitter.emit('error', new Error(`Fetching tags failed: ${err.message}`, {
@@ -82,7 +98,7 @@ function getFullSourceSpace({
82
98
  }, {
83
99
  title: 'Fetching editor interfaces data',
84
100
  task: (0, _contentfulBatchLibs.wrapTask)(ctx => {
85
- return getEditorInterfaces(ctx.data.contentTypes).then(editorInterfaces => {
101
+ return getEditorInterfaces(client, spaceId, environmentId, ctx.data.contentTypes).then(editorInterfaces => {
86
102
  ctx.data.editorInterfaces = editorInterfaces.filter(editorInterface => {
87
103
  return editorInterface !== null;
88
104
  });
@@ -92,17 +108,17 @@ function getFullSourceSpace({
92
108
  }, {
93
109
  title: 'Fetching content entries data',
94
110
  task: (0, _contentfulBatchLibs.wrapTask)(ctx => {
95
- const source = (cdaClient === null || cdaClient === void 0 ? void 0 : cdaClient.withAllLocales) || ctx.environment;
96
111
  if (cdaClient) {
97
112
  // let's not fetch children when using Content Delivery API
98
113
  queryEntries = queryEntries || {};
99
114
  queryEntries.include = 0;
100
115
  }
101
- return pagedGet({
102
- source,
103
- method: 'getEntries',
104
- query: queryEntries
105
- }).then(extractItems).then(items => filterDrafts(items, includeDrafts, cdaClient)).then(items => filterArchived(items, includeArchived)).then(items => removeTags(items, stripTags)).then(items => {
116
+ const fetchFn = cdaClient ? query => cdaClient.withAllLocales.getEntries(query) : query => client.entry.getMany({
117
+ spaceId,
118
+ environmentId,
119
+ query
120
+ });
121
+ return pagedGet(fetchFn, queryEntries).then(extractItems).then(items => filterDrafts(items, includeDrafts, cdaClient)).then(items => filterArchived(items, includeArchived)).then(items => removeTags(items, stripTags)).then(items => {
106
122
  ctx.data.entries = items;
107
123
  });
108
124
  }),
@@ -110,13 +126,13 @@ function getFullSourceSpace({
110
126
  }, {
111
127
  title: 'Fetching assets data',
112
128
  task: (0, _contentfulBatchLibs.wrapTask)(ctx => {
113
- const source = (cdaClient === null || cdaClient === void 0 ? void 0 : cdaClient.withAllLocales) || ctx.environment;
114
129
  queryAssets = queryAssets || {};
115
- return pagedGet({
116
- source,
117
- method: 'getAssets',
118
- query: queryAssets
119
- }).then(extractItems).then(items => filterDrafts(items, includeDrafts, cdaClient)).then(items => filterArchived(items, includeArchived)).then(items => removeTags(items, stripTags)).then(items => {
130
+ const fetchFn = cdaClient ? query => cdaClient.withAllLocales.getAssets(query) : query => client.asset.getMany({
131
+ spaceId,
132
+ environmentId,
133
+ query
134
+ });
135
+ return pagedGet(fetchFn, queryAssets).then(extractItems).then(items => filterDrafts(items, includeDrafts, cdaClient)).then(items => filterArchived(items, includeArchived)).then(items => removeTags(items, stripTags)).then(items => {
120
136
  ctx.data.assets = items;
121
137
  });
122
138
  }),
@@ -124,33 +140,34 @@ function getFullSourceSpace({
124
140
  }, {
125
141
  title: 'Fetching locales data',
126
142
  task: (0, _contentfulBatchLibs.wrapTask)(ctx => {
127
- return pagedGet({
128
- source: ctx.environment,
129
- method: 'getLocales'
130
- }).then(extractItems).then(items => {
143
+ return pagedGet(query => client.locale.getMany({
144
+ spaceId,
145
+ environmentId,
146
+ query
147
+ })).then(extractItems).then(items => {
131
148
  ctx.data.locales = items;
132
149
  });
133
150
  }),
134
151
  skip: () => skipContentModel
135
152
  }, {
136
153
  title: 'Fetching webhooks data',
137
- task: (0, _contentfulBatchLibs.wrapTask)(ctx => {
138
- return pagedGet({
139
- source: ctx.space,
140
- method: 'getWebhooks'
141
- }).then(extractItems).then(items => {
142
- ctx.data.webhooks = items;
154
+ task: (0, _contentfulBatchLibs.wrapTask)(async ctx => {
155
+ const webhooksResponse = await client.webhook.getMany({
156
+ query: {
157
+ // webhooks are capped to 100 per space
158
+ limit: 100
159
+ },
160
+ spaceId
143
161
  });
162
+ ctx.data.webhooks = webhooksResponse.items;
144
163
  }),
145
164
  skip: () => skipWebhooks || environmentId !== 'master' && 'Webhooks can only be exported from master environment'
146
165
  }, {
147
166
  title: 'Fetching roles data',
148
- task: (0, _contentfulBatchLibs.wrapTask)(ctx => {
149
- return pagedGet({
150
- source: ctx.space,
151
- method: 'getRoles'
152
- }).then(extractItems).then(items => {
153
- ctx.data.roles = items;
167
+ task: (0, _contentfulBatchLibs.wrapTask)(async ctx => {
168
+ ctx.data.roles = await pagedRolesGet({
169
+ client,
170
+ spaceId
154
171
  });
155
172
  }),
156
173
  skip: () => skipRoles || environmentId !== 'master' && 'Roles can only be exported from master environment'
@@ -158,12 +175,11 @@ function getFullSourceSpace({
158
175
  title: 'Fetching Design Tokens data',
159
176
  task: (0, _contentfulBatchLibs.wrapTask)(async ctx => {
160
177
  try {
161
- ctx.data.designTokens = await cursorPagedGet({
162
- client: plainClient,
178
+ ctx.data.designTokens = await cursorPagedGet(query => client.designToken.getMany({
163
179
  spaceId,
164
180
  environmentId,
165
- method: 'designToken.getMany'
166
- });
181
+ query
182
+ }), 'designToken');
167
183
  } catch (err) {
168
184
  _contentfulBatchLibs.logEmitter.emit('warning', `Skipping Design Tokens export: ${err.message}`);
169
185
  ctx.data.designTokens = [];
@@ -174,12 +190,11 @@ function getFullSourceSpace({
174
190
  title: 'Fetching Components data',
175
191
  task: (0, _contentfulBatchLibs.wrapTask)(async ctx => {
176
192
  try {
177
- ctx.data.components = await cursorPagedGet({
178
- client: plainClient,
193
+ ctx.data.components = await cursorPagedGet(query => client.component.getMany({
179
194
  spaceId,
180
195
  environmentId,
181
- method: 'component.getMany'
182
- });
196
+ query
197
+ }), 'component');
183
198
  } catch (err) {
184
199
  _contentfulBatchLibs.logEmitter.emit('warning', `Skipping Components export: ${err.message}`);
185
200
  ctx.data.components = [];
@@ -190,12 +205,11 @@ function getFullSourceSpace({
190
205
  title: 'Fetching Experience Templates data',
191
206
  task: (0, _contentfulBatchLibs.wrapTask)(async ctx => {
192
207
  try {
193
- ctx.data.experienceTemplates = await cursorPagedGet({
194
- client: plainClient,
208
+ ctx.data.experienceTemplates = await cursorPagedGet(query => client.experienceTemplate.getMany({
195
209
  spaceId,
196
210
  environmentId,
197
- method: 'experienceTemplate.getMany'
198
- });
211
+ query
212
+ }), 'experienceTemplate');
199
213
  } catch (err) {
200
214
  _contentfulBatchLibs.logEmitter.emit('warning', `Skipping Experience Templates export: ${err.message}`);
201
215
  ctx.data.experienceTemplates = [];
@@ -206,12 +220,11 @@ function getFullSourceSpace({
206
220
  title: 'Fetching Data Assemblies data',
207
221
  task: (0, _contentfulBatchLibs.wrapTask)(async ctx => {
208
222
  try {
209
- ctx.data.dataAssemblies = await cursorPagedGet({
210
- client: plainClient,
223
+ ctx.data.dataAssemblies = await cursorPagedGet(query => client.dataAssembly.getMany({
211
224
  spaceId,
212
225
  environmentId,
213
- method: 'dataAssembly.getMany'
214
- });
226
+ query
227
+ }), 'dataAssembly');
215
228
  } catch (err) {
216
229
  _contentfulBatchLibs.logEmitter.emit('warning', `Skipping Data Assemblies export: ${err.message}`);
217
230
  ctx.data.dataAssemblies = [];
@@ -222,12 +235,11 @@ function getFullSourceSpace({
222
235
  title: 'Fetching Experience Fragments data',
223
236
  task: (0, _contentfulBatchLibs.wrapTask)(async ctx => {
224
237
  try {
225
- ctx.data.experienceFragments = await cursorPagedGet({
226
- client: plainClient,
238
+ ctx.data.experienceFragments = await cursorPagedGet(query => client.experienceFragment.getMany({
227
239
  spaceId,
228
240
  environmentId,
229
- method: 'experienceFragment.getMany'
230
- });
241
+ query
242
+ }), 'experienceFragment');
231
243
  } catch (err) {
232
244
  _contentfulBatchLibs.logEmitter.emit('warning', `Skipping Experience Fragments export: ${err.message}`);
233
245
  ctx.data.experienceFragments = [];
@@ -238,23 +250,60 @@ function getFullSourceSpace({
238
250
  title: 'Fetching Experiences data',
239
251
  task: (0, _contentfulBatchLibs.wrapTask)(async ctx => {
240
252
  try {
241
- ctx.data.experiences = await cursorPagedGet({
242
- client: plainClient,
253
+ ctx.data.experiences = await cursorPagedGet(query => client.experience.getMany({
243
254
  spaceId,
244
255
  environmentId,
245
- method: 'experience.getMany'
246
- });
256
+ query
257
+ }), 'experience');
247
258
  } catch (err) {
248
259
  _contentfulBatchLibs.logEmitter.emit('warning', `Skipping Experiences export: ${err.message}`);
249
260
  ctx.data.experiences = [];
250
261
  }
251
262
  }),
252
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
253
298
  }], listrOptions);
254
299
  }
255
- function getEditorInterfaces(contentTypes) {
300
+ function getEditorInterfaces(client, spaceId, environmentId, contentTypes) {
256
301
  return _bluebird.default.map(contentTypes, contentType => {
257
- return contentType.getEditorInterface().then(editorInterface => {
302
+ return client.editorInterface.get({
303
+ spaceId,
304
+ environmentId,
305
+ contentTypeId: contentType.sys.id
306
+ }).then(editorInterface => {
258
307
  _contentfulBatchLibs.logEmitter.emit('info', `Fetched editor interface for ${contentType.name}`);
259
308
  return editorInterface;
260
309
  }).catch(() => {
@@ -272,55 +321,154 @@ function getEditorInterfaces(contentTypes) {
272
321
  * Gets all ExO entities using cursor-based pagination (pageNext/pagePrev tokens).
273
322
  * ExO list endpoints do not support skip-based pagination or the order param.
274
323
  */
275
- async function cursorPagedGet({
276
- client,
277
- spaceId,
278
- environmentId,
279
- method
280
- }) {
281
- const [entity, operation] = method.split('.');
324
+ async function cursorPagedGet(fetchFn, entityLabel) {
282
325
  const allItems = [];
283
326
  let pageNext = null;
284
327
  do {
285
328
  var _response$pages$next, _response$pages;
286
329
  const query = {
287
- spaceId,
288
- environmentId,
289
330
  limit: pageLimit
290
331
  };
291
332
  if (pageNext) {
292
333
  query.pageNext = pageNext;
293
334
  }
294
- const response = await client[entity][operation](query);
335
+ const response = await fetchFn(query);
295
336
  allItems.push(...response.items);
296
- _contentfulBatchLibs.logEmitter.emit('info', `Fetched ${allItems.length} ${entity} items`);
337
+ _contentfulBatchLibs.logEmitter.emit('info', `Fetched ${allItems.length} ${entityLabel} items`);
297
338
  pageNext = (_response$pages$next = (_response$pages = response.pages) === null || _response$pages === void 0 ? void 0 : _response$pages.next) !== null && _response$pages$next !== void 0 ? _response$pages$next : null;
298
339
  } while (pageNext);
299
340
  return allItems;
300
341
  }
301
342
 
302
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
+
396
+ /**
397
+ * Gets all roles. Roles are scheduled to switch from skip/limit to
398
+ * cursor-based (pages.next/pagePrev) pagination on Feb 15, 2027:
399
+ * https://www.contentful.com/developers/api-changes/space-roles-collection-endpoints-update/
400
+ * Handles both shapes so this keeps working across that migration.
401
+ */
402
+ async function pagedRolesGet({
403
+ client,
404
+ spaceId
405
+ }) {
406
+ const allItems = [];
407
+ const order = 'sys.createdAt,sys.id';
408
+ let response = await client.role.getMany({
409
+ spaceId,
410
+ query: {
411
+ limit: pageLimit,
412
+ order
413
+ }
414
+ });
415
+ allItems.push(...response.items);
416
+ _contentfulBatchLibs.logEmitter.emit('info', `Fetched ${allItems.length} roles`);
417
+ if (response.pages !== undefined) {
418
+ var _response$pages$next2, _response$pages2;
419
+ let pageNext = (_response$pages$next2 = (_response$pages2 = response.pages) === null || _response$pages2 === void 0 ? void 0 : _response$pages2.next) !== null && _response$pages$next2 !== void 0 ? _response$pages$next2 : null;
420
+ while (pageNext) {
421
+ var _response$pages$next3, _response$pages3;
422
+ response = await client.role.getMany({
423
+ spaceId,
424
+ query: {
425
+ limit: pageLimit,
426
+ order,
427
+ pageNext
428
+ }
429
+ });
430
+ allItems.push(...response.items);
431
+ _contentfulBatchLibs.logEmitter.emit('info', `Fetched ${allItems.length} roles`);
432
+ pageNext = (_response$pages$next3 = (_response$pages3 = response.pages) === null || _response$pages3 === void 0 ? void 0 : _response$pages3.next) !== null && _response$pages$next3 !== void 0 ? _response$pages$next3 : null;
433
+ }
434
+ } else {
435
+ let skip = allItems.length;
436
+ while (allItems.length < response.total && response.items.length > 0) {
437
+ response = await client.role.getMany({
438
+ spaceId,
439
+ query: {
440
+ limit: pageLimit,
441
+ order,
442
+ skip
443
+ }
444
+ });
445
+ allItems.push(...response.items);
446
+ _contentfulBatchLibs.logEmitter.emit('info', `Fetched ${allItems.length} roles`);
447
+ skip += response.items.length;
448
+ }
449
+ }
450
+ return allItems;
451
+ }
452
+
453
+ /**
454
+ * Recursively fetches all pages of a offset-based paginated CMA collection and returns
455
+ * them merged into a single response shaped like `{ items: [...], total, ... }`
456
+ *
303
457
  * Gets all the existing entities based on pagination parameters.
304
458
  * The first call will have no aggregated response. Subsequent calls will
305
459
  * concatenate the new responses to the original one.
306
460
  */
307
- function pagedGet({
308
- source,
309
- method,
310
- skip = 0,
311
- aggregatedResponse = null,
312
- query = null
313
- }) {
314
- const userQueryLimit = query && query.limit;
461
+ function pagedGet(fetchFn, userQuery = null, skip = 0, aggregatedResponse = null) {
462
+ const userQueryLimit = userQuery && userQuery.limit;
315
463
  const fetchedTotal = aggregatedResponse && aggregatedResponse.items.length;
316
464
  const limit = userQueryLimit ? Math.min(pageLimit, userQueryLimit - fetchedTotal) : pageLimit;
317
- const requestQuery = Object.assign({}, {
465
+ const query = _objectSpread(_objectSpread({
318
466
  skip,
319
467
  order: 'sys.createdAt,sys.id'
320
- }, query, {
468
+ }, userQuery), {}, {
321
469
  limit
322
470
  });
323
- return source[method](requestQuery).then(response => {
471
+ return fetchFn(query).then(response => {
324
472
  if (!aggregatedResponse) {
325
473
  aggregatedResponse = response;
326
474
  } else {
@@ -328,20 +476,14 @@ function pagedGet({
328
476
  }
329
477
  const totalItemsLength = aggregatedResponse.items.length;
330
478
  const total = response.total;
331
- logPagingStatus(response, requestQuery, userQueryLimit);
479
+ logPagingStatus(response, query, userQueryLimit);
332
480
  const gotAllQueryLimitedItems = userQueryLimit && totalItemsLength >= userQueryLimit;
333
481
  const gotAllItems = totalItemsLength >= total;
334
482
  const gotNoItems = totalItemsLength <= 0;
335
483
  if (gotAllQueryLimitedItems || gotAllItems || gotNoItems) {
336
484
  return aggregatedResponse;
337
485
  }
338
- return pagedGet({
339
- source,
340
- method,
341
- skip: skip + response.items.length,
342
- aggregatedResponse,
343
- query
344
- });
486
+ return pagedGet(fetchFn, userQuery, skip + response.items.length, aggregatedResponse);
345
487
  });
346
488
  }
347
489
  function logPagingStatus(response, requestQuery, userLimit) {
@@ -4,7 +4,6 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.default = initClient;
7
- exports.initPlainClient = initPlainClient;
8
7
  var _contentful = require("contentful");
9
8
  var _contentfulBatchLibs = require("contentful-batch-libs");
10
9
  var _contentfulManagement = require("contentful-management");
@@ -32,13 +31,10 @@ function initClient(opts, useCda = false) {
32
31
  return (0, _contentful.createClient)(cdaConfig).withoutLinkResolution;
33
32
  }
34
33
  return (0, _contentfulManagement.createClient)(config, {
35
- type: 'legacy'
34
+ defaults: {
35
+ spaceId: config.spaceId,
36
+ environmentId: config.environmentId
37
+ }
36
38
  });
37
39
  }
38
- function initPlainClient(opts) {
39
- return (0, _contentfulManagement.createClient)({
40
- accessToken: opts.managementToken,
41
- host: opts.host,
42
- logHandler
43
- });
44
- }
40
+ module.exports = exports.default;
@@ -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.2.2",
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",