@remkoj/optimizely-cms-cli 5.1.5 → 6.0.0-pre10

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/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
- import { globSync, glob } from 'glob';
2
- import dotenv from 'dotenv';
1
+ import { globSync, glob, globIterate } from 'glob';
2
+ import { config } from 'dotenv';
3
3
  import { expand } from 'dotenv-expand';
4
- import { getCmsIntegrationApiConfigFromEnvironment, createClient, OptiCmsVersion, IntegrationApi, ContentRoots } from '@remkoj/optimizely-cms-api';
4
+ import { readPartialEnvConfig, createClient, OptiCmsVersion, ContentRoots } from '@remkoj/optimizely-cms-api';
5
5
  import yargs from 'yargs';
6
6
  import chalk from 'chalk';
7
7
  import path from 'node:path';
@@ -10,6 +10,11 @@ import figures from 'figures';
10
10
  import Table from 'cli-table3';
11
11
  import fsAsync from 'node:fs/promises';
12
12
  import { input, select, confirm } from '@inquirer/prompts';
13
+ import createDeepMerge from '@fastify/deepmerge';
14
+ import { Ajv } from 'ajv';
15
+ import addFormats from 'ajv-formats';
16
+ import GraphQLGen from '@remkoj/optimizely-graph-functions/contenttype-loader';
17
+ import deepEqual from 'fast-deep-equal';
13
18
 
14
19
  /**
15
20
  * Prepare the application context, by parsing the .env files in the main
@@ -18,19 +23,25 @@ import { input, select, confirm } from '@inquirer/prompts';
18
23
  * @returns A string array with the files processed
19
24
  */
20
25
  function prepare() {
21
- const envFiles = globSync(".env*").sort((a, b) => b.length - a.length).filter(n => n == ".env" || n == ".env.local" || (process.env.NODE_ENV && n == `.env.${process.env.NODE_ENV}.local`));
22
- expand(dotenv.config({ path: envFiles, debug: false, quiet: true }));
26
+ const globResult = globSync(".env*");
27
+ const envFiles = globResult.sort((a, b) => b.length - a.length).filter(n => n == ".env" || n == ".env.local" || (process.env.NODE_ENV && n == `.env.${process.env.NODE_ENV}.local`));
28
+ expand(config({ path: envFiles, debug: false, quiet: true }));
23
29
  return envFiles;
24
30
  }
25
31
 
26
32
  function createOptiCmsApp(scriptName, version, epilogue, envFiles) {
33
+ if (envFiles) {
34
+ process.stdout.write(chalk.bold(`✅ Loaded environment files:`) + "\n");
35
+ envFiles.forEach(envFile => {
36
+ process.stdout.write(` 👉 ${envFile}\n`);
37
+ });
38
+ process.stdout.write("\n");
39
+ }
27
40
  let config;
28
41
  try {
29
- config = getCmsIntegrationApiConfigFromEnvironment();
42
+ config = readPartialEnvConfig();
30
43
  }
31
44
  catch (e) {
32
- if (envFiles)
33
- console.error(chalk.gray(`Included environment files: ${envFiles.join(', ')}`));
34
45
  console.error(chalk.redBright(`${chalk.bold("[ERROR]:")} Error processing environment variables: ${e.message}`));
35
46
  process.exit(1);
36
47
  }
@@ -40,7 +51,7 @@ function createOptiCmsApp(scriptName, version, epilogue, envFiles) {
40
51
  .usage('$0 <cmd> [args]')
41
52
  .option("path", { alias: "p", description: "Application root folder", string: true, type: "string", demandOption: false, default: process.cwd() })
42
53
  .option("components", { alias: "c", description: "Path to components folder", string: true, type: "string", demandOption: false, default: "./src/components/cms" })
43
- .option("cms_url", { alias: "cu", description: "Optimizely CMS URL", string: true, type: "string", demandOption: isDemanded(config.base), default: config.base, coerce: (val) => new URL(val) })
54
+ .option("cms_url", { alias: "cu", description: "Optimizely CMS URL", string: true, type: "string", demandOption: isDemanded(config.base), default: config.base, coerce: (val) => val ? new URL(val) : undefined })
44
55
  .option("client_id", { alias: "ci", description: "API Client ID", string: true, type: "string", demandOption: isDemanded(config.clientId), default: config.clientId })
45
56
  .option('client_secret', { alias: "cs", description: "API Client Secrent", string: true, type: "string", demandOption: isDemanded(config.clientSecret), default: config.clientSecret })
46
57
  .option('user_id', { alias: "u", description: "Impersonate user id", string: true, type: "string", demandOption: false, default: config.actAs })
@@ -52,12 +63,10 @@ function createOptiCmsApp(scriptName, version, epilogue, envFiles) {
52
63
  .epilogue(`Copyright Remko Jantzen - 2023-${(new Date(Date.now())).getFullYear()}`)
53
64
  .help()
54
65
  .fail((msg, error, args) => {
55
- if (envFiles)
56
- console.error(chalk.gray(`Included environment files: ${envFiles.join(', ')}\n`));
57
66
  if (msg)
58
- console.error(msg + "\n");
67
+ console.error(chalk.redBright(msg) + "\n");
59
68
  if (error)
60
- console.error(`[${chalk.bold(error.name ?? 'Error')}]: ${error.message ?? ''}\n`);
69
+ console.error(chalk.redBright(`[${chalk.bold(error.name ?? 'Error')}]: ${error.message ?? ''}`) + '\n');
61
70
  args.showHelp("error");
62
71
  });
63
72
  }
@@ -106,16 +115,23 @@ function parseArgs({ client_id, client_secret, cms_url, user_id, verbose, path:
106
115
  }
107
116
 
108
117
  function createCmsClient(args) {
109
- const { _config: cfg } = parseArgs(args);
110
- const baseConfig = getCmsIntegrationApiConfigFromEnvironment();
118
+ const cfg = getCmsIntegrationApiOptions(args);
119
+ const baseConfig = readPartialEnvConfig();
111
120
  const client = createClient({
112
121
  ...baseConfig,
113
122
  ...cfg
114
123
  });
115
124
  if (cfg.debug)
116
- process.stdout.write(chalk.gray(`${figures.arrowRight} Connecting to ${cfg.base.href} as ${cfg.actAs ?? cfg.clientId}\n`));
125
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Connecting to ${client.cmsUrl} as ${cfg.actAs ?? cfg.clientId}\n`));
117
126
  return client;
118
127
  }
128
+ function getCmsIntegrationApiOptions(args) {
129
+ if (typeof args !== 'object' || args === null)
130
+ return undefined;
131
+ if (args.base && (typeof args.base === 'object') && (typeof args.base.href === 'string'))
132
+ return args;
133
+ return parseArgs(args)._config;
134
+ }
119
135
 
120
136
  const StylesPushCommand = {
121
137
  command: "styles:push",
@@ -150,7 +166,12 @@ const StylesPushCommand = {
150
166
  return undefined; // Only include defined styles, if any
151
167
  if (cfg.debug)
152
168
  process.stdout.write(chalk.gray(`${figures.arrowRight} Pushing: ${styleKey}\n`));
153
- const newTemplate = await client.displayTemplates.displayTemplatesPut(styleKey, styleDefinition);
169
+ // Try to fetch the current template
170
+ const currentTemplate = await client.displayTemplatesGet({ path: { key: styleKey } }).catch(() => { return undefined; });
171
+ // Create / Replace the current template
172
+ const newTemplate = await (currentTemplate ?
173
+ client.displayTemplatesPatch({ path: { key: styleKey }, body: styleDefinition }) :
174
+ client.displayTemplatesCreate({ body: styleDefinition }));
154
175
  return newTemplate;
155
176
  }))).filter(isNotNullOrUndefined);
156
177
  const styles = new Table({
@@ -190,9 +211,61 @@ function isNotNullOrUndefined(i) {
190
211
  return i ? true : false;
191
212
  }
192
213
 
214
+ /**
215
+ * This file contains tools that allow using the project that we're targeting
216
+ */
217
+ /**
218
+ * Get all the file paths for the content type, taking the current project configuration into
219
+ * account.
220
+ *
221
+ * @param contentType
222
+ * @param basePath
223
+ * @returns
224
+ */
225
+ function getContentTypePaths(contentType, basePath, createFolder = false, debug = false) {
226
+ const baseTypeSlug = typeToSlug(contentType.baseType);
227
+ const typePath = path.join(basePath, baseTypeSlug, contentType.key);
228
+ if (createFolder) {
229
+ if (!fs.existsSync(typePath)) {
230
+ fs.mkdirSync(typePath, { recursive: true });
231
+ if (debug)
232
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Created folder ${typePath} for Content-Type ${contentType.key}\n`));
233
+ }
234
+ // Check folders
235
+ if (!fs.statSync(typePath).isDirectory())
236
+ throw new Error(`The folder ${typePath} for Content-Type ${contentType.key} exists, but is not a directory!`);
237
+ }
238
+ const typeFile = path.join(typePath, `${contentType.key}.opti-type.json`);
239
+ const fragmentFile = path.join(typePath, `${contentType.key}.${baseTypeSlug}.graphql`);
240
+ const propertyFragmentFile = path.join(typePath, `${contentType.key}.property.graphql`);
241
+ const queryFile = path.join(typePath, `get${contentType.key}Data.query.graphql`);
242
+ const componentFile = path.join(typePath, `index.tsx`);
243
+ return {
244
+ type: contentType.key,
245
+ path: typePath,
246
+ typePath,
247
+ typeFile,
248
+ fragmentFile,
249
+ componentFile,
250
+ propertyFragmentFile,
251
+ queryFile
252
+ };
253
+ }
254
+ /**
255
+ *
256
+ * @param typeKey
257
+ * @returns
258
+ */
259
+ function typeToSlug(typeKey) {
260
+ let typeKeySlug = typeKey.toLowerCase();
261
+ if (typeKeySlug.startsWith('_'))
262
+ typeKeySlug = typeKeySlug.substring(1);
263
+ return typeKeySlug;
264
+ }
265
+
193
266
  const ContentTypesArgsDefaults = {
194
- excludeBaseTypes: [],
195
- excludeTypes: ['folder', 'media', 'image', 'video'],
267
+ excludeBaseTypes: ['folder', 'media', 'image', 'video'],
268
+ excludeTypes: [],
196
269
  baseTypes: [],
197
270
  types: [],
198
271
  all: false
@@ -205,72 +278,92 @@ const contentTypesBuilder = (yargs, defaults = ContentTypesArgsDefaults) => {
205
278
  yargs.option('all', { alias: 'a', description: "Include non-supported base types", boolean: true, type: 'boolean', demandOption: false, default: defaults.all });
206
279
  return yargs;
207
280
  };
208
- function getEnumOptions(enumObject) {
209
- return Object.keys(enumObject).filter((item) => {
210
- return isNaN(Number(item));
211
- });
212
- }
213
- async function getContentTypes(client, args, pageSize = 100, allowSystem = false) {
281
+ async function getContentTypes(client, args, pageSize = 25, allowSystem = false, customFilter) {
214
282
  const { _config: cfg, excludeBaseTypes, excludeTypes, baseTypes, types, all } = parseArgs(args);
215
- process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Pulling Content Types from Optimizely CMS\n`));
216
- if (cfg.debug)
217
- process.stdout.write(chalk.gray(`${figures.arrowRight} Fetching page 1 of ? (${pageSize} items per page)\n`));
218
- let resultsPage = await client.contentTypes.contentTypesList(undefined, undefined, 0, pageSize);
219
- const results = resultsPage.items ?? [];
220
- let pagesRemaining = Math.ceil(resultsPage.totalItemCount / resultsPage.pageSize) - (resultsPage.pageIndex + 1);
221
- while (pagesRemaining > 0 && results.length < resultsPage.totalItemCount) {
222
- if (cfg.debug)
223
- process.stdout.write(chalk.gray(`${figures.arrowRight} Fetching page ${resultsPage.pageIndex + 2} of ${Math.ceil(resultsPage.totalItemCount / resultsPage.pageSize)} (${resultsPage.pageSize} items per page)\n`));
224
- resultsPage = await client.contentTypes.contentTypesList(undefined, undefined, resultsPage.pageIndex + 1, resultsPage.pageSize);
225
- results.push(...resultsPage.items);
226
- pagesRemaining = Math.ceil(resultsPage.totalItemCount / resultsPage.pageSize) - (resultsPage.pageIndex + 1);
227
- }
228
- if (cfg.debug) {
229
- process.stdout.write(chalk.gray(`${figures.arrowRight} Fetched ${results.length} Content-Types from Optimizely CMS\n`));
230
- process.stdout.write(chalk.gray(`${figures.arrowRight} Filtering Content-Types based upon arguments\n`));
231
- }
232
- const validBaseTypes = getEnumOptions(IntegrationApi.ContentBaseType).map(x => x.toLowerCase());
233
- const allContentTypes = all ?
234
- // If we're returning all content types, including non-supported base types, make sure the base type is always set
235
- results.map(contentType => {
236
- return {
237
- ...contentType,
238
- baseType: contentType.baseType ?? 'default'
239
- };
240
- }) :
241
- // Otherwise filter out any non supported content base type
242
- results.filter(contentType => {
243
- const baseType = (contentType.baseType ?? 'default').toLowerCase();
244
- const isValid = validBaseTypes.includes(baseType);
245
- if (!isValid && cfg.debug)
246
- process.stdout.write(chalk.gray(`${figures.arrowRight} Removing ${contentType.key} as it has an unsupported base type: ${baseType}\n`));
247
- return isValid;
248
- });
249
- const contentTypes = allContentTypes.filter(data => {
250
- // Remove items based upon filters
251
- const keepType = (!excludeBaseTypes.includes(data.baseType)) &&
252
- (!excludeTypes.includes(data.key)) &&
253
- (baseTypes.length == 0 || baseTypes.includes(data.baseType)) &&
254
- (types.length == 0 || types.includes(data.key));
255
- if (!keepType) {
283
+ const allContentTypes = [];
284
+ const filteredContentTypes = [];
285
+ for await (const contentType of getAllContentTypes(client, cfg.debug, pageSize)) {
286
+ // Skip content types mapped against Graph data, these should be used with their source type in Graph, not the reference in CMS
287
+ if (!all && contentType.key.toLowerCase().startsWith('graph:')) {
256
288
  if (cfg.debug)
257
- process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Content-Type ${data.key} due to applied filters\n`));
258
- return false;
289
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${contentType.key} as it is a reference to external data in Optimizely Graph\n`));
290
+ continue;
259
291
  }
260
- // Skip system types if desired
261
- if (data.source == 'system' && !allowSystem) {
292
+ // Build the unfiltered array
293
+ allContentTypes.push(contentType);
294
+ // Skip based upon base type filters
295
+ if (!shouldInclude(typeToSlug(contentType.baseType), baseTypes.map(x => typeToSlug(x)), excludeBaseTypes.map(x => typeToSlug(x)))) {
262
296
  if (cfg.debug)
263
- process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Content-Type ${data.key} due to it being a system type\n`));
264
- return false;
297
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${contentType.key} as it has a restricted base type: ${contentType.baseType}\n`));
298
+ continue;
265
299
  }
266
- return true;
267
- });
300
+ // Skip based upon type filters
301
+ if (!shouldInclude(contentType.key, types, excludeTypes)) {
302
+ if (cfg.debug)
303
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${contentType.key} as it has a restricted type: ${contentType.key}\n`));
304
+ continue;
305
+ }
306
+ // Skip based upon system filter
307
+ if (contentType.source == 'system' && !allowSystem) {
308
+ if (cfg.debug)
309
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Content-Type ${contentType.key} due to it being a system type\n`));
310
+ continue;
311
+ }
312
+ // Skip based upon custom filter
313
+ if (customFilter && !await customFilter(contentType)) {
314
+ if (cfg.debug)
315
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Content-Type ${contentType.key} due to the custom filter\n`));
316
+ continue;
317
+ }
318
+ // Add to list
319
+ filteredContentTypes.push(contentType);
320
+ }
268
321
  if (cfg.debug)
269
- process.stdout.write(chalk.gray(`${figures.arrowRight} Applied content type filters, reduced from ${results.length} to ${contentTypes.length} items\n`));
270
- return {
271
- all: allContentTypes,
272
- contentTypes: contentTypes
273
- };
322
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Applied content type filters, reduced from ${allContentTypes.length} to ${filteredContentTypes.length} items\n`));
323
+ return { all: allContentTypes, contentTypes: filteredContentTypes };
324
+ }
325
+ function shouldInclude(value, allow, disallow) {
326
+ // Item is allowed when either allow is not set, an empty array or has the value
327
+ const isAllowed = !Array.isArray(allow) || allow.length === 0 || allow.includes(value);
328
+ // Item is disallowed when and the array is set and includes the value
329
+ const isDisallowed = Array.isArray(disallow) && disallow.includes(value);
330
+ return isAllowed && !isDisallowed;
331
+ }
332
+ /**
333
+ * Retrieve all content types as an Async Generator, allowing processing of entries whilest they are being loaded from the CMS instance.
334
+ *
335
+ * @param client
336
+ * @param args
337
+ * @param pageSize
338
+ */
339
+ async function* getAllContentTypes(client, debug = false, pageSize = 25) {
340
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Pulling Content Types from Optimizely CMS\n`));
341
+ let requestPageSize = pageSize;
342
+ let requestPageIndex = 0;
343
+ let totalItemCount = 0;
344
+ let totalPages = 0;
345
+ do {
346
+ const resultsPage = await client.contentTypesList({ query: { pageIndex: requestPageIndex, pageSize: requestPageSize } }).catch((_) => {
347
+ return {
348
+ items: [],
349
+ totalItemCount: 0,
350
+ pageIndex: requestPageIndex,
351
+ pageSize: requestPageSize
352
+ };
353
+ });
354
+ // Calculate fields for next page
355
+ totalItemCount = resultsPage.totalItemCount ?? 0;
356
+ requestPageSize = resultsPage.pageSize;
357
+ requestPageIndex = resultsPage.pageIndex + 1;
358
+ totalPages = Math.ceil(totalItemCount / requestPageSize);
359
+ // Debug output
360
+ if (debug)
361
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Fetched contentTypes page ${requestPageIndex} of ${totalPages} (${requestPageSize} items per page)\n`));
362
+ // Yield items
363
+ for (const contentType of (resultsPage.items ?? [])) {
364
+ yield contentType;
365
+ }
366
+ } while (requestPageIndex < totalPages);
274
367
  }
275
368
 
276
369
  const stylesBuilder = yargs => {
@@ -282,73 +375,90 @@ const stylesBuilder = yargs => {
282
375
  newArgs.option("templateTypes", { alias: 'tt', description: "Select only these template types", choices: ['node', 'base', 'component'], type: 'array', demandOption: false, default: [] });
283
376
  return newArgs;
284
377
  };
285
- async function getStyles(client, args, pageSize = 100) {
378
+ async function getStyles(client, args, pageSize = 25) {
286
379
  if (client.runtimeCmsVersion == OptiCmsVersion.CMS12)
287
380
  return { all: [], styles: [] };
288
381
  const { _config: cfg, excludeBaseTypes, excludeTypes, excludeNodeTypes, excludeTemplates, baseTypes, types, nodes, templates, templateTypes } = parseArgs(args);
289
382
  process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Pulling Style-Definitions from Optimizely CMS\n`));
290
- if (cfg.debug)
291
- process.stdout.write(chalk.gray(`${figures.arrowRight} Fetching page 1 of ? (${pageSize} items per page)\n`));
292
- let resultsPage = await client.displayTemplates.displayTemplatesList(0, pageSize);
293
- const results = resultsPage.items ?? [];
294
- let pagesRemaining = Math.ceil(resultsPage.totalItemCount / resultsPage.pageSize) - (resultsPage.pageIndex + 1);
295
- while (pagesRemaining > 0 && results.length < resultsPage.totalItemCount) {
296
- if (cfg.debug)
297
- process.stdout.write(chalk.gray(`${figures.arrowRight} Fetching page ${resultsPage.pageIndex + 2} of ${Math.ceil(resultsPage.totalItemCount / resultsPage.pageSize)} (${resultsPage.pageSize} items per page)\n`));
298
- resultsPage = await client.displayTemplates.displayTemplatesList(resultsPage.pageIndex + 1, resultsPage.pageSize);
299
- results.push(...resultsPage.items);
300
- pagesRemaining = Math.ceil(resultsPage.totalItemCount / resultsPage.pageSize) - (resultsPage.pageIndex + 1);
301
- }
302
- if (cfg.debug) {
303
- process.stdout.write(chalk.gray(`${figures.arrowRight} Fetched ${results.length} Style-Definitions from Optimizely CMS\n`));
304
- process.stdout.write(chalk.gray(`${figures.arrowRight} Filtering Style-Definitions based upon arguments\n`));
305
- }
306
- const styles = results.filter(data => {
307
- if (isExcluded(data.key, excludeTemplates, templates)) {
383
+ const allDisplayTemplates = [];
384
+ const filteredDisplayTemplates = [];
385
+ for await (const displayTemplate of getAllStyles(client, cfg.debug, pageSize)) {
386
+ allDisplayTemplates.push(displayTemplate);
387
+ const templateType = displayTemplate.baseType ? 'base' : displayTemplate.nodeType ? 'node' : displayTemplate.contentType ? 'component' : 'unknown';
388
+ if (isExcluded(displayTemplate.key, excludeTemplates, templates)) {
308
389
  if (cfg.debug)
309
- process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Style-Defintion ${data.key} - Style defintion key filtering active\n`));
310
- return false;
390
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Style-Defintion ${displayTemplate.key} - Style defintion key filtering active\n`));
391
+ continue;
311
392
  }
312
- const templateType = data.baseType ? 'base' : data.nodeType ? 'node' : data.contentType ? 'component' : 'unknown';
313
393
  if (isExcluded(templateType, [], templateTypes)) {
314
394
  if (cfg.debug)
315
- process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Style-Defintion ${data.key} - Style type filtering is active\n`));
316
- return false;
395
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Style-Defintion ${displayTemplate.key} - Style type filtering is active\n`));
396
+ continue;
317
397
  }
318
- if (data.baseType && isExcluded(data.baseType, excludeBaseTypes, baseTypes)) {
398
+ if (displayTemplate.baseType && isExcluded(displayTemplate.baseType, excludeBaseTypes, baseTypes)) {
319
399
  if (cfg.debug)
320
- process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Style-Defintion ${data.key} - Style is defined at base type level and base type filtering is active\n`));
321
- return false;
400
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Style-Defintion ${displayTemplate.key} - Style is defined at base type level and base type filtering is active\n`));
401
+ continue;
322
402
  }
323
- if (data.contentType && isExcluded(data.contentType, excludeTypes, types)) {
403
+ if (displayTemplate.contentType && isExcluded(displayTemplate.contentType, excludeTypes, types)) {
324
404
  if (cfg.debug)
325
- process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Style-Defintion ${data.key} - Style is defined at component type level and component type filtering is active\n`));
326
- return false;
405
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Style-Defintion ${displayTemplate.key} - Style is defined at component type level and component type filtering is active\n`));
406
+ continue;
327
407
  }
328
408
  if (templateType != 'component' && types.length > 0) {
329
409
  if (cfg.debug)
330
- process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Style-Defintion ${data.key} - Style is targeting the ${templateType} level and component type selection is active\n`));
331
- return false;
410
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Style-Defintion ${displayTemplate.key} - Style is targeting the ${templateType} level and component type selection is active\n`));
411
+ continue;
332
412
  }
333
- if (data.nodeType && isExcluded(data.nodeType, excludeNodeTypes, nodes)) {
413
+ if (displayTemplate.nodeType && isExcluded(displayTemplate.nodeType, excludeNodeTypes, nodes)) {
334
414
  if (cfg.debug)
335
- process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Style-Defintion ${data.key} - Style is defined at node type level and node type filtering is active\n`));
336
- return false;
415
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Style-Defintion ${displayTemplate.key} - Style is defined at node type level and node type filtering is active\n`));
416
+ continue;
337
417
  }
338
418
  if (templateType != 'node' && nodes.length > 0) {
339
419
  if (cfg.debug)
340
- process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Style-Defintion ${data.key} - Style is targeting the ${templateType} level and node type selection is active\n`));
341
- return false;
420
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Style-Defintion ${displayTemplate.key} - Style is targeting the ${templateType} level and node type selection is active\n`));
421
+ continue;
342
422
  }
343
- return true;
344
- });
423
+ filteredDisplayTemplates.push(displayTemplate);
424
+ }
345
425
  if (cfg.debug)
346
- process.stdout.write(chalk.gray(`${figures.arrowRight} Applied content type filters, reduced from ${results.length} to ${styles.length} items\n`));
426
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Applied style filters, reduced from ${allDisplayTemplates.length} to ${filteredDisplayTemplates.length} items\n`));
347
427
  return {
348
- all: results,
349
- styles
428
+ all: allDisplayTemplates,
429
+ styles: filteredDisplayTemplates
350
430
  };
351
431
  }
432
+ async function* getAllStyles(client, debug = false, pageSize = 5) {
433
+ if (client.runtimeCmsVersion == OptiCmsVersion.CMS12)
434
+ return;
435
+ let requestPageSize = pageSize;
436
+ let requestPageIndex = 0;
437
+ let totalItemCount = 0;
438
+ let totalPages = 0;
439
+ do {
440
+ const resultsPage = await client.displayTemplatesList({ query: { pageIndex: requestPageIndex, pageSize: requestPageSize } }).catch((_) => {
441
+ return {
442
+ items: [],
443
+ totalItemCount: 0,
444
+ pageIndex: requestPageIndex,
445
+ pageSize: requestPageSize
446
+ };
447
+ });
448
+ // Calculate fields for next page
449
+ totalItemCount = resultsPage.totalItemCount ?? 0;
450
+ requestPageSize = resultsPage.pageSize;
451
+ requestPageIndex = resultsPage.pageIndex + 1;
452
+ totalPages = Math.ceil(totalItemCount / requestPageSize);
453
+ // Debug output
454
+ if (debug)
455
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Fetched displayTemplates page ${requestPageIndex} of ${totalPages} (${requestPageSize} items per page)\n`));
456
+ // Yield items
457
+ for (const displayTemplate of (resultsPage.items ?? [])) {
458
+ yield displayTemplate;
459
+ }
460
+ } while (requestPageIndex < totalPages);
461
+ }
352
462
  function isExcluded(value, exclusions, inclusions) {
353
463
  if (value == undefined || value == null)
354
464
  return false;
@@ -364,18 +474,9 @@ async function getStyleFilePath(definition, opts) {
364
474
  return `${opts?.contentBaseType}/${definition.contentType}/${definition.key}.opti-style.json`;
365
475
  if (!opts?.client)
366
476
  throw new Error("Neither the contentBaseType, nor the ApiClient has been provided for a definition for a specific ContentType - unable to generate the path");
367
- const pageSize = 50;
368
- let resultsPage = await opts.client.contentTypes.contentTypesList(undefined, undefined, 0, pageSize);
369
- const contentTypes = resultsPage.items ?? [];
370
- let pagesRemaining = Math.ceil(resultsPage.totalItemCount / resultsPage.pageSize) - (resultsPage.pageIndex + 1);
371
- while (pagesRemaining > 0 && contentTypes.length < resultsPage.totalItemCount) {
372
- resultsPage = await opts.client.contentTypes.contentTypesList(undefined, undefined, resultsPage.pageIndex + 1, resultsPage.pageSize);
373
- contentTypes.push(...resultsPage.items);
374
- pagesRemaining = Math.ceil(resultsPage.totalItemCount / resultsPage.pageSize) - (resultsPage.pageIndex + 1);
375
- }
376
- const fetchedBaseType = contentTypes.filter(x => x.key == definition.contentType).map(x => x.baseType).at(0);
377
- if (fetchedBaseType)
378
- return `${fetchedBaseType}/${definition.contentType}/${definition.key}.opti-style.json`;
477
+ const contentType = await opts.client.contentTypesGet({ path: { key: definition.contentType } }).catch(() => undefined);
478
+ if (contentType)
479
+ return `${contentType.baseType}/${definition.contentType}/${definition.key}.opti-style.json`;
379
480
  }
380
481
  throw new Error(`Unable to resolve the target for the DisplayTemplate: ${definition.key}`);
381
482
  }
@@ -487,20 +588,6 @@ function ucFirst$1(input) {
487
588
  return input;
488
589
  return input[0].toUpperCase() + input.substring(1);
489
590
  }
490
- function toTypeFilesList(client, templates, basePath, createPaths = true) {
491
- return templates.reduce(async (prevList, displayTemplate) => {
492
- const typeFiles = await prevList;
493
- const { targetType, styleFilePath: filePath, helperFilePath: helperPath } = await createTemplateMetadata(client, displayTemplate, basePath, createPaths);
494
- if (!typeFiles[targetType]) {
495
- typeFiles[targetType] = {
496
- filePath: helperPath,
497
- templates: []
498
- };
499
- }
500
- typeFiles[targetType].templates.push({ key: displayTemplate.key, file: filePath, data: displayTemplate });
501
- return typeFiles;
502
- }, Promise.resolve({}));
503
- }
504
591
  async function createTemplateMetadata(client, displayTemplate, basePath, createPaths = true) {
505
592
  let itemPath = undefined;
506
593
  let targetType;
@@ -512,17 +599,21 @@ async function createTemplateMetadata(client, displayTemplate, basePath, createP
512
599
  typesPath = path.join(basePath, 'nodes', displayTemplate.nodeType);
513
600
  targetType = targetPrefix + '/' + displayTemplate.nodeType;
514
601
  break;
515
- case 'base':
516
- itemPath = path.join(basePath, displayTemplate.baseType, 'styles', displayTemplate.key);
517
- typesPath = path.join(basePath, displayTemplate.baseType, 'styles');
518
- targetType = targetPrefix + '/' + displayTemplate.baseType;
602
+ case 'base': {
603
+ const baseTypeSlug = baseTypeToSlug(displayTemplate.baseType);
604
+ itemPath = path.join(basePath, baseTypeSlug, 'styles', displayTemplate.key);
605
+ typesPath = path.join(basePath, baseTypeSlug, 'styles');
606
+ targetType = targetPrefix + '/' + baseTypeSlug;
519
607
  break;
520
- case 'content':
521
- const contentType = await client.contentTypes.contentTypesGet(displayTemplate.contentType ?? '-');
522
- itemPath = path.join(basePath, contentType.baseType, contentType.key);
523
- typesPath = path.join(basePath, contentType.baseType, contentType.key);
608
+ }
609
+ case 'content': {
610
+ const contentType = await client.contentTypesGet({ path: { key: displayTemplate.contentType ?? '-' } });
611
+ const baseTypeSlug = baseTypeToSlug(contentType.baseType);
612
+ itemPath = path.join(basePath, baseTypeSlug, contentType.key);
613
+ typesPath = path.join(basePath, baseTypeSlug, contentType.key);
524
614
  targetType = targetPrefix + '/' + displayTemplate.contentType;
525
615
  break;
616
+ }
526
617
  default:
527
618
  throw new Error("Unsupported display template type");
528
619
  }
@@ -534,6 +625,9 @@ async function createTemplateMetadata(client, displayTemplate, basePath, createP
534
625
  const helperFilePath = path.join(typesPath, 'displayTemplates.ts');
535
626
  return { key: displayTemplate.key, itemPath, typesPath, targetType, styleFilePath, helperFilePath };
536
627
  }
628
+ function baseTypeToSlug(baseType) {
629
+ return baseType.replace(/^\_*/, "").toLowerCase();
630
+ }
537
631
  function getTemplateTarget(displayTemplate) {
538
632
  if (displayTemplate.baseType && displayTemplate.baseType.length > 0)
539
633
  return 'base';
@@ -574,8 +668,8 @@ async function createDisplayTemplateHelper(typeFile, typeFileId, force = false,
574
668
  process.stdout.write(chalk.gray(`${figures.arrowRight} Creating or updating definition file for ${typeFileId} - ${typeFilePath}\n`));
575
669
  // Write Style definition
576
670
  const imports = [
577
- 'import type { LayoutProps } from "@remkoj/optimizely-cms-react"',
578
- 'import type { ReactNode } from "react"'
671
+ 'import type { LayoutProps, LayoutPropsSettingKeys, LayoutPropsSettingValues, CmsComponentProps } from "@remkoj/optimizely-cms-react"',
672
+ 'import type { JSX, ComponentType } from "react"'
579
673
  ];
580
674
  const typeContents = [];
581
675
  const props = [];
@@ -584,11 +678,10 @@ async function createDisplayTemplateHelper(typeFile, typeFileId, force = false,
584
678
  const importPath = path.relative(path.dirname(typeFilePath), displayTemplateFile).replaceAll('\\', '/');
585
679
  imports.push(`import type ${displayTemplate.key}Styles from "./${importPath}"`);
586
680
  typeContents.push(`export type ${displayTemplate.key}Props = LayoutProps<typeof ${displayTemplate.key}Styles>`);
587
- typeContents.push(`export type ${displayTemplate.key}ComponentProps<DT extends Record<string, any> = Record<string, any>> = {
588
- data: DT
589
- layoutProps: ${displayTemplate.key}Props | undefined
590
- } & JSX.IntrinsicElements['div']`);
591
- typeContents.push(`export type ${displayTemplate.key}Component<DT extends Record<string, any> = Record<string, any>> = (props: ${displayTemplate.key}ComponentProps<DT>) => ReactNode`);
681
+ typeContents.push(`export type ${displayTemplate.key}Keys = LayoutPropsSettingKeys<${displayTemplate.key}Props>`);
682
+ typeContents.push(`export type ${displayTemplate.key}Options<K extends ${displayTemplate.key}Keys> = LayoutPropsSettingValues<${displayTemplate.key}Props, K>`);
683
+ typeContents.push(`export type ${displayTemplate.key}ComponentProps<DT extends Record<string, any> = Record<string, any>> = Omit<CmsComponentProps<DT, ${displayTemplate.key}Props>,'children'> & JSX.IntrinsicElements['div']`);
684
+ typeContents.push(`export type ${displayTemplate.key}Component<DT extends Record<string, any> = Record<string, any>> = ComponentType<${displayTemplate.key}ComponentProps<DT>>`);
592
685
  typeContents.push('');
593
686
  props.push(`${displayTemplate.key}Props`);
594
687
  if (!typeId)
@@ -596,14 +689,11 @@ async function createDisplayTemplateHelper(typeFile, typeFileId, force = false,
596
689
  });
597
690
  if (typeId) {
598
691
  typeId = ucFirst$1(typeId);
599
- typeContents.push('');
600
692
  typeContents.push(`export type ${typeId}LayoutProps = ${props.join(' | ')}
601
- export type ${typeId}ComponentProps<DT extends Record<string, any> = Record<string, any>, LP extends ${typeId}LayoutProps = ${typeId}LayoutProps> = {
602
- data: DT
603
- layoutProps: LP | undefined
604
- } & JSX.IntrinsicElements['div']
605
-
606
- export type ${typeId}Component<DT extends Record<string, any> = Record<string, any>, LP extends ${typeId}LayoutProps = ${typeId}LayoutProps> = (props: ${typeId}ComponentProps<DT,LP>) => ReactNode`);
693
+ export type ${typeId}LayoutKeys = LayoutPropsSettingKeys<${typeId}LayoutProps>
694
+ export type ${typeId}LayoutOptions<K extends ${typeId}LayoutKeys> = LayoutPropsSettingValues<${typeId}LayoutProps,K>
695
+ export type ${typeId}ComponentProps<DT extends Record<string, any> = Record<string, any>> = Omit<CmsComponentProps<DT, ${typeId}LayoutProps>,'children'> & JSX.IntrinsicElements['div']
696
+ export type ${typeId}Component<DT extends Record<string, any> = Record<string, any>> = ComponentType<${typeId}ComponentProps<DT>>`);
607
697
  const defaultTemplate = templates.find(t => t.data.isDefault);
608
698
  if (defaultTemplate) {
609
699
  typeContents.push(`
@@ -638,11 +728,11 @@ const StylesCreateCommand = {
638
728
  process.stdout.write(chalk.gray(`${figures.cross} Styles are not supported on CMS12\n`));
639
729
  return;
640
730
  }
641
- const allowedBaseTypes = ['section', 'element'];
731
+ const allowedBaseTypes = ['section', 'component', 'experience'];
642
732
  // Prepare
643
733
  process.stdout.write(chalk.yellowBright(chalk.bold(`Reading current information from Optimizely CMS\n`)));
644
734
  const [{ contentTypes }, { styles }] = await Promise.all([
645
- getContentTypes(client, { ...args, excludeBaseTypes: [], excludeTypes: [], baseTypes: allowedBaseTypes, types: [], all: false }),
735
+ getContentTypes(client, { ...args, excludeBaseTypes: [], excludeTypes: [], baseTypes: allowedBaseTypes, types: [], all: false }, 25, false, (ct) => ct.compositionBehaviors?.includes('elementEnabled') || ct.compositionBehaviors?.includes('sectionEnabled') || ct.baseType == 'experience' || ct.baseType == 'section'),
646
736
  getStyles(client, { ...args, excludeBaseTypes: [], excludeNodeTypes: [], excludeTemplates: [], excludeTypes: [], baseTypes: allowedBaseTypes, types: [], nodes: [], components: '', templates: [], templateTypes: [], all: false })
647
737
  ]);
648
738
  const styleKeys = styles.map(x => x.key);
@@ -650,27 +740,34 @@ const StylesCreateCommand = {
650
740
  process.stdout.write(chalk.yellowBright(chalk.bold(`\nConfigure your style definition\n`)));
651
741
  const key = await input({ message: "Identifier:", validate: (val) => { return val.match(/^[a-zA-Z][A-Za-z0-9\-_]*$/) != null && !styleKeys.includes(val); } });
652
742
  const displayName = await input({ message: "Display name:" });
653
- const type = (await select({ message: "Style target:", choices: [
654
- { value: "baseType", name: "Base Type", description: "Target all Content-Types that inherit from the specified base type" },
655
- { value: "contentType", name: "Content-Type", description: "Target a specific Content-Type that supports styling" },
656
- { value: "nodeType", name: "Experience Node", description: "Target a specific node type from a Section" }
657
- ] }));
743
+ const type = (await select({
744
+ message: "Style target:", choices: [
745
+ { value: "baseType", name: "Base type", description: "Target all Content-Types that inherit from the specified base type" },
746
+ { value: "contentType", name: "Content type", description: "Target a specific Content-Type that supports styling" },
747
+ { value: "nodeType", name: "Node", description: "Target a specific node type from a Section" }
748
+ ]
749
+ }));
658
750
  let typeId = "";
659
751
  switch (type) {
660
752
  case 'contentType':
661
753
  typeId = await select({ message: "Content-Type:", choices: contentTypes.map(x => { return { value: x.key, description: x.description, name: x.displayName }; }) });
662
754
  break;
663
755
  case 'nodeType':
664
- typeId = await select({ message: "Node type:", choices: [
756
+ typeId = await select({
757
+ message: "Node type:", choices: [
665
758
  { value: "row", description: "Row", name: "Row" },
666
759
  { value: "column", description: "Column", name: "Column" }
667
- ] });
760
+ ]
761
+ });
668
762
  break;
669
763
  case 'baseType':
670
- typeId = await select({ message: "Base type:", choices: [
764
+ typeId = await select({
765
+ message: "Base type:", choices: [
766
+ { value: "experience", description: "Target all experience types", name: "Experience" },
671
767
  { value: "section", description: "Target all section types", name: "Section" },
672
- { value: "element", description: "Target all element types", name: "Element" }
673
- ] });
768
+ { value: "component", description: "Target all component types", name: "Component" }
769
+ ]
770
+ });
674
771
  break;
675
772
  }
676
773
  const isDefault = await confirm({ message: "Should this style be marked as default?" });
@@ -690,8 +787,18 @@ const StylesCreateCommand = {
690
787
  process.exit(0);
691
788
  }
692
789
  }
790
+ // @ToDo: Add properties
693
791
  fs.writeFileSync(path.join(basePath, styleFilePath), JSON.stringify(definition, undefined, 4));
694
792
  process.stdout.write(chalk.yellowBright(chalk.bold(`\nWritten style defintion template to: ${path.normalize(path.join(basePath, styleFilePath))}\n`)));
793
+ if (await confirm({ message: "Do you want to publish this style into Optimizely CMS?" })) {
794
+ const response = await client.displayTemplatesCreate({ body: definition }).catch(_ => undefined);
795
+ if (!response) {
796
+ process.stderr.write(chalk.redBright(chalk.bold(`\[ERROR] Unable to create style definition ${definition.displayName} in Optimizely CMS\n`)));
797
+ }
798
+ else {
799
+ process.stdout.write(chalk.yellowBright(chalk.bold(`\nCreated style definition ${definition.displayName} in Optimizely CMS\n`)));
800
+ }
801
+ }
695
802
  process.stdout.write(chalk.yellowBright(`\n1. Add your properties to the 'settings' list in the file`));
696
803
  process.stdout.write(chalk.yellowBright(`\n2. Run `));
697
804
  process.stdout.write(chalk.whiteBright(`yarn opti-cms styles:push -t ${definition.key}`));
@@ -704,91 +811,6 @@ const StylesCreateCommand = {
704
811
  }
705
812
  };
706
813
 
707
- const StylesDeleteCommand = {
708
- command: "styles:delete",
709
- describe: "Remove Visual Builder style definitions from the CMS",
710
- builder: (yargs) => {
711
- const newYargs = stylesBuilder(yargs);
712
- newYargs.option('force', { alias: 'f', description: "Overwrite existing files", boolean: true, type: 'boolean', demandOption: false, default: false });
713
- newYargs.option('withStyleFile', { alias: 'w', description: "Delete the .opti-style.json file as weill", boolean: true, type: 'boolean', demandOption: false, default: true });
714
- newYargs.option("definitions", { alias: 'u', description: "Update typescript definitions", boolean: true, type: 'boolean', demandOption: false, default: true });
715
- return newYargs;
716
- },
717
- handler: async (args) => {
718
- const { components: basePath } = parseArgs(args);
719
- const client = createCmsClient(args);
720
- const { styles, all: allStyles } = await getStyles(client, args, 100);
721
- if (styles.findIndex(x => x.isDefault) >= 0)
722
- process.stdout.write(chalk.redBright(chalk.bold((`\n${figures.warning} You are deleting a default Display Template this may lead to unpredicted behavior.\n\n`))));
723
- if (!args.force) {
724
- process.stdout.write(`This will remove the following display templates\n`);
725
- for (const style of styles) {
726
- process.stdout.write(` ${figures.arrowRight} ${style.displayName || style.key} [Key: ${style.key}; Default: ${style.isDefault ? 'Yes' : 'No'}]\n`);
727
- }
728
- process.stdout.write(`\nRun with -f to actually preform removal\n`);
729
- return;
730
- }
731
- const keysToDelete = styles.map(displayTemplate => displayTemplate.key);
732
- const styleHelpers = await toTypeFilesList(client, allStyles, basePath, false);
733
- for (const displayTemplate of styles) {
734
- const { styleFilePath, targetType, itemPath, typesPath } = await createTemplateMetadata(client, displayTemplate, basePath, false);
735
- // Remove style file
736
- if (args.withStyleFile) {
737
- process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Removing *.opti-style.json file for ${displayTemplate.displayName} [${displayTemplate.key}]\n`));
738
- await fsAsync.rm(styleFilePath, { force: false, recursive: false, maxRetries: 1 }).catch((e) => {
739
- if (e?.code === 'ENOENT') // The file can't be deleted as it does not exist - that's the desired state
740
- return;
741
- throw e;
742
- });
743
- await fsAsync.rmdir(itemPath, { recursive: false, maxRetries: 1 }).catch((e) => {
744
- if (e?.code === 'ENOTEMPTY') {
745
- process.stdout.write(chalk.yellowBright(`${figures.warning} The folder is not empty, please remove ${itemPath} manually if needed\n`));
746
- return;
747
- }
748
- else if (e?.code === 'ENOENT')
749
- return;
750
- else
751
- throw e;
752
- });
753
- }
754
- // Remove/update typescript helper
755
- if (args.definitions && styleHelpers[targetType]) {
756
- process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Removing/updating displayTemplates.ts file for ${displayTemplate.displayName} [${displayTemplate.key}]\n`));
757
- const remainingTemplates = styleHelpers[targetType].templates.filter(x => !keysToDelete.includes(x.key));
758
- if (remainingTemplates.length === 0) {
759
- await fsAsync.rm(styleHelpers[targetType].filePath, { force: false, recursive: false, maxRetries: 1 }).catch((e) => {
760
- if (e?.code === 'ENOENT') // The file can't be deleted as it does not exist - that's the desired state
761
- return;
762
- throw e;
763
- });
764
- await fsAsync.rmdir(typesPath, { recursive: false, maxRetries: 1 }).catch((e) => {
765
- if (e?.code === 'ENOTEMPTY') {
766
- process.stdout.write(chalk.yellowBright(`${figures.warning} The folder is not empty, please remove ${typesPath} manually if needed\n`));
767
- return;
768
- }
769
- else if (e?.code === 'ENOENT')
770
- return;
771
- else
772
- throw e;
773
- });
774
- }
775
- else {
776
- const newEntry = {
777
- filePath: styleHelpers[targetType].filePath,
778
- templates: remainingTemplates
779
- };
780
- if (!await createDisplayTemplateHelper(newEntry, targetType, false, false))
781
- process.stdout.write(chalk.yellowBright(`${figures.warning} The display template was not updated, please update ${newEntry.filePath} manually`));
782
- }
783
- }
784
- // Actually remove from CMS
785
- process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Removing the display template ${displayTemplate.displayName} [${displayTemplate.key}] from Optimizely CMS\n`));
786
- await client.displayTemplates.displayTemplatesDelete(displayTemplate.key);
787
- }
788
- process.stdout.write("\n" + chalk.green(chalk.bold(figures.tick + " Done")) + "\n");
789
- }
790
- };
791
-
792
814
  const TypesPullCommand = {
793
815
  command: "types:pull",
794
816
  describe: "Pull content type definition files into the project",
@@ -802,8 +824,7 @@ const TypesPullCommand = {
802
824
  const client = createCmsClient(args);
803
825
  const { contentTypes } = await getContentTypes(client, args);
804
826
  const updatedTypes = contentTypes.map(contentType => {
805
- const typePath = path.join(basePath, contentType.baseType, contentType.key);
806
- const typeFile = path.join(typePath, `${contentType.key}.opti-type.json`);
827
+ const { typePath, typeFile } = getContentTypePaths(contentType, basePath);
807
828
  if (!fs.existsSync(typePath))
808
829
  fs.mkdirSync(typePath, { recursive: true });
809
830
  if (fs.existsSync(typeFile) && !force) {
@@ -814,16 +835,28 @@ const TypesPullCommand = {
814
835
  const outContentType = { ...contentType };
815
836
  if (outContentType.source || outContentType.source == "")
816
837
  delete outContentType.source;
817
- if (outContentType.features)
818
- delete outContentType.features;
819
- if (outContentType.usage)
820
- delete outContentType.usage;
838
+ //if (outContentType.features) delete outContentType.features
839
+ //if (outContentType.usage) delete outContentType.usage
821
840
  if (outContentType.lastModifiedBy)
822
841
  delete outContentType.lastModifiedBy;
823
842
  if (outContentType.lastModified)
824
843
  delete outContentType.lastModified;
825
844
  if (outContentType.created)
826
845
  delete outContentType.created;
846
+ for (const propName of Object.getOwnPropertyNames(outContentType.properties ?? {})) {
847
+ if (!["content", "contentReference"].includes(outContentType.properties[propName].type)) {
848
+ if (isEmptyArray(outContentType.properties[propName].allowedTypes))
849
+ delete outContentType.properties[propName].allowedTypes;
850
+ if (isEmptyArray(outContentType.properties[propName].restrictedTypes))
851
+ delete outContentType.properties[propName].restrictedTypes;
852
+ }
853
+ if (outContentType.properties[propName].type == 'array' && !["content", "contentReference"].includes(outContentType.properties[propName].items?.type)) {
854
+ if (isEmptyArray(outContentType.properties[propName].items.allowedTypes))
855
+ delete outContentType.properties[propName].items.allowedTypes;
856
+ if (isEmptyArray(outContentType.properties[propName].items.restrictedTypes))
857
+ delete outContentType.properties[propName].items.restrictedTypes;
858
+ }
859
+ }
827
860
  if (debug)
828
861
  process.stdout.write(chalk.gray(`${figures.arrowRight} Writing type definition for ${contentType.displayName} (${contentType.key})\n`));
829
862
  fs.writeFileSync(typeFile, JSON.stringify(outContentType, undefined, 2));
@@ -832,6 +865,197 @@ const TypesPullCommand = {
832
865
  process.stdout.write(chalk.green(chalk.bold(`${figures.tick} Created/updated type definitions for ${updatedTypes.join(', ')}\n`)));
833
866
  }
834
867
  };
868
+ function isEmptyArray(toTest) {
869
+ return toTest === null || !Array.isArray(toTest) || toTest.length === 0;
870
+ }
871
+
872
+ const deepmerge$1 = createDeepMerge();
873
+ async function loadSchema(client, schemaName) {
874
+ const schemas = Array.isArray(schemaName) ? schemaName : [schemaName];
875
+ process.stdout.write(`${figures.arrowRight} Downloading current JSON Schema\n`);
876
+ const spec = await client.getOpenApiSpec();
877
+ const specSchemas = spec.components?.schemas ?? {};
878
+ process.stdout.write(` ${figures.tick} Complete\n`);
879
+ const result = [];
880
+ process.stdout.write(`\n${figures.arrowRight} Constructing schema for ${schemas.join(', ')}\n`);
881
+ for await (const schema of schemas)
882
+ if (specSchemas[schema]) {
883
+ const definitions = {};
884
+ const processedSchema = processSchema(specSchemas[schema], definitions, spec);
885
+ const jsonSchema = {
886
+ //"$schema": "https://json-schema.org/draft-07/schema",
887
+ "$id": new URL(`schema/${schema}`, client.getSchemaItemBase()).href,
888
+ type: "object",
889
+ title: schema,
890
+ ...processedSchema,
891
+ definitions
892
+ };
893
+ result.push({
894
+ title: schema,
895
+ schema: postProcessDefintions(jsonSchema)
896
+ });
897
+ process.stdout.write(` ${figures.tick} Constructed schema of ${schema}\n`);
898
+ }
899
+ return result;
900
+ }
901
+ function getValidator(schemaObject) {
902
+ const ajv = new Ajv({
903
+ discriminator: true
904
+ });
905
+ addFormats(ajv);
906
+ return ajv.compile(schemaObject);
907
+ }
908
+ function postProcessDefintions(jsonSchema) {
909
+ if (!jsonSchema.definitions)
910
+ return jsonSchema;
911
+ for (const definitionName in jsonSchema.definitions) {
912
+ if ((definitionName.endsWith("Property") || definitionName.endsWith("ListItem")) && jsonSchema.definitions[definitionName].properties) {
913
+ const type = jsonSchema.definitions[definitionName].properties?.type;
914
+ if (isRefSchema(type)) {
915
+ const typeSchema = resolveRefSchema(type, jsonSchema);
916
+ if (typeSchema && typeSchema.type === 'string' && Array.isArray(typeSchema.enum)) {
917
+ let typeValue = definitionName.substring(0, definitionName.length - 8);
918
+ typeValue = typeValue[0].toLowerCase() + typeValue.substring(1);
919
+ typeValue = typeValue === 'list' ? 'array' : typeValue;
920
+ typeValue = typeValue === 'jsonString' ? 'json' : typeValue;
921
+ if (typeSchema.enum.includes(typeValue)) {
922
+ const newTypeDef = deepmerge$1({}, typeSchema);
923
+ newTypeDef.enum = newTypeDef.enum.filter((x) => x === typeValue);
924
+ jsonSchema.definitions[definitionName].properties.type = newTypeDef;
925
+ }
926
+ }
927
+ }
928
+ }
929
+ }
930
+ return jsonSchema;
931
+ }
932
+ /**
933
+ * Schema normalization to tranform the schema from a .Net generated OpenAPI Schema
934
+ * to a AJV compatible JSON Schema
935
+ *
936
+ * @see https://ajv.js.org/
937
+ * @param schema
938
+ * @param defs
939
+ * @param spec
940
+ * @returns
941
+ */
942
+ function processSchema(schema, defs, spec, mergeAllOf = true) {
943
+ if (Array.isArray(schema))
944
+ return schema.map(s => processSchema(s, defs, spec, mergeAllOf));
945
+ if (typeof schema !== 'object' || schema === null)
946
+ return schema;
947
+ const props = Object.getOwnPropertyNames(schema);
948
+ if (props.length === 1 && props[0] === '$ref') {
949
+ const ref = schema['$ref'];
950
+ if (isLocalRef(ref)) {
951
+ const refName = getLocalRefName(ref);
952
+ if (!defs[refName]) {
953
+ const refItem = resolveLocalRef(ref, spec);
954
+ defs[refName] = processSchema(refItem, defs, spec, mergeAllOf);
955
+ }
956
+ const newRef = `#/definitions/${refName}`;
957
+ return { "$ref": newRef };
958
+ }
959
+ else {
960
+ return schema;
961
+ }
962
+ }
963
+ else if (mergeAllOf && props.includes('allOf') && Array.isArray(schema['allOf'])) {
964
+ // process all of
965
+ const newObject = props.reduce((generated, propName) => {
966
+ if (propName != 'allOf')
967
+ generated[propName] = schema[propName];
968
+ return generated;
969
+ }, {});
970
+ const allOfSchemas = schema['allOf'].map((subschema) => {
971
+ const resolvedSchema = isRefSchema(subschema) ? resolveRefSchema(subschema, spec) : subschema;
972
+ const resolved = processSchema(resolvedSchema, defs, spec, mergeAllOf);
973
+ return resolved;
974
+ });
975
+ const merged = allOfSchemas.reduce((previous, current) => deepmerge$1(previous, current), newObject);
976
+ if (newObject['description'])
977
+ merged['description'] = newObject['description'];
978
+ if (newObject['title'])
979
+ merged['title'] = newObject['title'];
980
+ return merged;
981
+ }
982
+ else {
983
+ const newSchema = {};
984
+ for (const key of props) {
985
+ if (key === 'pattern' && typeof schema[key] === 'string') {
986
+ newSchema[key] = safeCreateUnicodeRegExp(schema[key]);
987
+ /*} else if (key === 'readOnly' && schema[key] === true) {
988
+ return undefined*/
989
+ }
990
+ else if (['additionalProperties', 'required', 'properties', 'discriminator'].includes(key) && typeof schema[key] !== 'object') ;
991
+ else if (key === 'discriminator' && !Array.isArray(schema['oneOf'])) ;
992
+ else if (key === 'discriminator' && !schema['type']) {
993
+ newSchema.type = 'object';
994
+ }
995
+ else {
996
+ const keyVal = processSchema(schema[key], defs, spec, mergeAllOf);
997
+ if (keyVal !== undefined)
998
+ newSchema[key] = keyVal;
999
+ }
1000
+ }
1001
+ if (newSchema['oneOf'] && Array.isArray(newSchema['oneOf']) && newSchema['nullable']) {
1002
+ delete newSchema['nullable'];
1003
+ }
1004
+ return newSchema;
1005
+ }
1006
+ }
1007
+ function isRefSchema(schema) {
1008
+ if (typeof schema != 'object' || schema == null)
1009
+ return false;
1010
+ return Object.getOwnPropertyNames(schema).length === 1 && typeof schema['$ref'] === 'string';
1011
+ }
1012
+ function resolveRefSchema(schema, spec) {
1013
+ if (!isRefSchema(schema))
1014
+ return undefined;
1015
+ return resolveLocalRef(schema['$ref'], spec);
1016
+ }
1017
+ function isLocalRef(ref) {
1018
+ return typeof ref === 'string' && ref.startsWith('#');
1019
+ }
1020
+ function getLocalRefName(ref) {
1021
+ if (!isLocalRef(ref))
1022
+ return undefined;
1023
+ const refPath = ref.substring(2).split('/');
1024
+ return refPath.at(refPath.length - 1);
1025
+ }
1026
+ function resolveLocalRef(ref, spec) {
1027
+ const refPath = ref.substring(2).split('/');
1028
+ return refPath.reduce((integrator, current) => {
1029
+ return integrator ? integrator[current] : undefined;
1030
+ }, spec);
1031
+ }
1032
+ const replaceEscapedChars = [
1033
+ [new RegExp('\\\\ ', 'g'), ' '],
1034
+ [new RegExp('\\\\!', 'g'), '!'],
1035
+ [new RegExp('\\\\"', 'g'), '"'],
1036
+ [new RegExp('\\\\#', 'g'), '#'],
1037
+ [new RegExp('\\\\%', 'g'), '%'],
1038
+ [new RegExp('\\\\&', 'g'), '&'],
1039
+ [new RegExp("\\\\'", 'g'), "'"],
1040
+ [new RegExp('\\\\,', 'g'), ','],
1041
+ [new RegExp('\\\\-', 'g'), '-'],
1042
+ [new RegExp('\\\\:', 'g'), ':'],
1043
+ [new RegExp('\\\\;', 'g'), ';'],
1044
+ [new RegExp('\\\\<', 'g'), '<'],
1045
+ [new RegExp('\\\\=', 'g'), '='],
1046
+ [new RegExp('\\\\>', 'g'), '>'],
1047
+ [new RegExp('\\\\@', 'g'), '@'],
1048
+ [new RegExp('\\\\_', 'g'), '_'],
1049
+ [new RegExp('\\\\`', 'g'), '`'],
1050
+ [new RegExp('\\\\~', 'g'), '~'],
1051
+ [new RegExp('\\\\[zZ]', 'g'), '$'],
1052
+ ];
1053
+ function safeCreateUnicodeRegExp(pattern) {
1054
+ for (const unEscape of replaceEscapedChars) {
1055
+ pattern = pattern.replace(unEscape[0], unEscape[1]);
1056
+ }
1057
+ return pattern;
1058
+ }
835
1059
 
836
1060
  const TypesPushCommand = {
837
1061
  command: "types:push",
@@ -862,25 +1086,52 @@ const TypesPushCommand = {
862
1086
  (!baseTypes || baseTypes.length == 0 || baseTypes.includes(data.definition.baseType)) &&
863
1087
  (!types || types.length == 0 || types.includes(data.definition.key));
864
1088
  });
1089
+ // Create validator
1090
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Loading OpenAPI Specification for validation\n`));
1091
+ const typeSchema = (await loadSchema(client, 'ContentType')).at(0)?.schema;
1092
+ const typeValidator = await getValidator(typeSchema);
865
1093
  // Output selected types
866
- const results = await Promise.all(typeDefinitions.map(type => {
1094
+ const results = await Promise.all(typeDefinitions.map(async (type) => {
1095
+ if (!type.definition.key) {
1096
+ process.stdout.write(chalk.yellowBright(` ${figures.arrowRight} Content Type has no key in ${type.file}\n`));
1097
+ return;
1098
+ }
867
1099
  process.stdout.write(chalk.yellowBright(` ${figures.arrowRight} Pushing ${type.definition.displayName} from ${type.file}\n`));
1100
+ // Filter unwanted fields from the ContentType definition
868
1101
  const outType = { ...type.definition };
869
- if (outType.source || outType.source == "")
1102
+ if (outType.source)
870
1103
  delete outType.source;
871
- if (outType.created || outType.created == "")
1104
+ if (outType.created)
872
1105
  delete outType.created;
873
- if (outType.lastModified || outType.lastModified == "")
1106
+ if (outType.lastModified)
874
1107
  delete outType.lastModified;
875
1108
  if (outType.lastModifiedBy || outType.lastModifiedBy == "")
876
1109
  delete outType.lastModifiedBy;
877
- if (outType.features)
878
- delete outType.features;
879
- if (outType.usage)
880
- delete outType.usage;
881
- return client.contentTypes.contentTypesPut(outType.key, outType, force)
882
- .then(ct => { return { key: type.definition.key, type: ct, file: type.file }; })
883
- .catch(e => { return { key: type.definition.key, type: type.definition, file: type.file, error: e }; });
1110
+ //if (outType.features) delete outType.features
1111
+ //if (outType.usage) delete outType.usage
1112
+ const validationResult = typeValidator(outType);
1113
+ if (validationResult || force) {
1114
+ if (!validationResult)
1115
+ process.stdout.write(chalk.yellowBright(` ${figures.arrowRight} Ignoring validation errors in ${type.file}\n`));
1116
+ const currentContentType = (await client.contentTypesGet({ path: { key: type.definition.key } }).catch(() => null));
1117
+ function buildError(e) {
1118
+ return { key: type.definition.key, type: type.definition, file: type.file, error: e };
1119
+ }
1120
+ function buildData(ct) {
1121
+ return { key: type.definition.key, type: ct, file: type.file };
1122
+ }
1123
+ return (currentContentType ?
1124
+ client.contentTypesPatch({ path: { key: type.definition.key }, body: outType }) :
1125
+ client.contentTypesCreate({ body: outType })).then(buildData).catch(buildError);
1126
+ }
1127
+ else {
1128
+ return {
1129
+ key: type.definition.key,
1130
+ type: type.definition,
1131
+ file: type.file,
1132
+ error: typeValidator.errors.map(x => x.message).join(', ')
1133
+ };
1134
+ }
884
1135
  }));
885
1136
  const overview = new Table({
886
1137
  head: [
@@ -912,237 +1163,10 @@ const builder = yargs => {
912
1163
  return updatedArgs;
913
1164
  };
914
1165
  function createTypeFolders(contentTypes, basePath, debug = false) {
915
- const folders = contentTypes.map(contentType => {
916
- const baseType = contentType.baseType ?? 'default';
917
- // Create the type folder
918
- const typePath = path.join(basePath, baseType, contentType.key);
919
- if (!fs.existsSync(typePath)) {
920
- fs.mkdirSync(typePath, { recursive: true });
921
- if (debug)
922
- process.stdout.write(chalk.gray(`${figures.arrowRight} Created folder ${typePath} for Content-Type ${contentType.key}\n`));
923
- }
924
- // Check folders
925
- if (!fs.statSync(typePath).isDirectory())
926
- throw new Error(`The folder ${typePath} for Content-Type ${contentType.key} exists, but is not a directory!`);
927
- return {
928
- type: contentType.key,
929
- path: typePath,
930
- };
931
- });
932
- return folders;
1166
+ return contentTypes.map(contentType => getContentTypePaths(contentType, basePath, true, debug));
933
1167
  }
934
1168
  function getTypeFolder(list, type) {
935
- return list.filter(x => x.type == type).at(0)?.path;
936
- }
937
-
938
- /**
939
- * Keep track of all generated properties
940
- */
941
- let generatedProps = [];
942
- const NextJsFragmentsCommand = {
943
- command: "nextjs:fragments",
944
- describe: "Create the GrapQL Fragments for a Next.JS / Optimizely Graph structure",
945
- builder,
946
- handler: async (args, opts) => {
947
- generatedProps = [];
948
- // Prepare
949
- const { loadedContentTypes, createdTypeFolders } = opts || {};
950
- const { components: basePath, _config: { debug }, force } = parseArgs(args);
951
- const client = createCmsClient(args);
952
- const { contentTypes, all: allContentTypes } = loadedContentTypes ?? await getContentTypes(client, args);
953
- // Start process
954
- process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Generating GraphQL fragments for ${contentTypes.map(x => x.key).join(', ')}\n`));
955
- const typeFolders = createdTypeFolders ?? createTypeFolders(contentTypes, basePath, debug);
956
- const updatedTypes = contentTypes.map(contentType => {
957
- const typePath = getTypeFolder(typeFolders, contentType.key);
958
- return createGraphFragments(contentType, typePath, basePath, force, debug, allContentTypes, client.runtimeCmsVersion == OptiCmsVersion.CMS12);
959
- }).filter(x => x).flat();
960
- // Report outcome
961
- if (updatedTypes.length > 0)
962
- process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Created/updated GraphQL fragments for ${updatedTypes.join(', ')}\n`));
963
- else
964
- process.stdout.write(chalk.yellowBright(`${figures.arrowRight} No GraphQL fragments created/updated\n`));
965
- if (!opts)
966
- process.stdout.write(chalk.green(chalk.bold(figures.tick + " Done")) + "\n");
967
- generatedProps = [];
968
- }
969
- };
970
- function createGraphFragments(contentType, typePath, basePath, force, debug, contentTypes, forCms12 = false) {
971
- const returnValue = [];
972
- const baseType = contentType.baseType ?? 'default';
973
- const baseQueryFile = path.join(typePath, `${contentType.key}.${baseType}.graphql`);
974
- if (fs.existsSync(baseQueryFile)) {
975
- if (force) {
976
- if (debug)
977
- process.stdout.write(chalk.gray(`${figures.arrowRight} Overwriting ${contentType.displayName} (${contentType.key}) base fragment\n`));
978
- }
979
- else {
980
- if (debug)
981
- process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${contentType.displayName} (${contentType.key}) base fragment - file already exists\n`));
982
- return undefined;
983
- }
984
- }
985
- else if (debug) {
986
- process.stdout.write(chalk.gray(`${figures.arrowRight} Creating ${contentType.displayName} (${contentType.key}) base fragment\n`));
987
- }
988
- const { fragment, propertyTypes } = createInitialFragment(contentType, false, undefined, forCms12);
989
- fs.writeFileSync(baseQueryFile, fragment);
990
- returnValue.push(contentType.key);
991
- let dependencies = Array.isArray(propertyTypes) ? [...propertyTypes] : [];
992
- while (Array.isArray(dependencies) && dependencies.length > 0) {
993
- let newDependencies = [];
994
- dependencies.forEach(dep => {
995
- const propContentType = contentTypes.filter(x => x.key == dep[0])[0];
996
- if (!propContentType) {
997
- console.warn(`🟠 The content type ${dep[0]} has been referenced, but is not found in the Optimizely CMS instance`);
998
- return;
999
- }
1000
- const fullTypeName = forCms12 ? contentType.key + propContentType.key : propContentType.key;
1001
- const propertyFragmentFile = path.join(basePath, propContentType.baseType, propContentType.key, `${fullTypeName}.property.graphql`);
1002
- const propertyFragmentDir = path.dirname(propertyFragmentFile);
1003
- if (!fs.existsSync(propertyFragmentDir))
1004
- fs.mkdirSync(propertyFragmentDir, { recursive: true });
1005
- if (!fs.existsSync(propertyFragmentFile) || force) {
1006
- if (debug)
1007
- process.stdout.write(chalk.gray(`${figures.arrowRight} Writing ${propContentType.displayName} (${propContentType.key}) property fragment\n`));
1008
- const propContentTypeInfo = createInitialFragment(propContentType, true, contentType, forCms12);
1009
- fs.writeFileSync(propertyFragmentFile, propContentTypeInfo.fragment);
1010
- returnValue.push(propContentType.key);
1011
- if (Array.isArray(propContentTypeInfo.propertyTypes))
1012
- newDependencies.push(...propContentTypeInfo.propertyTypes);
1013
- }
1014
- });
1015
- dependencies = newDependencies;
1016
- }
1017
- return returnValue.length > 0 ? returnValue : undefined;
1018
- }
1019
- function createInitialFragment(contentType, forProperty = false, forBaseType, forCms12 = false) {
1020
- const { fragmentFields, propertyTypes } = renderProperties(contentType, forCms12);
1021
- const fragmentTarget = forProperty ? (forCms12 ? (forBaseType?.key ?? '') + contentType.key : contentType.key + 'Property') : contentType.key;
1022
- const tpl = `fragment ${fragmentTarget}Data on ${fragmentTarget} {
1023
- ${fragmentFields.join("\n ")}
1024
- }`;
1025
- return {
1026
- fragment: tpl,
1027
- propertyTypes: propertyTypes.length == 0 ? null : propertyTypes
1028
- };
1029
- }
1030
- function renderProperties(contentType, forCms12 = false) {
1031
- const propertyTypes = [];
1032
- const fragmentFields = [];
1033
- const typeProps = contentType.properties ?? {};
1034
- Object.getOwnPropertyNames(typeProps).forEach(propKey => {
1035
- // Exclude system properties, which are not present in Optimizely Graph
1036
- if (['experience', 'section'].includes(contentType.baseType) && ['AdditionalData', 'UnstructuredData', 'Layout'].includes(propKey))
1037
- return;
1038
- // Exclude CMS 12 System Properties
1039
- if (forCms12 && ['Categories'].includes(propKey))
1040
- return;
1041
- const propType = typeProps[propKey].type;
1042
- const isConflict = generatedProps.some(x => x.propName == propKey && x.propType != propType);
1043
- const propName = isConflict ? `${contentType.key}${propKey}: ${propKey}` : propKey;
1044
- // Write the property
1045
- switch (propType) {
1046
- case IntegrationApi.PropertyDataType.ARRAY:
1047
- {
1048
- const typeData = typeProps[propKey];
1049
- switch (typeData.items.type) {
1050
- case IntegrationApi.PropertyDataType.INTEGER:
1051
- if (typeData.format == 'categorization')
1052
- fragmentFields.push(`${propName} { Id, Name, Description }`);
1053
- else
1054
- fragmentFields.push(propName);
1055
- break;
1056
- case IntegrationApi.PropertyDataType.STRING:
1057
- fragmentFields.push(propName);
1058
- break;
1059
- case IntegrationApi.PropertyDataType.CONTENT:
1060
- if (contentType.baseType == 'page' || contentType.baseType == 'experience')
1061
- fragmentFields.push(`${propName} { ...${forCms12 ? 'PageIContentListItem' : 'BlockData'} }`);
1062
- else
1063
- fragmentFields.push(`${propName} { ...IContentListItem }`);
1064
- break;
1065
- case IntegrationApi.PropertyDataType.COMPONENT:
1066
- const componentType = typeData.items.contentType;
1067
- switch (componentType) {
1068
- case 'link':
1069
- fragmentFields.push(`${propName} { ...LinkItemData }`);
1070
- break;
1071
- default:
1072
- {
1073
- const componentFragmentName = forCms12 ? contentType.key + componentType : componentType + 'Property';
1074
- fragmentFields.push(`${propName} { ...${componentFragmentName}Data }`);
1075
- propertyTypes.push([componentType, true]);
1076
- break;
1077
- }
1078
- }
1079
- break;
1080
- case IntegrationApi.PropertyDataType.CONTENT_REFERENCE:
1081
- fragmentFields.push(`${propName} { ...ReferenceData }`);
1082
- break;
1083
- default:
1084
- fragmentFields.push(`${propName} { __typename }`);
1085
- break;
1086
- }
1087
- break;
1088
- }
1089
- case IntegrationApi.PropertyDataType.STRING: {
1090
- const propDetails = typeProps[propKey];
1091
- switch (propDetails.format ?? "") {
1092
- case 'html':
1093
- fragmentFields.push(forCms12 ? `${propName} { Structure, Html }` : `${propName} { json, html }`);
1094
- break;
1095
- case 'shortString':
1096
- case 'selectOne':
1097
- case '':
1098
- fragmentFields.push(propName);
1099
- break;
1100
- default:
1101
- if (!isConflict)
1102
- console.warn(chalk.redBright(`❗ Unsupported string format "${propDetails.format}" for ${contentType.key}.${propKey}; add it manually to the fragment if you need to access this field`));
1103
- break;
1104
- }
1105
- break;
1106
- }
1107
- case IntegrationApi.PropertyDataType.URL:
1108
- fragmentFields.push(forCms12 ? propName : `${propName} { ...LinkData }`);
1109
- break;
1110
- case IntegrationApi.PropertyDataType.CONTENT_REFERENCE:
1111
- fragmentFields.push(`${propName} { ...ReferenceData }`);
1112
- break;
1113
- case IntegrationApi.PropertyDataType.COMPONENT: {
1114
- const componentType = typeProps[propKey].contentType;
1115
- if (forCms12 && componentType == "link") {
1116
- fragmentFields.push(`${propName} { ...LinkItemData }`);
1117
- }
1118
- else {
1119
- const componentFragmentName = forCms12 ? contentType.key + componentType : componentType + 'Property';
1120
- fragmentFields.push(`${propName} { ...${componentFragmentName}Data }`);
1121
- propertyTypes.push([componentType, true]);
1122
- }
1123
- break;
1124
- }
1125
- case IntegrationApi.PropertyDataType.BINARY:
1126
- fragmentFields.push(`${propName} { ...BinaryData }`);
1127
- break;
1128
- default:
1129
- fragmentFields.push(propName);
1130
- break;
1131
- }
1132
- generatedProps.push({
1133
- propType: typeProps[propKey].type,
1134
- propName: propKey
1135
- });
1136
- });
1137
- if (contentType.baseType == "experience")
1138
- fragmentFields.push('...ExperienceData');
1139
- if (fragmentFields.length == 0) {
1140
- if (forCms12)
1141
- fragmentFields.push('empty: _metadata: ContentLink { key: GuidValue }');
1142
- else
1143
- fragmentFields.push('empty: _metadata { key }');
1144
- }
1145
- return { fragmentFields, propertyTypes };
1169
+ return list.filter(x => x.type == type).at(0);
1146
1170
  }
1147
1171
 
1148
1172
  function ucFirst(current) {
@@ -1178,8 +1202,8 @@ const NextJsComponentsCommand = {
1178
1202
  process.stdout.write(chalk.green(chalk.bold(figures.tick + " Done")) + "\n");
1179
1203
  }
1180
1204
  };
1181
- function createComponent(contentType, typePath, force, debug = false) {
1182
- const componentFile = path.join(typePath, 'index.tsx');
1205
+ function createComponent(contentType, typePathInfo, force, debug = false) {
1206
+ const { componentFile, path: typePath } = typePathInfo;
1183
1207
  if (fs.existsSync(componentFile)) {
1184
1208
  if (!force) {
1185
1209
  if (debug)
@@ -1194,8 +1218,9 @@ function createComponent(contentType, typePath, force, debug = false) {
1194
1218
  // Get type information & short-hands
1195
1219
  const displayTemplate = getDisplayTemplateInfo$1(contentType, typePath);
1196
1220
  const baseDisplayTemplate = getBaseTypeTemplateInfo(contentType, typePath);
1197
- const varName = `${contentType.key}${ucFirst(contentType.baseType ?? 'part')}`;
1198
- const tplFn = Templates[contentType.baseType] ?? Templates['default'];
1221
+ const normalizedBaseType = normalizeBaseType(contentType.baseType ?? 'part');
1222
+ const varName = `${contentType.key}${ucFirst(normalizedBaseType)}`;
1223
+ const tplFn = Templates[normalizedBaseType] ?? Templates['default'];
1199
1224
  if (!tplFn) {
1200
1225
  if (debug)
1201
1226
  process.stdout.write(chalk.redBright(`${figures.cross} Skipping ${contentType.displayName} (${contentType.key}) component - no template for ${contentType.baseType} found\n`));
@@ -1219,6 +1244,11 @@ function getBaseTypeTemplateInfo(contentType, typePath) {
1219
1244
  }
1220
1245
  return undefined;
1221
1246
  }
1247
+ function normalizeBaseType(baseType) {
1248
+ if (baseType.startsWith('_'))
1249
+ return baseType.substring(1);
1250
+ return baseType;
1251
+ }
1222
1252
  function getDisplayTemplateInfo$1(contentType, typePath) {
1223
1253
  const displayTemplatesFile = path.join(typePath, 'displayTemplates.ts');
1224
1254
  if (fs.existsSync(displayTemplatesFile)) {
@@ -1239,6 +1269,7 @@ import { ${baseDisplayTemplate} } from "../styles/displayTemplates";` : ''}
1239
1269
 
1240
1270
  /**
1241
1271
  * ${contentType.displayName}
1272
+ * ---
1242
1273
  * ${contentType.description}
1243
1274
  */
1244
1275
  export const ${varName} : CmsComponent<${contentType.key}DataFragment${(displayTemplate || baseDisplayTemplate) ? ', ' + [displayTemplate, baseDisplayTemplate].filter(x => x).join(" | ") : ''}> = ({ data${(displayTemplate || baseDisplayTemplate) ? ', layoutProps' : ''}, editProps }) => {
@@ -1253,18 +1284,51 @@ export const ${varName} : CmsComponent<${contentType.key}DataFragment${(displayT
1253
1284
  ${varName}.displayName = "${contentType.displayName} (${ucFirst(contentType.baseType)}/${contentType.key})"
1254
1285
  ${varName}.getDataFragment = () => ['${contentType.key}Data', ${contentType.key}DataFragmentDoc]
1255
1286
 
1287
+ export default ${varName}`,
1288
+ // Default Template for all section types
1289
+ section: (contentType, varName, displayTemplate, baseDisplayTemplate) => `import { CmsEditable, type CmsComponent } from "@remkoj/optimizely-cms-react/rsc";
1290
+ import { ${contentType.key}DataFragmentDoc, type ${contentType.key}DataFragment } from "@/gql/graphql";${displayTemplate ? `
1291
+ import { ${displayTemplate} } from "./displayTemplates";` : ''}${baseDisplayTemplate ? `
1292
+ import { ${baseDisplayTemplate} } from "../styles/displayTemplates";` : ''}
1293
+
1294
+ /**
1295
+ * ${contentType.displayName}
1296
+ * ---
1297
+ * ${contentType.description}
1298
+ */
1299
+ export const ${varName} : CmsComponent<${contentType.key}DataFragment${(displayTemplate || baseDisplayTemplate) ? ', ' + [displayTemplate, baseDisplayTemplate].filter(x => x).join(" | ") : ''}> = ({ data${(displayTemplate || baseDisplayTemplate) ? ', layoutProps' : ''}, editProps, children }) => {
1300
+ const componentName = '${contentType.displayName}'
1301
+ const componentInfo = '${contentType.description?.replaceAll("'", "\\'") ?? ''}'
1302
+ return <CmsEditable className="w-full border-y border-y-solid border-y-slate-900 py-2 mb-4" {...editProps}>
1303
+ <div className="font-bold italic">{ componentName }</div>
1304
+ <div>{ componentInfo }</div>
1305
+ { Object.getOwnPropertyNames(data).length > 0 && <pre className="w-full overflow-x-hidden font-mono text-sm bg-slate-200 p-2 rounded-sm border border-solid border-slate-900 text-slate-900">{ JSON.stringify(data, undefined, 4) }</pre> }
1306
+ ${(displayTemplate || baseDisplayTemplate) ? '<pre className="w-full overflow-x-hidden font-mono text-sm bg-slate-200 p-2 rounded-sm border border-solid border-slate-900 text-slate-900">{ JSON.stringify(layoutProps, undefined, 4) }</pre>' : '{/* This component doesn\'t have layout options */}'}
1307
+ <div>{ children }</div>
1308
+ </CmsEditable>
1309
+ }
1310
+ ${varName}.displayName = "${contentType.displayName} (${ucFirst(contentType.baseType)}/${contentType.key})"
1311
+ ${varName}.getDataFragment = () => ['${contentType.key}Data', ${contentType.key}DataFragmentDoc]
1312
+
1256
1313
  export default ${varName}`,
1257
1314
  // Template for all page component types
1258
1315
  page: (contentType, varName, displayTemplate) => `import { type OptimizelyNextPage as CmsComponent } from "@remkoj/optimizely-cms-nextjs";
1259
- import { ${contentType.key}DataFragmentDoc, type ${contentType.key}DataFragment } from "@/gql/graphql";${displayTemplate ? `
1316
+ import { get${contentType.key}DataDocument, type get${contentType.key}DataQuery } from '@/gql/graphql'${displayTemplate ? `
1260
1317
  import { ${displayTemplate} } from "./displayTemplates";` : ''}
1261
1318
  import { getSdk } from "@/gql"
1262
1319
 
1263
1320
  /**
1264
1321
  * ${contentType.displayName}
1322
+ * ---
1265
1323
  * ${contentType.description}
1324
+ *
1325
+ * This component uses the content query that is auto-generated with the Optimizely CMS Preset for GraphQL Codegen, if you need
1326
+ * to override this query create a GraphQL file (for example: \`get${contentType.key}Data.query.graphql\`) in the same folder as
1327
+ * this file. This file must include a GraphQL query with the name \`get${contentType.key}Data\`.
1328
+ *
1329
+ * [Documentation: Customizing queries](https://github.com/remkoj/optimizely-dxp-clients/blob/main/packages/optimizely-graph-functions/docs/customizing_queries.md)
1266
1330
  */
1267
- export const ${varName} : CmsComponent<${contentType.key}DataFragment${displayTemplate ? ', ' + displayTemplate : ''}> = ({ data${displayTemplate ? ', layoutProps' : ''} }) => {
1331
+ export const ${varName} : CmsComponent<get${contentType.key}DataQuery${displayTemplate ? ', ' + displayTemplate : ''}> = ({ data${displayTemplate ? ', layoutProps' : ''} }) => {
1268
1332
  const componentName = '${contentType.displayName}'
1269
1333
  const componentInfo = '${contentType.description?.replaceAll("'", "\\'") ?? ''}'
1270
1334
  return <div className="mx-auto px-2 container">
@@ -1274,7 +1338,7 @@ export const ${varName} : CmsComponent<${contentType.key}DataFragment${displayTe
1274
1338
  </div>
1275
1339
  }
1276
1340
  ${varName}.displayName = "${contentType.displayName} (${ucFirst(contentType.baseType)}/${contentType.key})"
1277
- ${varName}.getDataFragment = () => ['${contentType.key}Data', ${contentType.key}DataFragmentDoc]
1341
+ ${varName}.getDataQuery = () => get${contentType.key}DataDocument
1278
1342
  ${varName}.getMetaData = async (contentLink, locale, client) => {
1279
1343
  const sdk = getSdk(client);
1280
1344
  // Add your metadata logic here
@@ -1284,24 +1348,36 @@ ${varName}.getMetaData = async (contentLink, locale, client) => {
1284
1348
  export default ${varName}`,
1285
1349
  // Template for all experience component types
1286
1350
  experience: (contentType, varName, displayTemplate) => `import { type OptimizelyNextPage as CmsComponent } from "@remkoj/optimizely-cms-nextjs";
1287
- import { getFragmentData } from "@/gql/fragment-masking";
1288
- import { ExperienceDataFragmentDoc, CompositionDataFragmentDoc, ${contentType.key}DataFragmentDoc, type ${contentType.key}DataFragment } from "@/gql/graphql";
1351
+ import { ExperienceDataFragmentDoc, get${contentType.key}DataDocument, type get${contentType.key}DataQuery } from '@/gql/graphql'
1352
+ import { getFragmentData } from '@/gql/fragment-masking'
1289
1353
  import { OptimizelyComposition, isNode, CmsEditable } from "@remkoj/optimizely-cms-react/rsc";${displayTemplate ? `
1290
1354
  import { ${displayTemplate} } from "./displayTemplates";` : ''}
1291
- import { getSdk } from "@/gql"
1355
+ import { getSdk } from "@/gql/client"
1292
1356
 
1293
1357
  /**
1294
1358
  * ${contentType.displayName}
1359
+ * ---
1295
1360
  * ${contentType.description}
1361
+ *
1362
+ * This component uses the content query that is auto-generated with the Optimizely CMS Preset for GraphQL Codegen, if you need
1363
+ * to override this query create the file \`${contentType.key}.query.graphql\` in the same folder as this file. This file then
1364
+ * must include a GraphQL query with the name \`get${contentType.key} Data\`.
1365
+ *
1366
+ * [Documentation: Customizing queries](https://github.com/remkoj/optimizely-dxp-clients/blob/main/packages/optimizely-graph-functions/docs/customizing_queries.md)
1296
1367
  */
1297
- export const ${varName} : CmsComponent<${contentType.key}DataFragment${displayTemplate ? ', ' + displayTemplate : ''}> = ({ data${displayTemplate ? ', layoutProps' : ''}, ctx }) => {
1298
- const composition = getFragmentData(CompositionDataFragmentDoc, getFragmentData(ExperienceDataFragmentDoc, data)?.composition)
1299
- return <CmsEditable as="div" className="mx-auto px-2 container" cmsFieldName="unstructuredData" ctx={ctx}>
1300
- { composition && isNode(composition) && <OptimizelyComposition node={composition} /> }
1301
- </CmsEditable>
1368
+ export const ${varName} : CmsComponent<get${contentType.key}DataQuery${displayTemplate ? ', ' + displayTemplate : ''}> = ({ data${displayTemplate ? ', layoutProps' : ''}, ctx }) => {
1369
+ if (ctx) ctx.editableContentIsExperience = true
1370
+ const composition = getFragmentData(ExperienceDataFragmentDoc, data).composition
1371
+ const componentName = '${contentType.displayName}'
1372
+ const componentInfo = '${contentType.description?.replaceAll("'", "\\'") ?? ''}'
1373
+ return <CmsEditable as="div" className="mx-auto px-2 container" cmsFieldName="unstructuredData" ctx={ctx}>
1374
+ <div className="font-bold italic">{ componentName }</div>
1375
+ <div>{ componentInfo }</div>
1376
+ { composition && isNode(composition) && <OptimizelyComposition node={composition} ctx={ctx} /> }
1377
+ </CmsEditable>
1302
1378
  }
1303
1379
  ${varName}.displayName = "${contentType.displayName} (${ucFirst(contentType.baseType)}/${contentType.key})"
1304
- ${varName}.getDataFragment = () => ['${contentType.key}Data', ${contentType.key}DataFragmentDoc]
1380
+ ${varName}.getDataQuery = () => get${contentType.key}DataDocument
1305
1381
  ${varName}.getMetaData = async (contentLink, locale, client) => {
1306
1382
  const sdk = getSdk(client);
1307
1383
  // Add your metadata logic here
@@ -1319,7 +1395,7 @@ const NextJsVisualBuilderCommand = {
1319
1395
  // Prepare
1320
1396
  const { components: basePath, _config: { debug }, force } = parseArgs(args);
1321
1397
  // Create the fall-back node and style defintions
1322
- await StylesPullCommand.handler({ ...args, excludeNodeTypes: [], excludeTemplates: [], nodes: [], templates: [], templateTypes: ['node'], definitions: true });
1398
+ await StylesPullCommand.handler({ ...args, excludeNodeTypes: [], excludeTemplates: [], nodes: [], templates: [], templateTypes: ['node', 'base'], definitions: true });
1323
1399
  createGenericNode(basePath, force, debug);
1324
1400
  // Get all styles
1325
1401
  const { styles } = await getStyles(createCmsClient(args), { ...args, excludeNodeTypes: [], excludeTemplates: [], nodes: [], templates: [], templateTypes: ['node', 'base'] });
@@ -1330,7 +1406,8 @@ const NextJsVisualBuilderCommand = {
1330
1406
  });
1331
1407
  // Process base styles
1332
1408
  styles.filter(x => typeof (x.baseType) == 'string' && x.baseType.length > 0).map(styleDefinition => {
1333
- const templatePath = path.join(basePath, styleDefinition.baseType, 'styles', styleDefinition.key);
1409
+ const baseType = (styleDefinition.baseType ?? '').startsWith('_') ? (styleDefinition.baseType ?? '').substring(1) : styleDefinition.baseType;
1410
+ const templatePath = path.join(basePath, baseType, 'styles', styleDefinition.key);
1334
1411
  createSpecificNode(styleDefinition, templatePath, force, debug);
1335
1412
  });
1336
1413
  }
@@ -1364,11 +1441,16 @@ function createSpecificNode(template, templatePath, force = false, debug = false
1364
1441
  componentProps.push('{ ...editProps }');
1365
1442
  componentArgs.push('editProps');
1366
1443
  }
1444
+ let style = '';
1445
+ if (baseType == "row")
1446
+ style = ` style={{ display: "flex", flexDirection: "row", justifyContent: "space-between", alignItems: "stretch" }}`;
1447
+ if (baseType == "column")
1448
+ style = ` style={{ display: "flex", flexDirection: "column", flexGrow: 1 }}`;
1367
1449
  const component = `${imports.join("\n")}
1368
1450
 
1369
1451
  export const ${template.key} : CmsLayoutComponent<${displayTemplateName ?? '{}'}> = ({ ${componentArgs.join(', ')} }) => {
1370
1452
  const layout = extractSettings(layoutProps);
1371
- return (<${componentType} ${componentProps.join(' ')}>{ children }</${componentType}>);
1453
+ return (<${componentType} ${componentProps.join(' ')}${style}>{ children }</${componentType}>);
1372
1454
  }
1373
1455
 
1374
1456
  export default ${template.key};`;
@@ -1388,13 +1470,19 @@ function createGenericNode(basePath, force, debug) {
1388
1470
  else if (debug)
1389
1471
  process.stdout.write(chalk.gray(`${figures.arrowRight} Creating generic node component\n`));
1390
1472
  const nodeContent = `import { CmsEditable, type CmsLayoutComponent } from '@remkoj/optimizely-cms-react/rsc'
1473
+ import { type CSSProperties } from 'react';
1391
1474
 
1392
1475
  export const VisualBuilderNode : CmsLayoutComponent = ({ editProps, layoutProps, children }) =>
1393
1476
  {
1394
- let className = \`vb:\${layoutProps?.layoutType}\`
1395
- if (layoutProps && layoutProps.layoutType == "section")
1396
- return <CmsEditable as="div" className={ className } {...editProps}>{ children }</CmsEditable>
1397
- return <div className={ className }>{ children }</div>
1477
+ let className = \`vb:\${layoutProps?.layoutType}\`;
1478
+ let style : CSSProperties | undefined = undefined;
1479
+ if (layoutProps?.layoutType == "row")
1480
+ style = {display: "flex", flexDirection: "row"}
1481
+ if (layoutProps?.layoutType == "column")
1482
+ style = {display: "flex", flexDirection: "column"}
1483
+ if (layoutProps && layoutProps.layoutType == "section")
1484
+ return <CmsEditable as="div" className={ className } {...editProps}>{ children }</CmsEditable>
1485
+ return <div className={ className } style={ style }>{ children }</div>
1398
1486
  }
1399
1487
 
1400
1488
  export default VisualBuilderNode`;
@@ -1642,7 +1730,7 @@ const NextJsCreateCommand = {
1642
1730
  await Promise.all([
1643
1731
  TypesPullCommand.handler(args),
1644
1732
  StylesPullCommand.handler({ ...args, excludeNodeTypes: [], excludeTemplates: [], nodes: [], templates: [], templateTypes: [], definitions: true }),
1645
- NextJsFragmentsCommand.handler(args, { loadedContentTypes: getContentTypesResult, createdTypeFolders: folders })
1733
+ //createFragments.handler(args, { loadedContentTypes: getContentTypesResult, createdTypeFolders: folders })
1646
1734
  ]);
1647
1735
  process.stdout.write(chalk.yellowBright(chalk.bold(figures.tick) + " Prepared types, styles and fragments") + "\n");
1648
1736
  // Then create the components
@@ -1657,6 +1745,125 @@ const NextJsCreateCommand = {
1657
1745
  }
1658
1746
  };
1659
1747
 
1748
+ const NextJsFragmentsCommand = {
1749
+ command: "nextjs:fragments",
1750
+ describe: "Create the GrapQL Fragments for a Next.JS / Optimizely Graph structure",
1751
+ builder,
1752
+ handler: async (args, opts) => {
1753
+ // Prepare
1754
+ const { loadedContentTypes, createdTypeFolders } = opts || {};
1755
+ const { components: basePath, _config: { debug }, force } = parseArgs(args);
1756
+ const client = createCmsClient(args);
1757
+ const { contentTypes, all: allContentTypes } = loadedContentTypes ?? await getContentTypes(client, args);
1758
+ // Start process
1759
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Generating GraphQL fragments\n`));
1760
+ const typeFolders = createdTypeFolders ?? createTypeFolders(contentTypes, basePath, debug);
1761
+ const tracker = new Map();
1762
+ const dependencies = [];
1763
+ const updatedTypes = contentTypes.map(contentType => {
1764
+ const { written, propertyTypes } = createComponentFragments(contentType, getTypeFolder(typeFolders, contentType.key), force, debug, tracker);
1765
+ dependencies.push(...propertyTypes);
1766
+ return written ? contentType.key : undefined;
1767
+ }).filter(x => x);
1768
+ // Report outcome
1769
+ if (updatedTypes.length > 0)
1770
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Created/updated GraphQL fragments for ${updatedTypes.join(', ')}\n`));
1771
+ else
1772
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} No GraphQL fragments created/updated\n`));
1773
+ // Start property generation process
1774
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Generating GraphQL property fragments\n`));
1775
+ const generatedProps = createPropertyFragments(dependencies, allContentTypes, (contentType) => {
1776
+ if (!contentType.key)
1777
+ return undefined;
1778
+ let tf = getTypeFolder(typeFolders, contentType.key);
1779
+ if (!tf) {
1780
+ tf = getContentTypePaths(contentType, basePath, true, debug);
1781
+ typeFolders.push(tf);
1782
+ }
1783
+ return tf;
1784
+ }, force, debug);
1785
+ // Report outcome
1786
+ if (generatedProps.length > 0)
1787
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Created/updated GraphQL property fragments for ${generatedProps.join(', ')}\n`));
1788
+ else
1789
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} No GraphQL property fragments created/updated\n`));
1790
+ if (!opts)
1791
+ process.stdout.write(chalk.green(chalk.bold(figures.tick + " Done")) + "\n");
1792
+ }
1793
+ };
1794
+ function createComponentFragments(contentType, typePath, force, debug, propertyTracker = new Map()) {
1795
+ const baseQueryFile = typePath.fragmentFile;
1796
+ let writeFragment = true;
1797
+ if (fs.existsSync(baseQueryFile)) {
1798
+ if (force) {
1799
+ if (debug)
1800
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Overwriting ${contentType.displayName} (${contentType.key}) base fragment\n`));
1801
+ }
1802
+ else {
1803
+ if (debug)
1804
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${contentType.displayName} (${contentType.key}) base fragment - file already exists\n`));
1805
+ writeFragment = false;
1806
+ }
1807
+ }
1808
+ else if (debug) {
1809
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Creating ${contentType.displayName} (${contentType.key}) base fragment\n`));
1810
+ }
1811
+ let written = false;
1812
+ if (writeFragment) {
1813
+ const fragment = GraphQLGen.buildFragment(contentType, undefined, false, propertyTracker);
1814
+ fs.writeFileSync(baseQueryFile, fragment);
1815
+ written = true;
1816
+ }
1817
+ return { written, propertyTypes: GraphQLGen.getReferencedPropertyComponents(contentType) };
1818
+ }
1819
+ function createPropertyFragments(propertyTypesList, allContentTypes, selectTypeFolder, force = false, debug = false) {
1820
+ const returnValue = [];
1821
+ for (const propertyContentTypeKey of propertyTypesList.filter((v, i, a) => a.indexOf(v, i + 1) <= i)) {
1822
+ // Load the ContentType definition
1823
+ const contentType = allContentTypes.filter(x => x.key == propertyContentTypeKey).at(0);
1824
+ if (!contentType) {
1825
+ console.warn(`🟠 The content type ${propertyContentTypeKey} has been referenced, but is not found in the Optimizely CMS instance`);
1826
+ continue;
1827
+ }
1828
+ // Load the paths for this ContentType
1829
+ const depFolder = selectTypeFolder(contentType);
1830
+ if (!depFolder) {
1831
+ console.warn(`🟠 The content type ${propertyContentTypeKey} cannot be stored in the project`);
1832
+ continue;
1833
+ }
1834
+ const propertyFragmentFile = depFolder.propertyFragmentFile;
1835
+ let mustWrite = true;
1836
+ if (fs.existsSync(propertyFragmentFile)) {
1837
+ if (force) {
1838
+ if (debug)
1839
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Overwriting ${contentType.displayName} (${contentType.key}) property fragment\n`));
1840
+ }
1841
+ else {
1842
+ if (debug)
1843
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${contentType.displayName} (${contentType.key}) property fragment - file already exists\n`));
1844
+ mustWrite = false;
1845
+ }
1846
+ }
1847
+ else if (debug) {
1848
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Creating ${contentType.displayName} (${contentType.key}) property fragment\n`));
1849
+ }
1850
+ if (mustWrite) {
1851
+ const fragment = GraphQLGen.buildFragment(contentType, undefined, true);
1852
+ fs.writeFileSync(propertyFragmentFile, fragment);
1853
+ returnValue.push(contentType.key);
1854
+ }
1855
+ // Recurse down for properties that we're not yet rendering
1856
+ const referencedPropertyTypes = GraphQLGen.getReferencedPropertyComponents(contentType).filter(x => !propertyTypesList.includes(x));
1857
+ if (referencedPropertyTypes.length > 0) {
1858
+ if (debug)
1859
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Component property ${propertyContentTypeKey} uses components a property, recursing down\n`));
1860
+ const additionalProperties = createPropertyFragments(referencedPropertyTypes, allContentTypes, selectTypeFolder, force, debug);
1861
+ returnValue.push(...additionalProperties);
1862
+ }
1863
+ }
1864
+ return returnValue;
1865
+ }
1866
+
1660
1867
  const NextJsQueriesCommand = {
1661
1868
  command: "nextjs:queries",
1662
1869
  describe: "Create the GrapQL Queries to use two queries to load content",
@@ -1672,24 +1879,43 @@ const NextJsQueriesCommand = {
1672
1879
  const client = createCmsClient(args);
1673
1880
  const { contentTypes, all: allContentTypes } = loadedContentTypes ?? await getContentTypes(client, args);
1674
1881
  // Start process
1675
- process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Generating GraphQL fragments for ${contentTypes.map(x => x.key).join(', ')}\n`));
1882
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Generating GraphQL queries\n`));
1883
+ const dependencies = [];
1676
1884
  const typeFolders = createdTypeFolders ?? createTypeFolders(contentTypes, basePath, debug);
1677
1885
  const updatedTypes = contentTypes.map(contentType => {
1678
1886
  const typePath = getTypeFolder(typeFolders, contentType.key);
1679
- return createGraphQueries(contentType, typePath, basePath, force, debug, allContentTypes, client.runtimeCmsVersion == OptiCmsVersion.CMS12);
1887
+ const { written, propertyTypes } = createGraphQueries(contentType, typePath, force, debug);
1888
+ dependencies.push(...propertyTypes);
1889
+ return written ? contentType.key : undefined;
1680
1890
  }).filter(x => x).flat();
1681
1891
  // Report outcome
1682
1892
  if (updatedTypes.length > 0)
1683
- process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Created/updated GraphQL fragments for ${updatedTypes.join(', ')}\n`));
1893
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Created/updated GraphQL queries for ${updatedTypes.join(', ')}\n`));
1684
1894
  else
1685
- process.stdout.write(chalk.yellowBright(`${figures.arrowRight} No GraphQL fragments created/updated\n`));
1895
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} No GraphQL queries created/updated\n`));
1896
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Generating GraphQL property fragments\n`));
1897
+ const generatedProps = createPropertyFragments(dependencies, allContentTypes, (contentType) => {
1898
+ if (!contentType.key)
1899
+ return undefined;
1900
+ let tf = getTypeFolder(typeFolders, contentType.key);
1901
+ if (!tf) {
1902
+ tf = getContentTypePaths(contentType, basePath, true, debug);
1903
+ typeFolders.push(tf);
1904
+ }
1905
+ return tf;
1906
+ }, force, debug);
1907
+ // Report outcome
1908
+ if (generatedProps.length > 0)
1909
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Created/updated GraphQL property fragments for ${generatedProps.join(', ')}\n`));
1910
+ else
1911
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} No GraphQL property fragments created/updated\n`));
1686
1912
  if (!opts)
1687
1913
  process.stdout.write(chalk.green(chalk.bold(figures.tick + " Done")) + "\n");
1688
1914
  }
1689
1915
  };
1690
- function createGraphQueries(contentType, typePath, basePath, force, debug, contentTypes, forCms12 = false) {
1691
- const returnValue = [];
1692
- const baseQueryFile = path.join(typePath, `${contentType.key}.query.graphql`);
1916
+ function createGraphQueries(contentType, typePath, force, debug) {
1917
+ const baseQueryFile = typePath.queryFile;
1918
+ let mustWrite = true;
1693
1919
  if (fs.existsSync(baseQueryFile)) {
1694
1920
  if (force) {
1695
1921
  if (debug)
@@ -1698,72 +1924,305 @@ function createGraphQueries(contentType, typePath, basePath, force, debug, conte
1698
1924
  else {
1699
1925
  if (debug)
1700
1926
  process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${contentType.displayName} (${contentType.key}) base fragment - file already exists\n`));
1701
- return undefined;
1927
+ mustWrite = false;
1702
1928
  }
1703
1929
  }
1704
1930
  else if (debug) {
1705
1931
  process.stdout.write(chalk.gray(`${figures.arrowRight} Creating ${contentType.displayName} (${contentType.key}) base fragment\n`));
1706
1932
  }
1707
- const { fragment, propertyTypes } = createInitialQuery(contentType, false, undefined, forCms12);
1708
- fs.writeFileSync(baseQueryFile, fragment);
1709
- returnValue.push(contentType.key);
1710
- let dependencies = Array.isArray(propertyTypes) ? [...propertyTypes] : [];
1711
- while (Array.isArray(dependencies) && dependencies.length > 0) {
1712
- let newDependencies = [];
1713
- dependencies.forEach(dep => {
1714
- const propContentType = contentTypes.filter(x => x.key == dep[0])[0];
1715
- if (!propContentType) {
1716
- console.warn(`🟠 The content type ${dep[0]} has been referenced, but is not found in the Optimizely CMS instance`);
1717
- return;
1933
+ if (mustWrite) {
1934
+ const fragment = GraphQLGen.buildGetQuery(contentType, undefined, new Map());
1935
+ fs.writeFileSync(baseQueryFile, fragment);
1936
+ }
1937
+ return {
1938
+ written: mustWrite,
1939
+ propertyTypes: GraphQLGen.getReferencedPropertyComponents(contentType)
1940
+ };
1941
+ }
1942
+
1943
+ const SchemaDownloadCommand = {
1944
+ command: "schema:download",
1945
+ describe: "Create JSON schema files for the specified types, which you can use to configure JSON validation in your IDE of choice.",
1946
+ builder: (yargs) => {
1947
+ const newYargs = yargs;
1948
+ newYargs.option('schemaDir', { alias: 'd', description: "The schema path relative to your project root directory", string: true, type: 'string', demandOption: false, default: './.schema' });
1949
+ newYargs.option('schemas', { alias: 's', description: "The schema's to download", array: true, type: 'string', demandOption: false, default: ['DisplayTemplate', 'ContentType'] });
1950
+ newYargs.option('force', { alias: 'f', description: "Overwrite existing files", boolean: true, type: 'boolean', demandOption: false, default: false });
1951
+ return newYargs;
1952
+ },
1953
+ async handler(args, opts) {
1954
+ const { _config: cmsConfig, path: projectPath, force, schemaDir, schemas } = parseArgs(args);
1955
+ const client = createCmsClient(cmsConfig);
1956
+ // Loading Schema
1957
+ const schemaDefs = await loadSchema(client, schemas);
1958
+ const fullSchemaDir = path.normalize(path.join(projectPath, schemaDir));
1959
+ const relativeSchemaDir = path.relative(projectPath, fullSchemaDir);
1960
+ process.stdout.write(`\n${figures.arrowRight} Validating schema folder ${relativeSchemaDir}\n`);
1961
+ const schemaDirInfo = await fsAsync.stat(fullSchemaDir).catch((e) => { if (e.code == 'ENOENT')
1962
+ return undefined;
1963
+ else
1964
+ throw e; });
1965
+ if (!schemaDirInfo) {
1966
+ await fsAsync.mkdir(fullSchemaDir, { recursive: true });
1967
+ }
1968
+ else if (!schemaDirInfo.isDirectory()) {
1969
+ process.stderr.write(chalk.redBright(chalk.bold(`${figures.cross} [Error] The schema directory exists, but is not a directory (${relativeSchemaDir})\n`)));
1970
+ process.exit(1);
1971
+ }
1972
+ process.stdout.write(chalk.greenBright(` ${figures.tick} `));
1973
+ process.stdout.write(`Schema directory exists\n`);
1974
+ process.stdout.write(`\n${figures.arrowRight} Writing schema files\n`);
1975
+ for (const schema of schemaDefs) {
1976
+ const schemaFile = path.normalize(path.join(fullSchemaDir, `${schema.title.toLowerCase()}.schema.json`));
1977
+ const relativePath = path.relative(projectPath, schemaFile);
1978
+ try {
1979
+ await fsAsync.writeFile(schemaFile, JSON.stringify(schema.schema, undefined, 2), {
1980
+ encoding: "utf-8",
1981
+ flag: force ? 'w' : 'wx'
1982
+ });
1983
+ process.stdout.write(chalk.greenBright(` ${figures.tick} `));
1984
+ process.stdout.write(`Written the ${schema.title} schema to ${relativePath}\n`);
1718
1985
  }
1719
- const fullTypeName = forCms12 ? contentType.key + propContentType.key : propContentType.key;
1720
- const propertyFragmentFile = path.join(basePath, propContentType.baseType, propContentType.key, `${fullTypeName}.property.graphql`);
1721
- const propertyFragmentDir = path.dirname(propertyFragmentFile);
1722
- if (!fs.existsSync(propertyFragmentDir))
1723
- fs.mkdirSync(propertyFragmentDir, { recursive: true });
1724
- if (!fs.existsSync(propertyFragmentFile) || force) {
1725
- if (debug)
1726
- process.stdout.write(chalk.gray(`${figures.arrowRight} Writing ${propContentType.displayName} (${propContentType.key}) property fragment\n`));
1727
- const propContentTypeInfo = createInitialFragment(propContentType, true, contentType, forCms12);
1728
- fs.writeFileSync(propertyFragmentFile, propContentTypeInfo.fragment);
1729
- returnValue.push(propContentType.key);
1730
- if (Array.isArray(propContentTypeInfo.propertyTypes))
1731
- newDependencies.push(...propContentTypeInfo.propertyTypes);
1986
+ catch (err) {
1987
+ if (!force && err.code === 'EEXIST') {
1988
+ process.stdout.write(chalk.yellowBright(` ${figures.info} `));
1989
+ process.stdout.write(`The schema ${schema.title} already exists in ${relativePath}, run with -f to overwrite\n`);
1990
+ }
1991
+ else
1992
+ process.stderr.write(chalk.redBright(chalk.bold(`${figures.cross} [Error] ${err.toString()}\n`)));
1732
1993
  }
1994
+ }
1995
+ }
1996
+ };
1997
+
1998
+ const SchemaListCommand = {
1999
+ command: "schema:list",
2000
+ describe: "List all schema's that are available within the SaaS CMS instance",
2001
+ builder: (yargs) => {
2002
+ const newYargs = yargs;
2003
+ return newYargs;
2004
+ },
2005
+ async handler(args, opts) {
2006
+ const client = createCmsClient(args);
2007
+ process.stdout.write(`\n${figures.arrowRight} Downloading OpenAPI Specification\n`);
2008
+ const spec = await client.getOpenApiSpec();
2009
+ process.stdout.write(chalk.greenBright(` ${figures.tick} `));
2010
+ process.stdout.write(`The ${spec.info.title} (Version: ${spec.info.version}) specification has been downloaded.\n`);
2011
+ const specSchemas = (spec?.components?.schemas ?? {});
2012
+ const schemas = new Table({
2013
+ head: [
2014
+ chalk.yellow(chalk.bold("Key")),
2015
+ chalk.yellow(chalk.bold("Description"))
2016
+ ],
2017
+ colWidths: [35, 60],
2018
+ colAligns: ["left", "left"],
2019
+ wordWrap: true,
2020
+ wrapOnWordBoundary: true
1733
2021
  });
1734
- dependencies = newDependencies;
1735
- }
1736
- return returnValue.length > 0 ? returnValue : undefined;
1737
- }
1738
- function createInitialQuery(contentType, forProperty = false, forBaseType, forCms12 = false) {
1739
- const { fragmentFields, propertyTypes } = renderProperties(contentType, forCms12);
1740
- const fragmentTarget = forProperty ? (forCms12 ? ('') + contentType.key : contentType.key + 'Property') : contentType.key;
1741
- const tpl = `query get${fragmentTarget}Data($key: String!, $locale: [Locales], $version: String, $changeset: String, $variation: String) {
1742
- data: ${fragmentTarget} (
1743
- where: {
1744
- _metadata: {
1745
- version: { eq: $version }
1746
- changeset: { eq: $changeset }
1747
- variation: { eq: $variation }
1748
- }
1749
- }
1750
- locale: $locale
1751
- ids: [$key]
1752
- ) {
1753
- item {
1754
- _metadata {
1755
- key
1756
- }
1757
- ${fragmentFields.join("\n ")}
1758
- }
1759
- }
1760
- }`;
1761
- return {
1762
- fragment: tpl,
1763
- propertyTypes: propertyTypes.length == 0 ? null : propertyTypes
1764
- };
2022
+ for (const key in specSchemas)
2023
+ if (key) {
2024
+ schemas.push([key, specSchemas[key].description?.replaceAll(/\s+/g, ' ') ?? `-`]);
2025
+ }
2026
+ process.stdout.write("\n" + schemas.toString() + "\n");
2027
+ process.stdout.write(chalk.green(chalk.bold(figures.tick + " Done")) + "\n");
2028
+ }
2029
+ };
2030
+
2031
+ const deepmerge = createDeepMerge();
2032
+ const SchemaValidateCommand = {
2033
+ command: "schema:validate",
2034
+ describe: "Validate the opti-type.json & opti-style.json files",
2035
+ builder: (yargs) => {
2036
+ const newYargs = yargs;
2037
+ //newYargs.option('schemas', { alias: 's', description: "The schema's to download", array: true, type: 'string', demandOption: false, default: ['DisplayTemplate', 'ContentType'] })
2038
+ //newYargs.option('force', { alias: 'f', description: "Overwrite existing files", boolean: true, type: 'boolean', demandOption: false, default: false })
2039
+ //newYargs.option("definitions", { alias: 'd', description: "Create/overwrite typescript definitions", boolean: true, type: 'boolean', demandOption: false, default: true })
2040
+ return newYargs;
2041
+ },
2042
+ handler: async (args) => {
2043
+ const projectPath = args.path;
2044
+ const client = createCmsClient(args);
2045
+ const schemas = await loadSchema(client, ['DisplayTemplate', 'ContentType']);
2046
+ process.stdout.write(``);
2047
+ const styleSchema = schemas.find(x => x.title == 'DisplayTemplate')?.schema;
2048
+ const typeSchema = schemas.find(x => x.title == 'ContentType')?.schema;
2049
+ const [styleValidator, typeValidator] = await Promise.all([getValidator(styleSchema), getValidator(typeSchema)]);
2050
+ process.stdout.write(`\n${figures.arrowRight} Validating display templates (*.opti-style.json) and content types (*.opti-type.json).\n`);
2051
+ const patterns = ["./**/*.opti-type.json", "./**/*.opti-style.json"];
2052
+ const globMatches = globIterate(patterns, { cwd: projectPath, absolute: true });
2053
+ let allOk = true;
2054
+ for await (const fileMatch of globMatches) {
2055
+ try {
2056
+ const content = JSON.parse(fs.readFileSync(fileMatch).toString());
2057
+ let errors = undefined;
2058
+ if (fileMatch.endsWith('.opti-type.json') && !typeValidator(content)) {
2059
+ errors = typeValidator.errors;
2060
+ }
2061
+ else if (fileMatch.endsWith('.opti-style.json') && !styleValidator(content)) {
2062
+ errors = styleValidator.errors;
2063
+ }
2064
+ if (errors) {
2065
+ allOk = false;
2066
+ process.stdout.write(chalk.redBright(chalk.bold(`\n${figures.cross} ${path.relative(projectPath, fileMatch)}\n`)));
2067
+ const groupedErrors = groupErrors(errors);
2068
+ for (const instancepath in groupedErrors) {
2069
+ for (const key in groupedErrors[instancepath]) {
2070
+ const errorList = groupedErrors[instancepath][key].filter((v, i, a) => a.findIndex(pv => deepEqual(v, pv)) === i);
2071
+ for (const error of errorList) {
2072
+ process.stdout.write(` Node ${instancepath} ${error.message}\n`);
2073
+ switch (error.keyword) {
2074
+ case 'enum':
2075
+ process.stdout.write(` Allowed values: ${error.params.allowedValues.map((x) => x.toString()).join(', ')}\n`);
2076
+ break;
2077
+ case 'oneOf':
2078
+ const matching = Array.isArray(error.params.passingSchemas) ? error.params.passingSchemas : [];
2079
+ if (matching.length === 0)
2080
+ process.stdout.write(` Currently none matching\n`);
2081
+ else
2082
+ process.stdout.write(` Currently matching ${matching.map((x) => x.toString()).join(", ")}\n`);
2083
+ break;
2084
+ }
2085
+ }
2086
+ }
2087
+ }
2088
+ }
2089
+ }
2090
+ catch (e) {
2091
+ allOk = false;
2092
+ process.stdout.write(chalk.redBright(chalk.bold(`${figures.cross} ${path.relative(projectPath, fileMatch)}\n`)));
2093
+ process.stdout.write(` ${e.message}\n`);
2094
+ }
2095
+ }
2096
+ process.stdout.write(`\n`);
2097
+ if (allOk)
2098
+ process.stdout.write(chalk.greenBright(`${figures.tick} All types & styles match the schema\n`));
2099
+ else {
2100
+ process.stdout.write(chalk.redBright(`${figures.cross} Some types & styles contain errors\n`));
2101
+ process.exit(1);
2102
+ }
2103
+ }
2104
+ };
2105
+ function groupErrors(errors) {
2106
+ const errorrsByInstance = groupErrorsByPath(errors);
2107
+ const instancePaths = Object.getOwnPropertyNames(errorrsByInstance);
2108
+ return instancePaths.reduce((previous, current) => {
2109
+ previous[current] = groupErrorsByKeyword(errorrsByInstance[current]);
2110
+ return previous;
2111
+ }, {});
2112
+ }
2113
+ function groupErrorsByPath(errors) {
2114
+ return errors.reduce((previous, current) => {
2115
+ const instancePath = current.instancePath == "" ? "[ROOT]" : current.instancePath;
2116
+ const partial = {};
2117
+ partial[instancePath] = [current];
2118
+ return deepmerge(previous, partial);
2119
+ }, {});
2120
+ }
2121
+ function groupErrorsByKeyword(errors) {
2122
+ const toMerge = errors.reduce((previous, current) => {
2123
+ const keyword = current.keyword;
2124
+ const partial = {};
2125
+ partial[keyword] = [current];
2126
+ return deepmerge(previous, partial);
2127
+ }, {});
2128
+ // Enum errors must be merged
2129
+ if (toMerge.enum)
2130
+ toMerge.enum = [toMerge.enum.reduce((prev, current) => prev ? deepmerge(prev, current) : current, undefined)];
2131
+ return toMerge;
1765
2132
  }
1766
2133
 
2134
+ const SchemaVsCodeCommand = {
2135
+ command: "schema:vscode",
2136
+ describe: "Configure schema validation for opti-type.json & opti-style.json files by VSCode in the current project folder",
2137
+ builder: (yargs) => {
2138
+ const newYargs = yargs;
2139
+ //newYargs.option('schemas', { alias: 's', description: "The schema's to download", array: true, type: 'string', demandOption: false, default: ['DisplayTemplate', 'ContentType'] })
2140
+ //newYargs.option('force', { alias: 'f', description: "Overwrite existing files", boolean: true, type: 'boolean', demandOption: false, default: false })
2141
+ //newYargs.option("definitions", { alias: 'd', description: "Create/overwrite typescript definitions", boolean: true, type: 'boolean', demandOption: false, default: true })
2142
+ return newYargs;
2143
+ },
2144
+ handler: async (args) => {
2145
+ const projectPath = args.path;
2146
+ const client = createCmsClient(args);
2147
+ const schemas = await loadSchema(client, ['DisplayTemplate', 'ContentType']);
2148
+ process.stdout.write(`\n${figures.arrowRight} Writing schema files\n`);
2149
+ for (const schema of schemas) {
2150
+ const schemaFile = path.join(projectPath, '.vscode', `${schema.title.toLowerCase()}.schema.json`);
2151
+ void await writeFileAsync(schemaFile, JSON.stringify(schema.schema, undefined, 2));
2152
+ process.stdout.write(` ${figures.tick} Written the ${schema.title} schema to ${path.relative(projectPath, schemaFile)}\n`);
2153
+ }
2154
+ process.stdout.write(`\n${figures.arrowRight} Updating folder settings\n`);
2155
+ const settingsFile = path.join(projectPath, '.vscode', 'settings.json');
2156
+ const settings = (await readJsonFileAsync(settingsFile).catch(() => undefined)) ?? {};
2157
+ settings['json.validate.enable'] = true;
2158
+ settings['json.schemaDownload.enable'] = true;
2159
+ settings['json.schemas'] = settings['json.schemas'] || [];
2160
+ const hasContentType = settings['json.schemas'].findIndex(v => v.fileMatch.findIndex(fm => fm.endsWith('.opti-type.json')) >= 0) >= 0;
2161
+ const hasStyleDefinition = settings['json.schemas'].findIndex(v => v.fileMatch.findIndex(fm => fm.endsWith('.opti-style.json')) >= 0) >= 0;
2162
+ const hasSchemaType = settings['json.schemas'].findIndex(v => v.fileMatch.findIndex(fm => fm.endsWith('.schema.json')) >= 0) >= 0;
2163
+ if (!hasContentType)
2164
+ settings['json.schemas'].push({
2165
+ "fileMatch": [
2166
+ "**/*.opti-type.json"
2167
+ ],
2168
+ "url": "./.vscode/contenttype.schema.json"
2169
+ });
2170
+ if (!hasStyleDefinition)
2171
+ settings['json.schemas'].push({
2172
+ "fileMatch": [
2173
+ "**/*.opti-style.json"
2174
+ ],
2175
+ "url": "./.vscode/displaytemplate.schema.json"
2176
+ });
2177
+ if (!hasSchemaType)
2178
+ settings['json.schemas'].push({
2179
+ "fileMatch": [
2180
+ "**/*.schema.json"
2181
+ ],
2182
+ "url": "https://json-schema.org/draft-07/schema"
2183
+ });
2184
+ if (settings['json.schemas'].length == 0)
2185
+ delete settings['json.schemas'];
2186
+ await writeFileAsync(settingsFile, JSON.stringify(settings, undefined, 2));
2187
+ }
2188
+ };
2189
+ function readJsonFileAsync(path) {
2190
+ return new Promise((resolve, reject) => fs.readFile(path, (err, data) => {
2191
+ if (err) {
2192
+ if (err?.code === 'ENOENT')
2193
+ resolve(undefined);
2194
+ else
2195
+ reject(err);
2196
+ }
2197
+ else {
2198
+ try {
2199
+ const d = data.toString();
2200
+ resolve(JSON.parse(d));
2201
+ }
2202
+ catch (e) {
2203
+ reject(e);
2204
+ }
2205
+ }
2206
+ }));
2207
+ }
2208
+ function writeFileAsync(path, data) {
2209
+ return new Promise((resolve, reject) => {
2210
+ fs.writeFile(path, data, (err) => {
2211
+ if (err)
2212
+ reject(err);
2213
+ else
2214
+ resolve();
2215
+ });
2216
+ });
2217
+ }
2218
+
2219
+ var version = "6.0.0-pre10";
2220
+ var name = "opti-cms";
2221
+ var APP = {
2222
+ version: version,
2223
+ name: name
2224
+ };
2225
+
1767
2226
  const CmsVersionCommand = {
1768
2227
  command: "cms:version",
1769
2228
  aliases: "$0",
@@ -1782,18 +2241,25 @@ const CmsVersionCommand = {
1782
2241
  throw error;
1783
2242
  }
1784
2243
  });
2244
+ const hasPreview2Info = versionInfo.results.preview2Data?.baseUrl ? true : false;
1785
2245
  const info = new Table({
1786
2246
  head: [
1787
2247
  chalk.yellow(chalk.bold("Component")),
1788
2248
  chalk.yellow(chalk.bold("Version"))
1789
2249
  ],
1790
- colWidths: [20, 40],
2250
+ colWidths: [30, 60],
1791
2251
  colAligns: ["left", "left"]
1792
2252
  });
1793
- info.push(["Client", client.apiVersion]);
1794
- info.push(["API", versionInfo.apiVersion]);
1795
- info.push(["CMS", versionInfo.cmsVersion]);
1796
- info.push(["Service", versionInfo.serviceVersion]);
2253
+ info.push(["Base URL", versionInfo.baseUrl ?? client.cmsUrl.href]);
2254
+ if (hasPreview2Info)
2255
+ info.push(["Base URL (Preview 2)", versionInfo.results.preview2Data?.baseUrl ?? 'n/a']);
2256
+ info.push(["Client API", client.apiVersion]);
2257
+ info.push(["Service API", versionInfo.apiVersion]);
2258
+ if (hasPreview2Info)
2259
+ info.push(["Service API (Preview 2)", versionInfo.results.preview2Data?.apiVersion ?? 'n/a']);
2260
+ info.push(["CMS Build", versionInfo.cmsVersion]);
2261
+ info.push(["Service Build", versionInfo.serviceVersion]);
2262
+ info.push(["SDK", APP.version]);
1797
2263
  process.stdout.write(info.toString() + "\n");
1798
2264
  process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Optimizely CMS Status: ${versionInfo.status}\n`));
1799
2265
  process.stdout.write(chalk.green(chalk.bold(figures.tick + " Done")) + "\n");
@@ -1913,7 +2379,7 @@ async function deleteContentItem(client, key, onlyChildren = false) {
1913
2379
  process.stderr.write(chalk.redBright(` ${figures.cross}The ContentItem ${key} is a reserved item, skipping deletion\n`));
1914
2380
  return removedCount;
1915
2381
  }
1916
- const deleteResult = await client.content.contentDelete(key, true).then(r => r.key).catch((e) => {
2382
+ const deleteResult = await client.preview2ContentDelete({ path: { key }, query: { permanent: true } }).then(r => r.key).catch((e) => {
1917
2383
  if (e.status == 404)
1918
2384
  return key;
1919
2385
  throw e;
@@ -1934,10 +2400,13 @@ async function resetSystemTypes(client) {
1934
2400
  for (const systemType of reservedTypes) {
1935
2401
  if (client.debug)
1936
2402
  process.stdout.write(chalk.gray(` ${figures.arrowRight} Resetting ${systemType} by removing all attributes\n`));
1937
- const newType = await client.contentTypes.contentTypesPatch(systemType, {
1938
- key: systemType,
1939
- properties: {}
1940
- }, true).catch((e) => e.status == 404 ? undefined : null);
2403
+ const newType = await client.contentTypesPatch({
2404
+ path: { key: systemType },
2405
+ body: {
2406
+ key: systemType,
2407
+ properties: {}
2408
+ }
2409
+ }).catch((e) => e.status == 404 ? undefined : null);
1941
2410
  if (newType)
1942
2411
  resetTypes++;
1943
2412
  }
@@ -1949,7 +2418,7 @@ async function removeContentTypes(client) {
1949
2418
  for (let contentType of contentTypes.filter(item => item.source == '' && !reservedTypes.includes(item.key))) {
1950
2419
  if (client.debug)
1951
2420
  process.stdout.write(` ${chalk.blueBright(figures.arrowRight)} Removing content type ${contentType.displayName} (${contentType.key})\n`);
1952
- const result = await client.contentTypes.contentTypesDelete(contentType.key).catch((e) => {
2421
+ const result = await client.contentTypesDelete({ path: { key: contentType.key } }).catch((e) => {
1953
2422
  if (e.status == 404)
1954
2423
  return contentType;
1955
2424
  return null;
@@ -1965,7 +2434,7 @@ async function removeDisplayTemplates(client) {
1965
2434
  for (const template of allTemplates) {
1966
2435
  if (client.debug)
1967
2436
  process.stdout.write(` ${chalk.blueBright(figures.arrowRight)} Removing display template ${template.displayName} (${template.key})\n`);
1968
- const deleteResult = client.displayTemplates.displayTemplatesDelete(template.key).catch((e) => {
2437
+ const deleteResult = client.displayTemplatesDelete({ path: { key: template.key } }).catch((e) => {
1969
2438
  if (e.status == 404)
1970
2439
  return template;
1971
2440
  console.log(e.body);
@@ -1977,7 +2446,7 @@ async function removeDisplayTemplates(client) {
1977
2446
  return removedTemplateCount;
1978
2447
  }
1979
2448
  async function getAllTemplates(client, batchSize = 100) {
1980
- const items = await client.displayTemplates.displayTemplatesList(0, batchSize).catch((e) => {
2449
+ const items = await client.displayTemplatesList({ query: { pageIndex: 0, pageSize: batchSize } }).catch((e) => {
1981
2450
  if (e.status == 404) {
1982
2451
  return {
1983
2452
  items: [],
@@ -1992,7 +2461,7 @@ async function getAllTemplates(client, batchSize = 100) {
1992
2461
  const actualItems = items.items ?? [];
1993
2462
  const pageCount = Math.ceil(totalItemCount / pageSize);
1994
2463
  for (let pageNr = 1; pageNr < pageCount; pageNr++) {
1995
- const pageData = await client.displayTemplates.displayTemplatesList(pageNr, pageSize).catch((e) => {
2464
+ const pageData = await client.displayTemplatesList({ query: { pageIndex: pageNr, pageSize } }).catch((e) => {
1996
2465
  if (e.status == 404) {
1997
2466
  return {
1998
2467
  items: [],
@@ -2007,7 +2476,7 @@ async function getAllTemplates(client, batchSize = 100) {
2007
2476
  return actualItems;
2008
2477
  }
2009
2478
  async function getAllTypes(client, batchSize = 100) {
2010
- const items = await client.contentTypes.contentTypesList(undefined, undefined, 0, batchSize).catch((e) => {
2479
+ const items = await client.contentTypesList({ query: { pageIndex: 0, pageSize: batchSize } }).catch((e) => {
2011
2480
  if (e.status == 404) {
2012
2481
  return {
2013
2482
  items: [],
@@ -2022,7 +2491,7 @@ async function getAllTypes(client, batchSize = 100) {
2022
2491
  const actualItems = items.items ?? [];
2023
2492
  const pageCount = Math.ceil(totalItemCount / pageSize);
2024
2493
  for (let pageNr = 1; pageNr < pageCount; pageNr++) {
2025
- const pageData = await client.contentTypes.contentTypesList(undefined, undefined, pageNr, pageSize).catch((e) => {
2494
+ const pageData = await client.contentTypesList({ query: { pageIndex: pageNr, pageSize } }).catch((e) => {
2026
2495
  if (e.status == 404) {
2027
2496
  return {
2028
2497
  items: [],
@@ -2037,7 +2506,7 @@ async function getAllTypes(client, batchSize = 100) {
2037
2506
  return actualItems;
2038
2507
  }
2039
2508
  async function getAllAssets(client, parentKey, batchSize = 100) {
2040
- const items = await client.content.contentListAssets(parentKey, undefined, 0, batchSize).catch((e) => {
2509
+ const items = await client.preview2ContentListAssets({ path: { key: parentKey }, query: { pageIndex: 0, pageSize: batchSize } }).catch((e) => {
2041
2510
  if (e.status == 404) {
2042
2511
  return {
2043
2512
  items: [],
@@ -2052,7 +2521,7 @@ async function getAllAssets(client, parentKey, batchSize = 100) {
2052
2521
  const actualItems = items.items ?? [];
2053
2522
  const pageCount = Math.ceil(totalItemCount / pageSize);
2054
2523
  for (let pageNr = 1; pageNr < pageCount; pageNr++) {
2055
- const pageData = await client.content.contentListAssets(parentKey, undefined, pageNr, pageSize).catch((e) => {
2524
+ const pageData = await client.preview2ContentListAssets({ path: { key: parentKey }, query: { pageIndex: pageNr, pageSize } }).catch((e) => {
2056
2525
  if (e.status == 404) {
2057
2526
  return {
2058
2527
  items: [],
@@ -2067,7 +2536,7 @@ async function getAllAssets(client, parentKey, batchSize = 100) {
2067
2536
  return actualItems;
2068
2537
  }
2069
2538
  async function getAllItems(client, parentKey, batchSize = 100) {
2070
- const items = await client.content.contentListItems(parentKey, undefined, 0, batchSize).catch((e) => {
2539
+ const items = await client.preview2ContentListItems({ path: { key: parentKey }, query: { pageIndex: 0, pageSize: batchSize } }).catch((e) => {
2071
2540
  if (e.status == 404) {
2072
2541
  return {
2073
2542
  items: [],
@@ -2082,7 +2551,7 @@ async function getAllItems(client, parentKey, batchSize = 100) {
2082
2551
  const actualItems = items.items ?? [];
2083
2552
  const pageCount = Math.ceil(totalItemCount / pageSize);
2084
2553
  for (let pageNr = 1; pageNr < pageCount; pageNr++) {
2085
- const pageData = await client.content.contentListItems(parentKey, undefined, pageNr, pageSize).catch((e) => {
2554
+ const pageData = await client.preview2ContentListItems({ path: { key: parentKey }, query: { pageIndex: pageNr, pageSize } }).catch((e) => {
2086
2555
  if (e.status == 404) {
2087
2556
  return {
2088
2557
  items: [],
@@ -2099,30 +2568,26 @@ async function getAllItems(client, parentKey, batchSize = 100) {
2099
2568
 
2100
2569
  // Style processing
2101
2570
  const commands = [
2102
- StylesCreateCommand,
2103
- StylesDeleteCommand,
2104
- StylesListCommand,
2105
- StylesPullCommand,
2106
- StylesPushCommand,
2107
- TypesPullCommand,
2108
- TypesPushCommand,
2571
+ CmsResetCommand,
2572
+ CmsVersionCommand,
2109
2573
  NextJsComponentsCommand,
2110
2574
  NextJsCreateCommand,
2111
2575
  NextJsFactoryCommand,
2112
2576
  NextJsFragmentsCommand,
2113
2577
  NextJsQueriesCommand,
2114
2578
  NextJsVisualBuilderCommand,
2115
- CmsResetCommand,
2116
- CmsVersionCommand
2579
+ SchemaDownloadCommand,
2580
+ SchemaListCommand,
2581
+ SchemaValidateCommand,
2582
+ SchemaVsCodeCommand,
2583
+ StylesCreateCommand,
2584
+ StylesListCommand,
2585
+ StylesPullCommand,
2586
+ StylesPushCommand,
2587
+ TypesPullCommand,
2588
+ TypesPushCommand
2117
2589
  ];
2118
2590
 
2119
- var version = "5.1.5";
2120
- var name = "opti-cms";
2121
- var APP = {
2122
- version: version,
2123
- name: name
2124
- };
2125
-
2126
2591
  async function main() {
2127
2592
  const envFiles = prepare();
2128
2593
  const app = createOptiCmsApp(APP.name, APP.version, undefined, envFiles);
@@ -2132,7 +2597,7 @@ async function main() {
2132
2597
  }
2133
2598
  catch {
2134
2599
  //We're ignoring error here, as yargs will already generate the "nice output" for it
2135
- //console.log ('Caught error')
2600
+ //console.log('Caught error')
2136
2601
  }
2137
2602
  }
2138
2603
  main();