@stackbit/cms-core 0.2.1 → 0.3.0-develop.1

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 (54) hide show
  1. package/dist/content-store-utils.d.ts +5 -1
  2. package/dist/content-store-utils.d.ts.map +1 -1
  3. package/dist/content-store-utils.js +28 -3
  4. package/dist/content-store-utils.js.map +1 -1
  5. package/dist/content-store.d.ts +12 -1
  6. package/dist/content-store.d.ts.map +1 -1
  7. package/dist/content-store.js +399 -177
  8. package/dist/content-store.js.map +1 -1
  9. package/dist/types/content-store-document-fields.d.ts +26 -4
  10. package/dist/types/content-store-document-fields.d.ts.map +1 -1
  11. package/dist/types/content-store-documents.d.ts +14 -3
  12. package/dist/types/content-store-documents.d.ts.map +1 -1
  13. package/dist/types/content-store-types.d.ts +7 -1
  14. package/dist/types/content-store-types.d.ts.map +1 -1
  15. package/dist/utils/backward-compatibility.d.ts +184 -0
  16. package/dist/utils/backward-compatibility.d.ts.map +1 -0
  17. package/dist/utils/backward-compatibility.js +151 -0
  18. package/dist/utils/backward-compatibility.js.map +1 -0
  19. package/dist/utils/config-delegate.d.ts +11 -0
  20. package/dist/utils/config-delegate.d.ts.map +1 -0
  21. package/dist/utils/config-delegate.js +226 -0
  22. package/dist/utils/config-delegate.js.map +1 -0
  23. package/dist/utils/create-update-csi-docs.d.ts +7 -5
  24. package/dist/utils/create-update-csi-docs.d.ts.map +1 -1
  25. package/dist/utils/create-update-csi-docs.js +24 -24
  26. package/dist/utils/create-update-csi-docs.js.map +1 -1
  27. package/dist/utils/csi-to-store-docs-converter.d.ts +17 -3
  28. package/dist/utils/csi-to-store-docs-converter.d.ts.map +1 -1
  29. package/dist/utils/csi-to-store-docs-converter.js +187 -47
  30. package/dist/utils/csi-to-store-docs-converter.js.map +1 -1
  31. package/dist/utils/site-map.d.ts.map +1 -1
  32. package/dist/utils/site-map.js +4 -1
  33. package/dist/utils/site-map.js.map +1 -1
  34. package/dist/utils/store-to-api-docs-converter.d.ts +6 -1
  35. package/dist/utils/store-to-api-docs-converter.d.ts.map +1 -1
  36. package/dist/utils/store-to-api-docs-converter.js +140 -51
  37. package/dist/utils/store-to-api-docs-converter.js.map +1 -1
  38. package/dist/utils/store-to-csi-docs-converter.d.ts +1 -0
  39. package/dist/utils/store-to-csi-docs-converter.d.ts.map +1 -1
  40. package/dist/utils/store-to-csi-docs-converter.js +2 -1
  41. package/dist/utils/store-to-csi-docs-converter.js.map +1 -1
  42. package/package.json +5 -5
  43. package/src/content-store-utils.ts +40 -6
  44. package/src/content-store.ts +552 -299
  45. package/src/types/content-store-document-fields.ts +16 -4
  46. package/src/types/content-store-documents.ts +12 -3
  47. package/src/types/content-store-types.ts +4 -1
  48. package/src/utils/backward-compatibility.ts +269 -0
  49. package/src/utils/config-delegate.ts +277 -0
  50. package/src/utils/create-update-csi-docs.ts +47 -50
  51. package/src/utils/csi-to-store-docs-converter.ts +256 -43
  52. package/src/utils/site-map.ts +19 -7
  53. package/src/utils/store-to-api-docs-converter.ts +185 -52
  54. package/src/utils/store-to-csi-docs-converter.ts +1 -1
@@ -440,7 +440,7 @@ export type DocumentObjectFieldNonLocalized = {
440
440
  | { isUnset: true }
441
441
  | {
442
442
  isUnset?: false;
443
- srcObjectLabel: string;
443
+ getPreview: (options: { delegate?: CSITypes.ConfigDelegate; locale?: string }) => DocumentObjectFieldPreview;
444
444
  fields: Record<string, DocumentField>;
445
445
  }
446
446
  );
@@ -453,12 +453,18 @@ export interface DocumentObjectFieldLocalized {
453
453
  string,
454
454
  {
455
455
  locale: string;
456
- srcObjectLabel: string;
456
+ getPreview: (options: { delegate?: CSITypes.ConfigDelegate; locale?: string }) => DocumentObjectFieldPreview;
457
457
  fields: Record<string, DocumentField>;
458
458
  }
459
459
  >;
460
460
  }
461
461
 
462
+ export interface DocumentObjectFieldPreview {
463
+ previewTitle?: string;
464
+ previewSubtitle?: string;
465
+ previewImage?: unknown;
466
+ }
467
+
462
468
  export type DocumentObjectFieldAPI = {
463
469
  type: 'object';
464
470
  label?: string;
@@ -486,9 +492,9 @@ export type DocumentModelFieldNonLocalized = {
486
492
  | { isUnset: true }
487
493
  | {
488
494
  isUnset?: false;
489
- srcObjectLabel: string;
490
495
  srcModelName: string;
491
496
  srcModelLabel: string;
497
+ getPreview: (options: { delegate?: CSITypes.ConfigDelegate; locale?: string }) => DocumentModelFieldPreview;
492
498
  fields: Record<string, DocumentField>;
493
499
  }
494
500
  );
@@ -501,9 +507,9 @@ export interface DocumentModelFieldLocalized {
501
507
  string,
502
508
  {
503
509
  locale: string;
504
- srcObjectLabel: string;
505
510
  srcModelName: string;
506
511
  srcModelLabel: string;
512
+ getPreview: (options: { delegate?: CSITypes.ConfigDelegate; locale?: string }) => DocumentModelFieldPreview;
507
513
  fields: Record<string, DocumentField>;
508
514
  }
509
515
  >;
@@ -525,6 +531,12 @@ export type DocumentModelFieldAPI = {
525
531
  ) &
526
532
  ({ localized?: false } | { localized: true; locale: string });
527
533
 
534
+ export interface DocumentModelFieldPreview {
535
+ previewTitle: string;
536
+ previewSubtitle?: string;
537
+ previewImage?: unknown;
538
+ }
539
+
528
540
  /**
529
541
  * reference
530
542
  */
@@ -1,4 +1,4 @@
1
- import { DocumentStatus } from '@stackbit/types';
1
+ import { DocumentStatus, ConfigDelegate } from '@stackbit/types';
2
2
  import { DocumentField, DocumentFieldAPI, DocumentFieldAPIForType, DocumentStringLikeFieldForType } from './content-store-document-fields';
3
3
 
4
4
  export interface Document {
@@ -9,7 +9,9 @@ export interface Document {
9
9
  srcEnvironment: string;
10
10
  srcObjectId: string;
11
11
  srcObjectUrl: string;
12
- srcObjectLabel: string;
12
+ /** @deprecated used by older, non-csi cms interface */
13
+ srcObjectLabel?: string;
14
+ getPreview: (options: { delegate?: ConfigDelegate; locale?: string }) => DocumentPreview;
13
15
  srcModelName: string;
14
16
  srcModelLabel: string;
15
17
  isChanged: boolean;
@@ -22,6 +24,12 @@ export interface Document {
22
24
  fields: Record<string, DocumentField>;
23
25
  }
24
26
 
27
+ export interface DocumentPreview {
28
+ previewTitle: string;
29
+ previewSubtitle?: string;
30
+ previewImage?: unknown;
31
+ }
32
+
25
33
  export interface Asset {
26
34
  type: 'asset';
27
35
  srcType: string;
@@ -85,8 +93,9 @@ export interface AssetFileFieldProps {
85
93
  */
86
94
  export type APIObject = APIDocumentObject | APIImageObject;
87
95
 
88
- export interface APIDocumentObject extends Omit<Document, 'fields' | 'type'> {
96
+ export interface APIDocumentObject extends Omit<Document, 'getPreview' | 'fields' | 'type'> {
89
97
  type: 'object';
98
+ srcObjectLabel: string;
90
99
  fields: Record<string, DocumentFieldAPI>;
91
100
  }
92
101
 
@@ -1,12 +1,14 @@
1
1
  import * as CSITypes from '@stackbit/types';
2
2
  import { Model } from '@stackbit/sdk';
3
3
  import { Asset, Document } from './content-store-documents';
4
+ import { BackCompatContentSourceInterface } from '../utils/backward-compatibility';
4
5
 
5
6
  export interface ContentSourceData {
6
7
  /* Internal content source id computed by concatenating srcType and srcProjectId */
7
8
  id: string;
8
9
  /* The content source instance loaded from stackbitConfig.contentSources */
9
- instance: CSITypes.ContentSourceInterface;
10
+ instance: BackCompatContentSourceInterface;
11
+ version: { interfaceVersion: string; contentSourceVersion: string };
10
12
  srcType: string;
11
13
  srcProjectId: string;
12
14
  locales?: CSITypes.Locale[];
@@ -16,6 +18,7 @@ export interface ContentSourceData {
16
18
  /* Map of extended and validated Models by model name */
17
19
  modelMap: Record<string, Model>;
18
20
  /* Array of original Models (as provided by content source) */
21
+ csiSchema: CSITypes.Schema;
19
22
  csiModels: CSITypes.Model[];
20
23
  /* Map of original Models (as provided by content source) by model name */
21
24
  csiModelMap: Record<string, CSITypes.Model>;
@@ -0,0 +1,269 @@
1
+ import * as CSITypes from '@stackbit/types';
2
+
3
+ /**
4
+ * AnyContentSourceInterface is the union of the previous ContentSourceInterface
5
+ * versions.
6
+ *
7
+ * When changing the ContentSourceInterface in a way that it may break previous
8
+ * content source modules, create a new type by omitting the changed methods and
9
+ * redefine them with their new signatures. Then add the new type to the union.
10
+ */
11
+ export type AnyContentSourceInterface = ContentSourceInterface_v0_1_0 | ContentSourceInterface_v0_2_0;
12
+
13
+ export type ContentSourceInterface_v0_2_0 = CSITypes.ContentSourceInterface;
14
+
15
+ export type ContentSourceInterface_v0_1_0 = Omit<
16
+ CSITypes.ContentSourceInterface,
17
+ | 'getVersion'
18
+ | 'init'
19
+ | 'destroy'
20
+ | 'getSchema'
21
+ | 'onFilesChange'
22
+ | 'startWatchingContentUpdates'
23
+ | 'stopWatchingContentUpdates'
24
+ | 'hasAccess'
25
+ | 'getDocuments'
26
+ | 'createDocument'
27
+ | 'updateDocument'
28
+ > & {
29
+ init(options: {
30
+ logger: CSITypes.Logger;
31
+ userLogger: CSITypes.Logger;
32
+ userCommandSpawner?: CSITypes.UserCommandSpawner;
33
+ localDev: boolean;
34
+ webhookUrl?: string;
35
+ devAppRestartNeeded?: () => void;
36
+ }): Promise<void>;
37
+ getModels(): Promise<CSITypes.Model[]>;
38
+ getLocales(): Promise<CSITypes.Locale[]>;
39
+ onFilesChange?(options: { updatedFiles: string[] }): Promise<{ schemaChanged?: boolean; contentChangeEvent?: CSITypes.ContentChangeEvent }>;
40
+ startWatchingContentUpdates(options: {
41
+ getModelMap: () => CSITypes.ModelMap;
42
+ getDocument: ({ documentId }: { documentId: string }) => CSITypes.Document | undefined;
43
+ getAsset: ({ assetId }: { assetId: string }) => CSITypes.Asset | undefined;
44
+ onContentChange: (contentChangeEvent: CSITypes.ContentChangeEvent) => Promise<void>;
45
+ onSchemaChange: () => void;
46
+ }): void;
47
+ stopWatchingContentUpdates(): void;
48
+ hasAccess(options: { userContext?: unknown }): boolean | Promise<{ hasConnection: boolean; hasPermissions: boolean }>;
49
+ getDocuments(options: { modelMap: CSITypes.ModelMap }): Promise<CSITypes.Document[]>;
50
+ createDocument(options: {
51
+ updateOperationFields: Record<string, CSITypes.UpdateOperationField>;
52
+ model: CSITypes.Model;
53
+ modelMap: CSITypes.ModelMap;
54
+ locale?: string;
55
+ defaultLocaleDocumentId?: string;
56
+ userContext?: unknown;
57
+ }): Promise<CSITypes.Document>;
58
+ updateDocument(options: {
59
+ document: CSITypes.Document;
60
+ operations: CSITypes.UpdateOperation[];
61
+ modelMap: CSITypes.ModelMap;
62
+ userContext?: unknown;
63
+ }): Promise<CSITypes.Document>;
64
+ };
65
+
66
+ /**
67
+ * BackCompatContentSourceInterface redefines the ContentSourceInterface such
68
+ * that when its methods are called, it can correctly invoke the previous
69
+ * content source module versions.
70
+ *
71
+ * The parameters of its methods must be intersections of the parameters in the
72
+ * previous versions. For example, if a method in an older version received
73
+ * 'options.x', and a method in the newer version receives 'options.y', the
74
+ * matching method should receive both options '{ x } & { y }' to ensure that
75
+ * both the older and the newer content sources will receive what they need.
76
+ *
77
+ * The method return values must match the most recent content source versions.
78
+ */
79
+ export type BackCompatContentSourceInterface = Omit<
80
+ CSITypes.ContentSourceInterface,
81
+ 'getVersion' | 'onFilesChange' | 'startWatchingContentUpdates' | 'hasAccess' | 'getDocuments' | 'createDocument' | 'updateDocument'
82
+ > & {
83
+ getVersion(): GetVersionReturn;
84
+ onFilesChange(options: BCOnFilesChangeOptions): OnFilesChangeReturn;
85
+ startWatchingContentUpdates?(options: BCStartWatchingOptions): StartWatchingReturn;
86
+ hasAccess(options: BCHasAccessOptions): HasAccessReturn;
87
+ getDocuments(options: BCGetDocumentsOptions): GetDocumentsReturn;
88
+ createDocument(options: BCCreateDocumentOptions): CreateDocumentReturn;
89
+ updateDocument(options: BCUpdateDocumentOptions): UpdateDocumentReturn;
90
+ };
91
+
92
+ export function backwardCompatibleContentSource(contentSource: AnyContentSourceInterface): BackCompatContentSourceInterface {
93
+ return new Proxy(contentSource, {
94
+ get(target: AnyContentSourceInterface, prop: keyof CSITypes.ContentSourceInterface): any {
95
+ switch (prop) {
96
+ case 'getVersion':
97
+ return getVersion.bind(undefined, target);
98
+ case 'destroy':
99
+ return destroy.bind(undefined, target);
100
+ case 'onFilesChange':
101
+ return onFilesChange.bind(undefined, target);
102
+ case 'startWatchingContentUpdates':
103
+ return startWatchingContentUpdates.bind(undefined, target);
104
+ case 'getSchema':
105
+ return getSchema.bind(undefined, target);
106
+ case 'hasAccess':
107
+ return hasAccess.bind(undefined, target);
108
+ case 'getDocuments':
109
+ return getDocuments.bind(undefined, target);
110
+ case 'createDocument':
111
+ return createDocument.bind(undefined, target);
112
+ case 'updateDocument':
113
+ return updateDocument.bind(undefined, target);
114
+ default:
115
+ return target[prop];
116
+ }
117
+ }
118
+ }) as BackCompatContentSourceInterface;
119
+ }
120
+
121
+ type ReturnTypeOfMethod<Method extends keyof CSITypes.ContentSourceInterface> = ReturnType<NonNullable<CSITypes.ContentSourceInterface[Method]>>;
122
+
123
+ type GetVersionReturn = Promise<{ interfaceVersion: string; contentSourceVersion: string }>;
124
+ export async function getVersion(contentSource: AnyContentSourceInterface): GetVersionReturn {
125
+ if ('getVersion' in contentSource) {
126
+ return contentSource.getVersion();
127
+ }
128
+ return {
129
+ interfaceVersion: '0.1.0',
130
+ contentSourceVersion: ''
131
+ };
132
+ }
133
+
134
+ type DestroyReturn = ReturnTypeOfMethod<'destroy'>;
135
+ export async function destroy(contentSource: AnyContentSourceInterface): DestroyReturn {
136
+ if ('destroy' in contentSource) {
137
+ return contentSource.destroy();
138
+ }
139
+ }
140
+
141
+ type BCOnFilesChangeOptions = { updatedFiles: string[] };
142
+ type OnFilesChangeReturn = ReturnTypeOfMethod<'onFilesChange'>;
143
+ /**
144
+ * Converts the old onFilesChange API to the new one
145
+ * OLD: onFilesChange?(options: { updatedFiles: string[]; }):
146
+ * Promise<{ schemaChanged?: boolean; contentChangeEvent?: ContentChangeEvent<DocumentContext, AssetContext> }>
147
+ * NEW: onFilesChange?(options: { updatedFiles: string[]; }):
148
+ * Promise<{ invalidateSchema?: boolean; contentChanges?: ContentChanges<DocumentContext, AssetContext> }>
149
+ */
150
+ export async function onFilesChange(contentSource: AnyContentSourceInterface, options: BCOnFilesChangeOptions): OnFilesChangeReturn {
151
+ const value = await contentSource.onFilesChange?.(options);
152
+ if (!value) {
153
+ return {};
154
+ }
155
+ // if there are properties from the new API return the value as is
156
+ if ('invalidateSchema' in value || 'contentChanges' in value) {
157
+ return value;
158
+ }
159
+ // if there are properties from the old API return convert to the new API value
160
+ const result: Awaited<OnFilesChangeReturn> = {};
161
+ if ('schemaChanged' in value) {
162
+ result.invalidateSchema = value.schemaChanged;
163
+ }
164
+ if ('contentChangeEvent' in value) {
165
+ result.contentChanges = value.contentChangeEvent;
166
+ }
167
+ return result;
168
+ }
169
+
170
+ type BCStartWatchingOptions = {
171
+ getModelMap: () => CSITypes.ModelMap;
172
+ getDocument: ({ documentId }: { documentId: string }) => CSITypes.Document | undefined;
173
+ getAsset: ({ assetId }: { assetId: string }) => CSITypes.Asset | undefined;
174
+ onContentChange: (contentChangeEvent: CSITypes.ContentChangeEvent) => Promise<void>;
175
+ onSchemaChange: () => void;
176
+ };
177
+ type StartWatchingReturn = ReturnTypeOfMethod<'startWatchingContentUpdates'>;
178
+ /**
179
+ * Converts between the old startWatchingContentUpdates API and the new one.
180
+ * OLD: startWatchingContentUpdates(options: startWatchingContentUpdatesOptionsOld): void;
181
+ * NEW: startWatchingContentUpdates?(): void;
182
+ */
183
+ export function startWatchingContentUpdates(contentSource: AnyContentSourceInterface, options: BCStartWatchingOptions): StartWatchingReturn {
184
+ contentSource.startWatchingContentUpdates?.(options);
185
+ }
186
+
187
+ /**
188
+ * Converts the old getModels() and getLocales() API methods to the new getSchema() API method.
189
+ * OLD:
190
+ * getModels(): Promise<Model[]>;
191
+ * getLocales(): Promise<Locale[]>
192
+ * NEW:
193
+ * getSchema(): Promise<Schema<SchemaContext>>
194
+ */
195
+ export async function getSchema(contentSource: AnyContentSourceInterface): Promise<CSITypes.Schema> {
196
+ if ('getSchema' in contentSource) {
197
+ return contentSource.getSchema();
198
+ }
199
+ const models = await contentSource.getModels();
200
+ const locales = await contentSource.getLocales();
201
+ return { models, locales, context: null };
202
+ }
203
+
204
+ type HasAccessReturn = ReturnTypeOfMethod<'hasAccess'>;
205
+ type BCHasAccessOptions = { userContext?: unknown };
206
+ /**
207
+ * Converts the old hasAccess API to the new one
208
+ * OLD: hasAccess(options: { userContext?: UserContext }): Promise<boolean>
209
+ * NEW: hasAccess(options: { userContext?: UserContext }): Promise<{ hasConnection: boolean; hasPermissions: boolean }>
210
+ */
211
+ export async function hasAccess(contentSource: AnyContentSourceInterface, options: BCHasAccessOptions): HasAccessReturn {
212
+ const result = await contentSource.hasAccess(options);
213
+ if (typeof result === 'boolean') {
214
+ return {
215
+ hasConnection: result,
216
+ hasPermissions: result
217
+ };
218
+ }
219
+ return result;
220
+ }
221
+
222
+ type BCGetDocumentsOptions = { modelMap: CSITypes.ModelMap };
223
+ type GetDocumentsReturn = ReturnTypeOfMethod<'getDocuments'>;
224
+ /**
225
+ * Converts the old getDocuments API to the new one
226
+ * OLD: getDocuments(options: { modelMap: ModelMap }): Promise<Document[]>
227
+ * NEW: getDocuments(): Promise<Document[]>
228
+ */
229
+ export async function getDocuments(contentSource: AnyContentSourceInterface, options: BCGetDocumentsOptions): GetDocumentsReturn {
230
+ return contentSource.getDocuments(options);
231
+ }
232
+
233
+ type BCCreateDocumentOptions = {
234
+ updateOperationFields: Record<string, CSITypes.UpdateOperationField>;
235
+ model: CSITypes.Model;
236
+ modelMap: CSITypes.ModelMap;
237
+ locale?: string;
238
+ defaultLocaleDocumentId?: string;
239
+ userContext?: unknown;
240
+ };
241
+ type CreateDocumentReturn = ReturnTypeOfMethod<'createDocument'>;
242
+ /**
243
+ * Converts the old createDocument API to the new one
244
+ * OLD: createDocument(options: Options & { modelMap: ModelMap }): Promise<Document<DocumentContext>>
245
+ * NEW: createDocument(options: Options): Promise<{ documentId: string }>
246
+ */
247
+ export async function createDocument(contentSource: AnyContentSourceInterface, options: BCCreateDocumentOptions): CreateDocumentReturn {
248
+ const result = await contentSource.createDocument(options);
249
+ if ('id' in result) {
250
+ return { documentId: result.id };
251
+ }
252
+ return result;
253
+ }
254
+
255
+ type UpdateDocumentReturn = ReturnTypeOfMethod<'updateDocument'>;
256
+ type BCUpdateDocumentOptions = {
257
+ document: CSITypes.Document;
258
+ operations: CSITypes.UpdateOperation[];
259
+ modelMap: CSITypes.ModelMap;
260
+ userContext?: unknown;
261
+ };
262
+ /**
263
+ * Converts the old updateDocument API to the new one
264
+ * OLD: updateDocument(options: Options & { modelMap: ModelMap }): Promise<CSITypes.Document>;
265
+ * NEW: updateDocument(options: Options): Promise<void>
266
+ */
267
+ export async function updateDocument(contentSource: AnyContentSourceInterface, options: BCUpdateDocumentOptions): UpdateDocumentReturn {
268
+ await contentSource.updateDocument(options);
269
+ }
@@ -0,0 +1,277 @@
1
+ import _ from 'lodash';
2
+ import * as CSITypes from '@stackbit/types';
3
+ import { getLocalizedFieldForLocale } from '@stackbit/types';
4
+ import * as ContentStoreTypes from '../types';
5
+ import { getContentSourceId } from '../content-store-utils';
6
+ import { mapStoreDocumentToCSIDocumentWithSource } from './store-to-csi-docs-converter';
7
+
8
+ export function getCreateConfigDelegateThunk({
9
+ getContentSourceDataById,
10
+ logger
11
+ }: {
12
+ getContentSourceDataById: () => Record<string, ContentStoreTypes.ContentSourceData>;
13
+ logger: CSITypes.Logger;
14
+ }): () => CSITypes.ConfigDelegate {
15
+ return () =>
16
+ createConfigDelegate({
17
+ contentSourceDataById: getContentSourceDataById(),
18
+ logger: logger
19
+ });
20
+ }
21
+
22
+ export function createConfigDelegate({
23
+ contentSourceDataById,
24
+ logger
25
+ }: {
26
+ contentSourceDataById: Record<string, ContentStoreTypes.ContentSourceData>;
27
+ logger: CSITypes.Logger;
28
+ }): CSITypes.ConfigDelegate {
29
+ return {
30
+ getDocumentById: ({ id, srcType, srcProjectId }) => {
31
+ const document = findDocumentByIdAndSourceTypeOrId({
32
+ contentSourceDataById,
33
+ documentId: id,
34
+ srcType,
35
+ srcProjectId,
36
+ logger
37
+ });
38
+ if (document) {
39
+ return mapStoreDocumentToCSIDocumentWithSource(document);
40
+ }
41
+ return undefined;
42
+ },
43
+ getModelByName: ({ modelName, srcType, srcProjectId }) => {
44
+ return findModelByNameAndSourceTypeOrId({
45
+ contentSourceDataById,
46
+ modelName,
47
+ srcType,
48
+ srcProjectId,
49
+ logger
50
+ });
51
+ },
52
+ getDefaultLocaleBySource: ({ srcType, srcProjectId }) => {
53
+ // if srcType and srcProjectId are provided, use them to get the specific contentSourceData without trying to infer the right model
54
+ if (srcType && srcProjectId) {
55
+ const contentSourceId = getContentSourceId(srcType, srcProjectId);
56
+ return contentSourceDataById[contentSourceId]?.defaultLocaleCode;
57
+ }
58
+ const contentSourcesData = findContentSourcesDataForTypeOrId({
59
+ contentSourceDataById,
60
+ srcType,
61
+ srcProjectId
62
+ });
63
+ if (contentSourcesData.length === 1) {
64
+ return contentSourcesData[0]!.defaultLocaleCode;
65
+ } else if (contentSourcesData.length > 1) {
66
+ logger.warn(
67
+ `The getDefaultLocaleBySource() found more than one content sources for '${srcType}'. ` +
68
+ `Please specify 'srcType' and 'srcProjectId' to narrow down the search.`
69
+ );
70
+ }
71
+ return undefined;
72
+ },
73
+ getDocumentFieldForFieldPath: ({ document, fromField, fieldPath, locale }): CSITypes.DocumentFieldNonLocalized | undefined => {
74
+ const fieldPathArr = _.toPath(fieldPath);
75
+ const contentSourceId = getContentSourceId(document.srcType, document.srcProjectId);
76
+ const contentSource = contentSourceDataById[contentSourceId];
77
+ if (!contentSource) {
78
+ return undefined;
79
+ }
80
+
81
+ function getNestedFieldFromFieldsForPath(
82
+ fields: Record<string, CSITypes.DocumentField>,
83
+ fieldPathArr: string[],
84
+ currentContentSource: ContentStoreTypes.ContentSourceData
85
+ ): CSITypes.DocumentFieldNonLocalized | undefined {
86
+ const fieldName = fieldPathArr[0];
87
+ fieldPathArr = fieldPathArr.slice(1);
88
+ if (!fieldName) {
89
+ return undefined;
90
+ }
91
+ const field = fields[fieldName];
92
+ if (!field) {
93
+ return undefined;
94
+ }
95
+ const resolvedLocale = locale ?? currentContentSource.defaultLocaleCode;
96
+ const nonLocalizedField = getLocalizedFieldForLocale(field, resolvedLocale);
97
+ if (!nonLocalizedField) {
98
+ return undefined;
99
+ }
100
+ if (fieldPathArr.length === 0) {
101
+ return nonLocalizedField;
102
+ }
103
+ return getNestedFieldFromLocalizedFieldForPath(nonLocalizedField, fieldPathArr, currentContentSource);
104
+ }
105
+
106
+ function getNestedFieldFromLocalizedFieldForPath(
107
+ nonLocalizedField: CSITypes.DocumentFieldNonLocalized,
108
+ fieldPathArr: string[],
109
+ currentContentSource: ContentStoreTypes.ContentSourceData
110
+ ): CSITypes.DocumentFieldNonLocalized | undefined {
111
+ if (nonLocalizedField.type === 'object' || nonLocalizedField.type === 'model') {
112
+ return getNestedFieldFromFieldsForPath(nonLocalizedField.fields, fieldPathArr, currentContentSource);
113
+ } else if (nonLocalizedField.type === 'reference') {
114
+ const refDocument = currentContentSource.documentMap[nonLocalizedField.refId];
115
+ if (!refDocument) {
116
+ return undefined;
117
+ }
118
+ const fields = mapStoreDocumentToCSIDocumentWithSource(refDocument).fields;
119
+ return getNestedFieldFromFieldsForPath(fields, fieldPathArr, currentContentSource);
120
+ } else if (nonLocalizedField.type === 'cross-reference') {
121
+ const contentSourceId = getContentSourceId(nonLocalizedField.refSrcType, nonLocalizedField.refProjectId);
122
+ const contentSource = contentSourceDataById[contentSourceId];
123
+ if (!contentSource) {
124
+ return undefined;
125
+ }
126
+ const refDocument = contentSource.documentMap[nonLocalizedField.refId];
127
+ if (!refDocument) {
128
+ return undefined;
129
+ }
130
+ const fields = mapStoreDocumentToCSIDocumentWithSource(refDocument).fields;
131
+ return getNestedFieldFromFieldsForPath(fields, fieldPathArr, contentSource);
132
+ } else if (nonLocalizedField.type === 'list') {
133
+ const index = _.toNumber(fieldPathArr[0]);
134
+ fieldPathArr = fieldPathArr.slice(1);
135
+ if (_.isNaN(index)) {
136
+ return undefined;
137
+ }
138
+ const localizedItem = nonLocalizedField.items[index];
139
+ if (!localizedItem) {
140
+ return undefined;
141
+ }
142
+ if (fieldPathArr.length === 0) {
143
+ return localizedItem;
144
+ }
145
+ return getNestedFieldFromLocalizedFieldForPath(localizedItem, fieldPathArr, currentContentSource);
146
+ }
147
+ return undefined;
148
+ }
149
+
150
+ if (fromField) {
151
+ const resolvedLocale = locale ?? contentSource.defaultLocaleCode;
152
+ const nonLocalizedField = getLocalizedFieldForLocale(fromField, resolvedLocale);
153
+ if (!nonLocalizedField) {
154
+ return undefined;
155
+ }
156
+ return getNestedFieldFromLocalizedFieldForPath(nonLocalizedField, fieldPathArr, contentSource);
157
+ } else {
158
+ return getNestedFieldFromFieldsForPath(document.fields, fieldPathArr, contentSource);
159
+ }
160
+ }
161
+ };
162
+ }
163
+
164
+ function findDocumentByIdAndSourceTypeOrId({
165
+ contentSourceDataById,
166
+ documentId,
167
+ srcType,
168
+ srcProjectId,
169
+ logger
170
+ }: {
171
+ contentSourceDataById: Record<string, ContentStoreTypes.ContentSourceData>;
172
+ documentId: string;
173
+ srcType?: string;
174
+ srcProjectId?: string;
175
+ logger: CSITypes.Logger;
176
+ }): ContentStoreTypes.Document | undefined {
177
+ // if srcType and srcProjectId are provided, use them to get the specific contentSourceData without trying to infer the right document
178
+ if (srcType && srcProjectId) {
179
+ const contentSourceId = getContentSourceId(srcType, srcProjectId);
180
+ return contentSourceDataById[contentSourceId]?.documentMap[documentId];
181
+ }
182
+ const contentSourcesData = findContentSourcesDataForTypeOrId({
183
+ contentSourceDataById,
184
+ srcType,
185
+ srcProjectId
186
+ });
187
+ const matchingDocuments = contentSourcesData.reduce((matchingDocuments: ContentStoreTypes.Document[], contentSourceData) => {
188
+ const document = contentSourceData.documentMap[documentId];
189
+ if (document) {
190
+ matchingDocuments.push(document);
191
+ }
192
+ return matchingDocuments;
193
+ }, []);
194
+ if (matchingDocuments.length === 1) {
195
+ return matchingDocuments[0];
196
+ } else if (matchingDocuments.length > 1) {
197
+ const matchedContentSources = matchingDocuments.map((document) => `srcType: ${document.srcType}, srcProjectId: ${document.srcProjectId}`).join('; ');
198
+ logger.warn(
199
+ `The getDocumentById() found more than one documents with ID '${documentId}' ` +
200
+ `in the following content sources ${matchedContentSources}. ` +
201
+ `Please specify 'srcType' and/or 'srcProjectId' to narrow down the search.`
202
+ );
203
+ }
204
+ return;
205
+ }
206
+
207
+ function findModelByNameAndSourceTypeOrId({
208
+ contentSourceDataById,
209
+ modelName,
210
+ srcType,
211
+ srcProjectId,
212
+ logger
213
+ }: {
214
+ contentSourceDataById: Record<string, ContentStoreTypes.ContentSourceData>;
215
+ modelName: string;
216
+ srcType?: string;
217
+ srcProjectId?: string;
218
+ logger: CSITypes.Logger;
219
+ }): CSITypes.ModelWithSource | undefined {
220
+ // if srcType and srcProjectId are provided, use them to get the specific contentSourceData without trying to infer the right model
221
+ if (srcType && srcProjectId) {
222
+ const contentSourceId = getContentSourceId(srcType, srcProjectId);
223
+ const contentSourceData = contentSourceDataById[contentSourceId];
224
+ const model = contentSourceData?.modelMap[modelName];
225
+ if (model) {
226
+ return {
227
+ ...model,
228
+ srcType: contentSourceData.srcType,
229
+ srcProjectId: contentSourceData.srcProjectId
230
+ };
231
+ }
232
+ return undefined;
233
+ }
234
+ const contentSourcesData = findContentSourcesDataForTypeOrId({
235
+ contentSourceDataById,
236
+ srcType,
237
+ srcProjectId
238
+ });
239
+ const matchingModels = contentSourcesData.reduce((matchingModels: CSITypes.ModelWithSource[], contentSourceData) => {
240
+ const model = contentSourceData.modelMap[modelName];
241
+ if (model) {
242
+ matchingModels.push({
243
+ ...model,
244
+ srcType: contentSourceData.srcType,
245
+ srcProjectId: contentSourceData.srcProjectId
246
+ });
247
+ }
248
+ return matchingModels;
249
+ }, []);
250
+ if (matchingModels.length === 1) {
251
+ return matchingModels[0]!;
252
+ } else if (matchingModels.length > 1) {
253
+ const matchedContentSources = matchingModels.map((model) => `srcType: ${model.srcType}, srcProjectId: ${model.srcProjectId}`).join('; ');
254
+ logger.warn(
255
+ `The getModelByName() found more than one model with name '${modelName}' ` +
256
+ `in the following content sources ${matchedContentSources}. ` +
257
+ `Please specify 'srcType' and/or 'srcProjectId' to narrow down the search.`
258
+ );
259
+ }
260
+ return;
261
+ }
262
+
263
+ function findContentSourcesDataForTypeOrId({
264
+ contentSourceDataById,
265
+ srcType,
266
+ srcProjectId
267
+ }: {
268
+ contentSourceDataById: Record<string, ContentStoreTypes.ContentSourceData>;
269
+ srcType?: string;
270
+ srcProjectId?: string;
271
+ }): ContentStoreTypes.ContentSourceData[] {
272
+ return _.filter(contentSourceDataById, (contentSourceData) => {
273
+ const srcTypeMatch = !srcType || contentSourceData.srcType === srcType;
274
+ const srcProjectIdMatch = !srcProjectId || contentSourceData.srcProjectId === srcProjectId;
275
+ return srcTypeMatch && srcProjectIdMatch;
276
+ });
277
+ }