@stackbit/cms-contentstack 0.1.1-staging.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/README.md +1 -0
  2. package/dist/.tsbuildinfo +1 -0
  3. package/dist/contentstack-api-client.d.ts +63 -0
  4. package/dist/contentstack-api-client.d.ts.map +1 -0
  5. package/dist/contentstack-api-client.js +295 -0
  6. package/dist/contentstack-api-client.js.map +1 -0
  7. package/dist/contentstack-content-poller.d.ts +46 -0
  8. package/dist/contentstack-content-poller.d.ts.map +1 -0
  9. package/dist/contentstack-content-poller.js +111 -0
  10. package/dist/contentstack-content-poller.js.map +1 -0
  11. package/dist/contentstack-content-source.d.ts +138 -0
  12. package/dist/contentstack-content-source.d.ts.map +1 -0
  13. package/dist/contentstack-content-source.js +544 -0
  14. package/dist/contentstack-content-source.js.map +1 -0
  15. package/dist/contentstack-conversion-utils.d.ts +41 -0
  16. package/dist/contentstack-conversion-utils.d.ts.map +1 -0
  17. package/dist/contentstack-conversion-utils.js +504 -0
  18. package/dist/contentstack-conversion-utils.js.map +1 -0
  19. package/dist/contentstack-entries-converter.d.ts +39 -0
  20. package/dist/contentstack-entries-converter.d.ts.map +1 -0
  21. package/dist/contentstack-entries-converter.js +333 -0
  22. package/dist/contentstack-entries-converter.js.map +1 -0
  23. package/dist/contentstack-operation-converter.d.ts +42 -0
  24. package/dist/contentstack-operation-converter.d.ts.map +1 -0
  25. package/dist/contentstack-operation-converter.js +535 -0
  26. package/dist/contentstack-operation-converter.js.map +1 -0
  27. package/dist/contentstack-schema-converter.d.ts +26 -0
  28. package/dist/contentstack-schema-converter.d.ts.map +1 -0
  29. package/dist/contentstack-schema-converter.js +379 -0
  30. package/dist/contentstack-schema-converter.js.map +1 -0
  31. package/dist/contentstack-types.d.ts +429 -0
  32. package/dist/contentstack-types.d.ts.map +1 -0
  33. package/dist/contentstack-types.js +3 -0
  34. package/dist/contentstack-types.js.map +1 -0
  35. package/dist/contentstack-utils.d.ts +31 -0
  36. package/dist/contentstack-utils.d.ts.map +1 -0
  37. package/dist/contentstack-utils.js +144 -0
  38. package/dist/contentstack-utils.js.map +1 -0
  39. package/dist/entries-converter.d.ts +10 -0
  40. package/dist/entries-converter.d.ts.map +1 -0
  41. package/dist/entries-converter.js +245 -0
  42. package/dist/entries-converter.js.map +1 -0
  43. package/dist/file-download.d.ts +2 -0
  44. package/dist/file-download.d.ts.map +1 -0
  45. package/dist/file-download.js +33 -0
  46. package/dist/file-download.js.map +1 -0
  47. package/dist/index.d.ts +4 -0
  48. package/dist/index.d.ts.map +1 -0
  49. package/dist/index.js +14 -0
  50. package/dist/index.js.map +1 -0
  51. package/dist/schema-converter.d.ts +3 -0
  52. package/dist/schema-converter.d.ts.map +1 -0
  53. package/dist/schema-converter.js +169 -0
  54. package/dist/schema-converter.js.map +1 -0
  55. package/dist/transformation-utils.d.ts +41 -0
  56. package/dist/transformation-utils.d.ts.map +1 -0
  57. package/dist/transformation-utils.js +730 -0
  58. package/dist/transformation-utils.js.map +1 -0
  59. package/dist/types.d.ts +120 -0
  60. package/dist/types.d.ts.map +1 -0
  61. package/dist/types.js +3 -0
  62. package/dist/types.js.map +1 -0
  63. package/package.json +44 -0
  64. package/src/contentstack-api-client.ts +330 -0
  65. package/src/contentstack-content-poller.ts +157 -0
  66. package/src/contentstack-content-source.ts +687 -0
  67. package/src/contentstack-entries-converter.ts +438 -0
  68. package/src/contentstack-operation-converter.ts +703 -0
  69. package/src/contentstack-schema-converter.ts +486 -0
  70. package/src/contentstack-types.ts +527 -0
  71. package/src/contentstack-utils.ts +174 -0
  72. package/src/index.ts +3 -0
@@ -0,0 +1,486 @@
1
+ import _ from 'lodash';
2
+ import * as StackbitTypes from '@stackbit/types';
3
+ import type {
4
+ ContenteStackFieldText,
5
+ ContentStackField,
6
+ ContentStackFieldDate,
7
+ ContentStackFieldNumber,
8
+ ContentStackLocale,
9
+ ContentType,
10
+ GlobalField
11
+ } from './contentstack-types';
12
+
13
+ export type ModelWithContext = StackbitTypes.Model<ModelContext>;
14
+ export type ModelContext = {
15
+ clonedFromModel?: string;
16
+ referencedIn?: ReferencedIn;
17
+ blockMap?: BlockMap;
18
+ };
19
+ export type ReferencedIn = {
20
+ modelName: string;
21
+ fieldPath: string;
22
+ blockUID: string;
23
+ };
24
+ export type BlockMap = Record<
25
+ string,
26
+ {
27
+ modelNameToBlockId: Record<string, string>;
28
+ blockIdToModelName: Record<string, string>;
29
+ }
30
+ >;
31
+
32
+ type AddBlockMap = (options: { blockFieldPath: string; modelName: string; blockUID: string }) => void;
33
+ type AddBlockModel = (model: ModelWithContext) => void;
34
+ type CloneModelOptions = { origModelUID: string; newModelName: string; label: string; referencedIn: ReferencedIn };
35
+ type CloneModel = (options: CloneModelOptions) => void;
36
+
37
+ export function convertLocales({
38
+ contentStackLocales,
39
+ masterLocale,
40
+ logger
41
+ }: {
42
+ contentStackLocales: ContentStackLocale[];
43
+ masterLocale?: string;
44
+ logger: StackbitTypes.Logger;
45
+ }): StackbitTypes.Locale[] {
46
+ const locales: StackbitTypes.Locale[] = contentStackLocales.map((locale) => ({ code: locale.code }));
47
+
48
+ if (masterLocale) {
49
+ const defaultLocale = locales.find((locale) => locale.code === masterLocale);
50
+ if (defaultLocale) {
51
+ defaultLocale.default = true;
52
+ } else {
53
+ throw new Error(
54
+ `The 'masterLocale' '${masterLocale}' provided to ContentStackSource constructor ` +
55
+ `does not match any locale code: [${locales.map((locale) => locale.code).join(', ')}]`
56
+ );
57
+ }
58
+ } else if (locales.length === 1) {
59
+ logger.info(`The Contentstack stack has a single '${locales[0]!.code}' locale, using it as master locale`);
60
+ locales[0]!.default = true;
61
+ } else {
62
+ throw new Error(
63
+ `Stack has multiple locales: [${locales.map((locale) => locale.code).join(', ')}], ` +
64
+ `please specify which one should be used as the master locale using the 'masterLocale' property ` +
65
+ `in the ContentStackSource constructor options`
66
+ );
67
+ }
68
+
69
+ return locales;
70
+ }
71
+
72
+ export function convertModels({ contentStackModels }: { contentStackModels: (ContentType | GlobalField)[] }): ModelWithContext[] {
73
+ const modelsToClone: CloneModelOptions[] = [];
74
+ const models = contentStackModels.reduce((stackbitModels: ModelWithContext[], contentStackModel) => {
75
+ const blockModels: ModelWithContext[] = [];
76
+ const stackbitModel = convertModel({
77
+ contentStackModel: contentStackModel,
78
+ addBlockModel: (model: ModelWithContext) => {
79
+ blockModels.push(model);
80
+ },
81
+ cloneModel: (options) => {
82
+ modelsToClone.push(options);
83
+ }
84
+ });
85
+ return stackbitModels.concat(stackbitModel, blockModels);
86
+ }, []);
87
+ const modelMap = _.keyBy(models, 'name');
88
+ const clonedModels = modelsToClone.map((options): ModelWithContext => {
89
+ const model = modelMap[options.origModelUID];
90
+ if (!model) {
91
+ throw new Error(`could not clone model, model ${options.origModelUID} not found`);
92
+ }
93
+ return {
94
+ ...model,
95
+ name: options.newModelName,
96
+ label: options.label,
97
+ context: _.omitBy(
98
+ {
99
+ clonedFromModel: options.origModelUID,
100
+ referencedIn: options.referencedIn
101
+ },
102
+ _.isNil
103
+ )
104
+ };
105
+ });
106
+ const linkModel: ModelWithContext = {
107
+ type: 'object',
108
+ name: 'CNTSTK_LINK_MODEL',
109
+ label: 'Link',
110
+ fields: [
111
+ { type: 'string', name: 'title', label: 'Title', default: '' },
112
+ { type: 'string', name: 'href', label: 'URL', default: '' }
113
+ ]
114
+ };
115
+ return [...models, ...clonedModels, linkModel];
116
+ }
117
+
118
+ function convertModel({
119
+ contentStackModel,
120
+ addBlockModel,
121
+ cloneModel,
122
+ referencedIn
123
+ }: {
124
+ contentStackModel: ContentType | GlobalField;
125
+ addBlockModel: AddBlockModel;
126
+ cloneModel: CloneModel;
127
+ referencedIn?: ReferencedIn;
128
+ }): ModelWithContext {
129
+ const blockMap: BlockMap = {};
130
+ const isContentType = 'options' in contentStackModel;
131
+ return {
132
+ type: isContentType ? (contentStackModel.options?.is_page ? 'page' : 'data') : 'object',
133
+ name: contentStackModel.uid,
134
+ label: contentStackModel.title,
135
+ ...(_.isEmpty(contentStackModel.description) ? null : { description: contentStackModel.description }),
136
+ ...(isContentType ? { labelField: 'title' } : null),
137
+ ...(isContentType && contentStackModel.options?.singleton ? { singleInstance: true } : null),
138
+ fields: convertFields({
139
+ contentStackFields: contentStackModel.schema,
140
+ parentModelName: contentStackModel.uid,
141
+ isNested: !!referencedIn,
142
+ fieldPath: [],
143
+ addBlockMap: ({ blockFieldPath, modelName, blockUID }) => {
144
+ if (!blockMap[blockFieldPath]) {
145
+ blockMap[blockFieldPath] = {
146
+ modelNameToBlockId: {},
147
+ blockIdToModelName: {}
148
+ };
149
+ }
150
+ blockMap[blockFieldPath]!.modelNameToBlockId[modelName] = blockUID;
151
+ blockMap[blockFieldPath]!.blockIdToModelName[blockUID] = modelName;
152
+ },
153
+ addBlockModel,
154
+ cloneModel
155
+ }),
156
+ context: _.omitBy(
157
+ {
158
+ ...(_.isEmpty(blockMap) ? null : { blockMap }),
159
+ referencedIn
160
+ },
161
+ _.isNil
162
+ )
163
+ };
164
+ }
165
+
166
+ function convertFields(options: {
167
+ contentStackFields: ContentStackField[];
168
+ parentModelName: string;
169
+ isNested: boolean;
170
+ fieldPath: string[];
171
+ addBlockMap: AddBlockMap;
172
+ addBlockModel: AddBlockModel;
173
+ cloneModel: CloneModel;
174
+ }): StackbitTypes.Field[] {
175
+ const { contentStackFields, fieldPath, ...rest } = options;
176
+ return contentStackFields.map((field) =>
177
+ convertField({
178
+ field,
179
+ fieldPath: fieldPath.concat(field.uid),
180
+ ...rest
181
+ })
182
+ );
183
+ }
184
+
185
+ function convertField({
186
+ field,
187
+ parentModelName,
188
+ isNested,
189
+ fieldPath,
190
+ addBlockMap,
191
+ addBlockModel,
192
+ cloneModel
193
+ }: {
194
+ field: ContentStackField;
195
+ parentModelName: string;
196
+ isNested: boolean;
197
+ fieldPath: string[];
198
+ addBlockMap: AddBlockMap;
199
+ addBlockModel: AddBlockModel;
200
+ cloneModel: CloneModel;
201
+ }): StackbitTypes.Field {
202
+ switch (field.data_type) {
203
+ case 'text': {
204
+ if (field.field_metadata?.multiline) {
205
+ return toFieldOrListField(field, {
206
+ type: 'text'
207
+ });
208
+ } else if (field.field_metadata?.markdown) {
209
+ return toFieldOrListField(field, {
210
+ type: 'markdown'
211
+ });
212
+ } else if (field.field_metadata?.allow_rich_text) {
213
+ return toFieldOrListField(field, {
214
+ // Stackbit doesn't support HTML editor
215
+ // type: 'html'
216
+ type: 'text'
217
+ });
218
+ } else if (field.enum) {
219
+ return toEnumField(field as StackbitTypes.RequiredBy<ContenteStackFieldText, 'enum'>);
220
+ } else if (field.uid === 'slug' || (field.uid === 'url' && field.field_metadata?._default === true)) {
221
+ return toFieldOrListField(field, {
222
+ type: 'slug'
223
+ });
224
+ } else {
225
+ return toFieldOrListField(field, {
226
+ type: 'string'
227
+ });
228
+ }
229
+ }
230
+
231
+ case 'boolean':
232
+ return toFieldOrListField(field, {
233
+ type: 'boolean'
234
+ });
235
+
236
+ case 'json':
237
+ if (field.field_metadata?.allow_json_rte) {
238
+ return toFieldOrListField(field, {
239
+ type: 'richText'
240
+ });
241
+ }
242
+ return toFieldOrListField(field, {
243
+ type: 'json'
244
+ });
245
+
246
+ case 'file':
247
+ return toFieldOrListField(field, {
248
+ type: field.field_metadata?.image ? 'image' : 'file'
249
+ });
250
+
251
+ case 'isodate': {
252
+ const fieldType = field.field_metadata?.hide_time ? 'date' : 'datetime';
253
+ return toFieldOrListField(
254
+ field,
255
+ {
256
+ type: fieldType
257
+ },
258
+ (defaultValue: ContentStackFieldDate['field_metadata']['default_value']) => {
259
+ if (defaultValue?.custom && defaultValue?.date) {
260
+ if (fieldType === 'datetime' && defaultValue?.time) {
261
+ return new Date(`${defaultValue?.date}T${defaultValue?.time}`).toISOString();
262
+ } else {
263
+ return defaultValue?.date;
264
+ }
265
+ }
266
+ return undefined;
267
+ }
268
+ );
269
+ }
270
+ case 'number': {
271
+ if (field.enum) {
272
+ return toEnumField(field as StackbitTypes.RequiredBy<ContentStackFieldNumber, 'enum'>);
273
+ }
274
+ const min = !_.isNil(field.min) ? { min: field.min } : null;
275
+ const max = !_.isNil(field.max) ? { max: field.max } : null;
276
+ return toFieldOrListField(field, {
277
+ type: 'number',
278
+ ...min,
279
+ ...max
280
+ });
281
+ }
282
+
283
+ case 'link': {
284
+ return toFieldOrListField(
285
+ field,
286
+ {
287
+ type: 'model',
288
+ models: ['CNTSTK_LINK_MODEL']
289
+ },
290
+ (value: any): any => {
291
+ // rewrite url property to href
292
+ if (!value) {
293
+ return value;
294
+ }
295
+ const { url, ...rest } = value;
296
+ return {
297
+ ...rest,
298
+ href: url
299
+ };
300
+ }
301
+ );
302
+ }
303
+
304
+ case 'group': {
305
+ return toFieldOrListField(field, {
306
+ type: 'object',
307
+ fields: convertFields({
308
+ contentStackFields: field.schema,
309
+ parentModelName: parentModelName,
310
+ isNested,
311
+ fieldPath,
312
+ addBlockMap,
313
+ addBlockModel,
314
+ cloneModel
315
+ })
316
+ });
317
+ }
318
+
319
+ case 'global_field': {
320
+ // ContentStack global fields can only reference one model
321
+ return toFieldOrListField(field, {
322
+ type: 'model',
323
+ models: field.reference_to ? [field.reference_to] : []
324
+ });
325
+ }
326
+
327
+ case 'reference': {
328
+ // in reference fields, the "field_metadata.ref_multiple" is responsible for marking the field as list, not "multiple".
329
+ return toFieldOrListField(field, {
330
+ type: 'reference',
331
+ models: field.reference_to || []
332
+ });
333
+ }
334
+
335
+ case 'blocks': {
336
+ const commonProps = getStackbitFieldPropsFromContentStackField(field, true);
337
+ // blocks, are always lists
338
+ const referencedModelCountMap: Record<string, number> = {};
339
+ return {
340
+ type: 'list',
341
+ ...commonProps,
342
+ items: {
343
+ type: 'model',
344
+ models: field.blocks.map((block) => {
345
+ const modelName = `${isNested ? '' : 'BLOCK_'}${parentModelName}__${fieldPath.length ? fieldPath.join('.') + '__' : ''}${block.uid}`;
346
+ if ('schema' in block) {
347
+ const contentStackModel = {
348
+ uid: modelName,
349
+ title: block.title,
350
+ schema: block.schema
351
+ } as GlobalField;
352
+ const blockModel = convertModel({
353
+ contentStackModel,
354
+ addBlockModel,
355
+ cloneModel,
356
+ referencedIn: {
357
+ modelName: parentModelName,
358
+ fieldPath: fieldPath.join('.'),
359
+ blockUID: block.uid
360
+ }
361
+ });
362
+ addBlockModel(blockModel);
363
+ addBlockMap({ blockFieldPath: fieldPath.join('.'), modelName, blockUID: block.uid });
364
+ return modelName;
365
+ } else if ('reference_to' in block) {
366
+ // Blocks can reference the same global field in different blocks (with different uid)
367
+ // If the referenced global field (object model) is defined only in one block,
368
+ // then don't create a new model and reuse the referenced model. Add a map entry between
369
+ // the block's uid and the model's name, so when a new object is added to this field,
370
+ // we will know to which block uid to assign.
371
+ if (typeof referencedModelCountMap[block.reference_to] === 'undefined') {
372
+ referencedModelCountMap[block.reference_to] = 1;
373
+ } else {
374
+ referencedModelCountMap[block.reference_to] += 1;
375
+ }
376
+ if (referencedModelCountMap[block.reference_to] === 1) {
377
+ addBlockMap({ blockFieldPath: fieldPath.join('.'), modelName: block.reference_to, blockUID: block.uid });
378
+ return block.reference_to;
379
+ } else {
380
+ // If the referenced global field is used more than once, then create a new model
381
+ // by cloning the original model and ad a mao entry between the block's uid and the
382
+ // cloned model name.
383
+ cloneModel({
384
+ origModelUID: block.reference_to,
385
+ newModelName: modelName,
386
+ label: block.title,
387
+ referencedIn: {
388
+ modelName: parentModelName,
389
+ fieldPath: fieldPath.join('.'),
390
+ blockUID: block.uid
391
+ }
392
+ });
393
+ addBlockMap({ blockFieldPath: fieldPath.join('.'), modelName, blockUID: block.uid });
394
+ return modelName;
395
+ }
396
+ } else {
397
+ throw new Error(`block doesn't have 'schema' nor 'reference_to'`);
398
+ }
399
+ })
400
+ }
401
+ };
402
+ }
403
+ default: {
404
+ const _exhaustiveCheck: never = field;
405
+ return _exhaustiveCheck;
406
+ }
407
+ }
408
+ }
409
+
410
+ function toEnumField(
411
+ field: StackbitTypes.RequiredBy<ContenteStackFieldText, 'enum'> | StackbitTypes.RequiredBy<ContentStackFieldNumber, 'enum'>
412
+ ): StackbitTypes.Field {
413
+ const isList = field.multiple;
414
+ const commonProps = getStackbitFieldPropsFromContentStackField(field, isList);
415
+ const options: StackbitTypes.FieldEnumOptionValue[] | StackbitTypes.FieldEnumOptionObject[] = field.enum.advanced
416
+ ? field.enum.choices.map((choice) => ({ label: choice.key, value: choice.value }))
417
+ : field.enum.choices.map((choice) => choice.value);
418
+ if (isList) {
419
+ return {
420
+ type: 'list',
421
+ ...commonProps,
422
+ ...(field.display_type === 'checkbox' ? { controlType: 'checkbox' } : null),
423
+ items: {
424
+ type: 'enum',
425
+ options
426
+ }
427
+ };
428
+ } else {
429
+ return {
430
+ type: 'enum',
431
+ ...commonProps,
432
+ controlType: field.display_type === 'radio' ? 'button-group' : 'dropdown',
433
+ options: options
434
+ };
435
+ }
436
+ }
437
+
438
+ function toFieldOrListField(
439
+ field: ContentStackField,
440
+ fieldSpecificProps: Exclude<StackbitTypes.FieldSpecificProps, StackbitTypes.FieldStyleProps | StackbitTypes.FieldListProps>,
441
+ resolveDefault?: (value: unknown) => unknown
442
+ ): Exclude<StackbitTypes.Field, StackbitTypes.FieldStyle> {
443
+ // In reference fields, the "field_metadata.ref_multiple" is responsible for marking the field as list, not "multiple".
444
+ const isList = field.data_type === 'reference' ? field.field_metadata?.ref_multiple : field.multiple;
445
+ // In lists resolve defaults to a list with a single value
446
+ const commonProps = getStackbitFieldPropsFromContentStackField(field, isList, resolveDefault);
447
+ if (isList) {
448
+ return {
449
+ type: 'list',
450
+ ...commonProps,
451
+ items: fieldSpecificProps
452
+ };
453
+ }
454
+ return {
455
+ ...fieldSpecificProps,
456
+ ...commonProps
457
+ };
458
+ }
459
+
460
+ function getStackbitFieldPropsFromContentStackField(
461
+ field: ContentStackField,
462
+ isList: boolean,
463
+ resolveDefault?: (value: unknown) => unknown
464
+ ): {
465
+ name: string;
466
+ label?: string;
467
+ description?: string;
468
+ required?: boolean;
469
+ default?: any;
470
+ } {
471
+ const description = field.field_metadata?.description;
472
+ const defaultValue = field.field_metadata?.default_value;
473
+ const resolvedDefault = defaultValue === null || defaultValue === '' ? undefined : resolveDefault ? resolveDefault(defaultValue) : defaultValue;
474
+ return {
475
+ name: field.uid,
476
+ label: field.display_name,
477
+ ..._.omitBy(
478
+ {
479
+ description: description || undefined,
480
+ required: field.mandatory || undefined,
481
+ default: !_.isNil(resolvedDefault) && isList ? [resolvedDefault] : resolvedDefault
482
+ },
483
+ _.isNil
484
+ )
485
+ };
486
+ }