@stackbit/cms-core 0.0.3

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/src/encoder.js ADDED
@@ -0,0 +1,315 @@
1
+ const _ = require('lodash');
2
+ const consts = require('./consts');
3
+ const { mergeAtPath, omitByNil } = require('./utils');
4
+
5
+ function mapObjectFields({ data, model, fieldDataPath, fieldDataPathsInverted, fieldModelPath, encodeValue, delegate }) {
6
+ if (!fieldModelPath) {
7
+ fieldModelPath = [model.name];
8
+ }
9
+
10
+ const isRoot = fieldDataPath.length === 1;
11
+ const metadata = delegate.getItemMetadata(data, model, isRoot);
12
+ const fields = delegate.getItemFields(data, model);
13
+
14
+ return _.reduce(
15
+ fields,
16
+ (accum, field) => {
17
+ const fieldModel = _.find(model.fields, { name: field.name });
18
+
19
+ // if field model wasn't found ignore this field
20
+ if (!fieldModel) {
21
+ return accum;
22
+ }
23
+
24
+ // don't include const fields
25
+ if (_.has(fieldModel, 'const')) {
26
+ return accum;
27
+ }
28
+
29
+ let childFieldModelPath;
30
+ if (_.get(model, 'isList') && field.name === 'items') {
31
+ childFieldModelPath = fieldModelPath;
32
+ } else {
33
+ childFieldModelPath = _.concat(fieldModelPath, field.name);
34
+ }
35
+
36
+ const fieldPath = ['fields', field.name];
37
+
38
+ const mapLocalizedField = (accum, localizedField) => {
39
+ const localePath = fieldModel.localized ? ['locales', localizedField.locale] : [];
40
+ const fullFieldDataPath = _.concat(fieldPath, localePath);
41
+
42
+ const mappedData = mapField({
43
+ fieldValue: localizedField.value,
44
+ fieldModel: fieldModel,
45
+ fieldDataPath: _.concat(fieldDataPath, fullFieldDataPath),
46
+ fieldDataPaths: accum.fieldDataPaths,
47
+ fieldDataPathsInverted: fieldDataPathsInverted,
48
+ fieldModelPath: childFieldModelPath,
49
+ delegate: delegate
50
+ });
51
+ if (localizedField.locale && localizedField.locale !== '_unset') {
52
+ mappedData.fieldData.locale = localizedField.locale;
53
+ }
54
+ return {
55
+ fieldData: mergeAtPath(accum.fieldData, fullFieldDataPath, omitByNil(mappedData.fieldData))
56
+ };
57
+ };
58
+
59
+ // always create 'fields' as object, even for objects that have field names as numbers
60
+ _.setWith(accum.fieldData, fieldPath, fieldModelToFieldData(fieldModel), Object);
61
+
62
+ if (fieldModel.localized) {
63
+ accum = mapLocalizedField(accum, { locale: '_unset' });
64
+ }
65
+
66
+ if (_.has(field, 'locales')) {
67
+ return _.reduce(
68
+ field.locales,
69
+ (accum, localizedField) => {
70
+ return mapLocalizedField(accum, localizedField);
71
+ },
72
+ accum
73
+ );
74
+ }
75
+
76
+ return mapLocalizedField(accum, field);
77
+ },
78
+ {
79
+ fieldData: metadata
80
+ }
81
+ );
82
+ }
83
+
84
+ function mapField({ fieldValue, fieldModel, fieldDataPath, fieldModelPath, delegate }) {
85
+ if (_.includes(consts.SIMPLE_VALUE_FIELDS, fieldModel.type)) {
86
+ return {
87
+ fieldData: { value: fieldValue }
88
+ };
89
+ } else if (['enum', 'style'].includes(fieldModel.type)) {
90
+ return {
91
+ fieldData: { value: fieldValue }
92
+ };
93
+ } else if (fieldModel.type === 'list') {
94
+ const itemsModel = _.get(fieldModel, 'items');
95
+ let getListItemModel;
96
+ if (_.isArray(itemsModel)) {
97
+ // in Sanity, list items may have multiple types, in this case, 'items' will be an array
98
+ getListItemModel = (listItem, fieldModel) => delegate.getItemTypeForListItem(listItem, fieldModel);
99
+ } else {
100
+ // get the type of list items, if type is not defined, set string as it is the default
101
+ const listItemsType = _.get(itemsModel, 'type', 'string');
102
+ getListItemModel = _.constant(_.defaults({}, itemsModel, { type: listItemsType }));
103
+ }
104
+ return _.reduce(
105
+ fieldValue,
106
+ (accum, listItem, listIdx) => {
107
+ const itemModel = getListItemModel(listItem, fieldModel);
108
+ let mappedData;
109
+ if (!itemModel) {
110
+ mappedData = unresolvedModel();
111
+ accum.fieldData.items = _.concat(accum.fieldData.items, mappedData.fieldData);
112
+ } else {
113
+ mappedData = mapField({
114
+ fieldValue: listItem,
115
+ fieldModel: itemModel,
116
+ fieldDataPath: _.concat(fieldDataPath, ['items', listIdx]),
117
+ fieldModelPath: fieldModelPath,
118
+ delegate
119
+ });
120
+ accum.fieldData.items = _.concat(accum.fieldData.items, fieldModelToFieldData(itemModel, mappedData.fieldData));
121
+ }
122
+ return {
123
+ fieldData: accum.fieldData
124
+ };
125
+ },
126
+ {
127
+ fieldData: { items: [] }
128
+ }
129
+ );
130
+ } else if (fieldModel.type === 'object') {
131
+ // inline object
132
+ if (!fieldValue) {
133
+ return unsetObject();
134
+ }
135
+ return mapObjectFields({
136
+ data: fieldValue,
137
+ model: fieldModel,
138
+ fieldDataPath,
139
+ fieldModelPath,
140
+ delegate
141
+ });
142
+ } else if (fieldModel.type === 'reference') {
143
+ if (!fieldValue) {
144
+ return unsetObject();
145
+ }
146
+ const isLink = delegate.isLinkItem(fieldValue);
147
+ if (isLink) {
148
+ return unresolvedReference(fieldValue, delegate);
149
+ }
150
+ const model = delegate.getModelForItemOfReferenceType(fieldValue);
151
+ if (!model) {
152
+ return unresolvedModel();
153
+ }
154
+ return mapObjectFields({
155
+ data: fieldValue,
156
+ model,
157
+ fieldDataPath,
158
+ delegate
159
+ });
160
+ } else if (fieldModel.type === 'model') {
161
+ if (!fieldValue) {
162
+ return unsetObject();
163
+ }
164
+ const fieldModels = _.get(fieldModel, 'models', []);
165
+ const modelsByName = delegate.getModelsByName();
166
+ const model = fieldModels.length === 1 ? _.get(modelsByName, fieldModels[0]) : delegate.getModelForItemOfModelsType(fieldValue);
167
+ if (!model) {
168
+ return unresolvedModel();
169
+ }
170
+ return mapObjectFields({
171
+ data: fieldValue,
172
+ model,
173
+ fieldDataPath,
174
+ delegate
175
+ });
176
+ } else {
177
+ // for everything else, delegate encoding to the delegate
178
+ const fieldData = {};
179
+ const encodedField = delegate.encodeField(fieldValue, fieldModel, fieldDataPath);
180
+ if (encodedField) {
181
+ if (_.has(encodedField, 'fieldData')) {
182
+ _.assign(fieldData, encodedField.fieldData);
183
+ }
184
+ }
185
+ return {
186
+ fieldData: omitByNil(fieldData)
187
+ };
188
+ }
189
+ }
190
+
191
+ function fieldModelToFieldData(fieldModel, overrides) {
192
+ const type = ['reference', 'model'].includes(fieldModel.type) ? 'object' : fieldModel.type;
193
+ return omitByNil(
194
+ _.assign(
195
+ {
196
+ type: type,
197
+ label: fieldModel.label,
198
+ description: fieldModel.description,
199
+ // fieldType: fieldModel.type,
200
+ // fieldLabel: fieldModel.label,
201
+
202
+ // "localized" field will be excluded from final fieldData by decoder
203
+ localized: fieldModel.localized,
204
+
205
+ // "models" field will be excluded from final fieldData by decoder
206
+ models: getFieldModelNames(fieldModel)
207
+ },
208
+
209
+ fieldModel.type === 'markdown'
210
+ ? {
211
+ multiElement: true
212
+ }
213
+ : null,
214
+
215
+ fieldModel.type === 'enum'
216
+ ? {
217
+ options: _.get(fieldModel, 'options', []),
218
+ source: _.get(fieldModel, 'source')
219
+ }
220
+ : null,
221
+
222
+ overrides
223
+ )
224
+ );
225
+ }
226
+
227
+ function getFieldModelNames(fieldModel) {
228
+ const fieldType = fieldModel.type;
229
+ if (fieldType === 'reference' || fieldType === 'model') {
230
+ return _.clone(_.get(fieldModel, 'models', []));
231
+ } else if (fieldModel.type === 'list') {
232
+ const itemsModel = _.get(fieldModel, 'items');
233
+ // in Sanity, array can have items of multiple types
234
+ if (_.isArray(itemsModel)) {
235
+ return Array.from(
236
+ _.reduce(
237
+ itemsModel,
238
+ (accum, itemModel) => {
239
+ return new Set([...accum, ...(getFieldModelNames(itemModel) || [])]);
240
+ },
241
+ new Set()
242
+ )
243
+ );
244
+ } else {
245
+ return getFieldModelNames(itemsModel);
246
+ }
247
+ }
248
+ return null;
249
+ }
250
+
251
+ function unsetObject() {
252
+ return {
253
+ fieldData: {
254
+ isUnset: true
255
+ }
256
+ };
257
+ }
258
+
259
+ function unresolvedReference(fieldValue, delegate) {
260
+ return {
261
+ fieldData: {
262
+ type: 'unresolved_reference',
263
+ refId: delegate.getReferenceId(fieldValue),
264
+ refType: 'object'
265
+ }
266
+ };
267
+ }
268
+
269
+ function unresolvedModel() {
270
+ return {
271
+ fieldData: {
272
+ type: 'unresolved_model'
273
+ }
274
+ };
275
+ }
276
+
277
+ function mapData(data, prevEncodingResult, delegate) {
278
+ // scan model instances and replace their 'data' with an identity-mapped data
279
+ return _.reduce(
280
+ data,
281
+ (accum, item) => {
282
+ const model = delegate.getModelForRootItem(item);
283
+ if (!model) {
284
+ return {
285
+ fieldData: accum.fieldData
286
+ };
287
+ }
288
+ const itemId = delegate.getItemId(item);
289
+ // todo: pass the accumulated hashedData to all children, because we need to remove duplicate fieldPaths from it per strategy when a field has changed (because we might get partial field data)
290
+ // todo: create inverse map to find duplications
291
+ const mappedData = mapObjectFields({
292
+ data: item,
293
+ model: model,
294
+ fieldDataPath: [itemId],
295
+ delegate: delegate
296
+ });
297
+ return {
298
+ fieldData: _.assign(accum.fieldData, { [itemId]: mappedData.fieldData })
299
+ };
300
+ },
301
+ {
302
+ fieldData: _.get(prevEncodingResult, 'fieldData', {})
303
+ }
304
+ );
305
+ }
306
+
307
+ function encodeData({ data, prevEncodingResult, delegate }) {
308
+ data = _.cloneDeep(data);
309
+ const mappedData = mapData(data, prevEncodingResult, delegate);
310
+ return {
311
+ fieldData: mappedData.fieldData
312
+ };
313
+ }
314
+
315
+ module.exports = encodeData;
package/src/index.js ADDED
@@ -0,0 +1,13 @@
1
+ const encodeData = require('./encoder');
2
+ const stackbit = require('./stackbit');
3
+ const annotator = require('./annotator');
4
+ const utils = require('./utils');
5
+ const consts = require('./consts');
6
+
7
+ module.exports = {
8
+ utils,
9
+ consts,
10
+ annotator,
11
+ encodeData,
12
+ stackbit
13
+ };
@@ -0,0 +1,55 @@
1
+ const _ = require('lodash');
2
+ const { loadConfig, isListDataModel } = require('@stackbit/sdk');
3
+
4
+ module.exports = {
5
+ fetchAndConvertSchema
6
+ };
7
+
8
+ function fetchAndConvertSchema(options) {
9
+ return loadConfig({ dirPath: options.dirPath }).then(({ config, errors }) => {
10
+ if (!config) {
11
+ return { schema: {}, errors };
12
+ }
13
+ wrapListDataModels(config);
14
+ const schema = _.pick(config, [
15
+ 'stackbitVersion',
16
+ 'ssgName',
17
+ 'ssgVersion',
18
+ 'nodeVersion',
19
+ 'devCommand',
20
+ 'cmsName',
21
+ 'import',
22
+ 'publishDir',
23
+ 'staticDir',
24
+ 'uploadDir',
25
+ 'assets',
26
+ 'dataDir',
27
+ 'pagesDir',
28
+ 'pageLayoutKey',
29
+ 'objectTypeKey',
30
+ 'excludePages',
31
+ 'logicFields',
32
+ 'contentModels',
33
+ 'modelsSource',
34
+ 'models',
35
+ 'presets'
36
+ ]);
37
+ return { schema, errors };
38
+ });
39
+ }
40
+
41
+ function wrapListDataModels(config) {
42
+ _.forEach(config.models, (model) => {
43
+ if (!isListDataModel(model)) {
44
+ return;
45
+ }
46
+ _.set(model, 'fields', [
47
+ {
48
+ type: 'list',
49
+ name: 'items',
50
+ items: model.items
51
+ }
52
+ ]);
53
+ _.unset(model, 'items');
54
+ });
55
+ }