@stackbit/sdk 0.2.19 → 0.2.23
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/config/config-consts.d.ts +1 -1
- package/dist/config/config-consts.js +2 -0
- package/dist/config/config-consts.js.map +1 -1
- package/dist/config/config-loader.d.ts +3 -2
- package/dist/config/config-loader.js +256 -72
- package/dist/config/config-loader.js.map +1 -1
- package/dist/config/config-schema.js +28 -7
- package/dist/config/config-schema.js.map +1 -1
- package/dist/config/config-types.d.ts +22 -5
- package/dist/config/config-writer.js +3 -0
- package/dist/config/config-writer.js.map +1 -1
- package/dist/config/presets-loader.js +18 -11
- package/dist/config/presets-loader.js.map +1 -1
- package/dist/content/content-loader.js +1 -1
- package/dist/content/content-schema.js +8 -0
- package/dist/content/content-schema.js.map +1 -1
- package/dist/utils/model-extender.js +3 -2
- package/dist/utils/model-extender.js.map +1 -1
- package/dist/utils/model-iterators.d.ts +61 -1
- package/dist/utils/model-iterators.js +60 -11
- package/dist/utils/model-iterators.js.map +1 -1
- package/dist/utils/model-utils.d.ts +44 -3
- package/dist/utils/model-utils.js +93 -10
- package/dist/utils/model-utils.js.map +1 -1
- package/package.json +2 -2
- package/src/.DS_Store +0 -0
- package/src/config/config-consts.ts +2 -0
- package/src/config/config-loader.ts +281 -83
- package/src/config/config-schema.ts +35 -8
- package/src/config/config-types.ts +26 -5
- package/src/config/config-writer.ts +3 -0
- package/src/config/presets-loader.ts +19 -15
- package/src/content/content-loader.ts +2 -2
- package/src/content/content-schema.ts +9 -0
- package/src/utils/model-extender.ts +4 -3
- package/src/utils/model-iterators.ts +61 -13
- package/src/utils/model-utils.ts +91 -8
|
@@ -9,7 +9,7 @@ import { ConfigError, ConfigLoadError, ConfigValidationError } from './config-er
|
|
|
9
9
|
import {
|
|
10
10
|
assignLabelFieldIfNeeded,
|
|
11
11
|
extendModelMap,
|
|
12
|
-
|
|
12
|
+
getListFieldItems,
|
|
13
13
|
isCustomModelField,
|
|
14
14
|
isEnumField,
|
|
15
15
|
isListDataModel,
|
|
@@ -20,13 +20,17 @@ import {
|
|
|
20
20
|
isPageModel,
|
|
21
21
|
isDataModel,
|
|
22
22
|
isReferenceField,
|
|
23
|
-
iterateModelFieldsRecursively
|
|
23
|
+
iterateModelFieldsRecursively,
|
|
24
|
+
mapModelFieldsRecursively,
|
|
25
|
+
normalizeListFieldInPlace,
|
|
26
|
+
getModelFieldForModelKeyPath
|
|
24
27
|
} from '../utils';
|
|
25
|
-
import { append, parseFile, readDirRecursively, reducePromise, rename } from '@stackbit/utils';
|
|
28
|
+
import { append, omitByNil, parseFile, readDirRecursively, reducePromise, rename } from '@stackbit/utils';
|
|
26
29
|
import { Config, DataModel, FieldEnum, FieldModel, FieldObjectProps, Model, PageModel, YamlModel } from './config-types';
|
|
27
30
|
import { loadPresets } from './presets-loader';
|
|
28
31
|
|
|
29
32
|
export interface ConfigLoaderOptions {
|
|
33
|
+
[option: string]: any;
|
|
30
34
|
dirPath: string;
|
|
31
35
|
}
|
|
32
36
|
|
|
@@ -43,11 +47,11 @@ export interface NormalizedValidationResult {
|
|
|
43
47
|
}
|
|
44
48
|
|
|
45
49
|
export interface TempConfigLoaderResult {
|
|
46
|
-
config?:
|
|
50
|
+
config?: Record<string, unknown>;
|
|
47
51
|
errors: ConfigLoadError[];
|
|
48
52
|
}
|
|
49
53
|
|
|
50
|
-
export async function loadConfig({ dirPath }: ConfigLoaderOptions): Promise<ConfigLoaderResult> {
|
|
54
|
+
export async function loadConfig({ dirPath, ...options }: ConfigLoaderOptions): Promise<ConfigLoaderResult> {
|
|
51
55
|
let configLoadResult: TempConfigLoaderResult;
|
|
52
56
|
try {
|
|
53
57
|
configLoadResult = await loadConfigFromDir(dirPath);
|
|
@@ -67,14 +71,18 @@ export async function loadConfig({ dirPath }: ConfigLoaderOptions): Promise<Conf
|
|
|
67
71
|
};
|
|
68
72
|
}
|
|
69
73
|
|
|
70
|
-
const
|
|
74
|
+
const externalModelsResult = await loadModelsFromExternalSource(configLoadResult.config, options);
|
|
75
|
+
|
|
76
|
+
const mergedConfig = mergeConfigWithExternalModels(configLoadResult.config, externalModelsResult.models);
|
|
77
|
+
|
|
78
|
+
const normalizedResult = validateAndNormalizeConfig(mergedConfig);
|
|
71
79
|
|
|
72
80
|
const presetsResult = await loadPresets(dirPath, normalizedResult.config);
|
|
73
81
|
|
|
74
82
|
return {
|
|
75
83
|
valid: normalizedResult.valid,
|
|
76
84
|
config: presetsResult.config,
|
|
77
|
-
errors: [...configLoadResult.errors, ...normalizedResult.errors, ...presetsResult.errors]
|
|
85
|
+
errors: [...configLoadResult.errors, ...externalModelsResult.errors, ...normalizedResult.errors, ...presetsResult.errors]
|
|
78
86
|
};
|
|
79
87
|
}
|
|
80
88
|
|
|
@@ -110,14 +118,14 @@ async function loadConfigFromDir(dirPath: string): Promise<TempConfigLoaderResul
|
|
|
110
118
|
if (error) {
|
|
111
119
|
return { errors: [error] };
|
|
112
120
|
}
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
return { config, errors:
|
|
121
|
+
const modelsFromFileResult = await loadModelsFromFiles(dirPath, config);
|
|
122
|
+
const mergedConfig = mergeConfigModelsWithModelsFromFiles(config, modelsFromFileResult.models);
|
|
123
|
+
return { config: mergedConfig, errors: modelsFromFileResult.errors };
|
|
116
124
|
}
|
|
117
125
|
|
|
118
|
-
type
|
|
126
|
+
type StackbitYamlConfigResult = { config: any; error?: undefined } | { config?: undefined; error: ConfigLoadError };
|
|
119
127
|
|
|
120
|
-
async function loadConfigFromStackbitYaml(dirPath: string): Promise<
|
|
128
|
+
async function loadConfigFromStackbitYaml(dirPath: string): Promise<StackbitYamlConfigResult> {
|
|
121
129
|
const stackbitYamlPath = path.join(dirPath, 'stackbit.yaml');
|
|
122
130
|
const stackbitYamlExists = await fse.pathExists(stackbitYamlPath);
|
|
123
131
|
if (!stackbitYamlExists) {
|
|
@@ -135,54 +143,56 @@ async function loadConfigFromStackbitYaml(dirPath: string): Promise<LoadConfigFr
|
|
|
135
143
|
return { config };
|
|
136
144
|
}
|
|
137
145
|
|
|
138
|
-
async function
|
|
146
|
+
async function loadModelsFromFiles(dirPath: string, config: any): Promise<{ models: Record<string, any>; errors: ConfigLoadError[] }> {
|
|
139
147
|
const modelsSource = _.get(config, 'modelsSource', {});
|
|
140
148
|
const sourceType = _.get(modelsSource, 'type', 'files');
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
return modelFiles
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
return {
|
|
172
|
-
models: result.models,
|
|
173
|
-
errors: result.errors.concat(new ConfigLoadError(`model does not have a name, file: ${modelFile}`))
|
|
174
|
-
};
|
|
175
|
-
}
|
|
176
|
-
result.models[modelName] = _.omit(model, 'name');
|
|
177
|
-
result.models[modelName].__metadata = {
|
|
178
|
-
filePath: modelFile
|
|
149
|
+
const defaultModelDirs = ['node_modules/@stackbit/components/models', '.stackbit/models'];
|
|
150
|
+
const modelDirs =
|
|
151
|
+
sourceType === 'files'
|
|
152
|
+
? _.castArray(_.get(modelsSource, 'modelDirs', defaultModelDirs)).map((modelDir: string) => _.trim(modelDir, '/'))
|
|
153
|
+
: defaultModelDirs;
|
|
154
|
+
|
|
155
|
+
const modelFiles = await reducePromise(
|
|
156
|
+
modelDirs,
|
|
157
|
+
async (modelFiles: string[], modelDir) => {
|
|
158
|
+
const absModelsDir = path.join(dirPath, modelDir);
|
|
159
|
+
const dirExists = await fse.pathExists(absModelsDir);
|
|
160
|
+
if (!dirExists) {
|
|
161
|
+
return modelFiles;
|
|
162
|
+
}
|
|
163
|
+
const files = await readModelFilesFromDir(absModelsDir);
|
|
164
|
+
return modelFiles.concat(files.map((filePath) => path.join(modelDir, filePath)));
|
|
165
|
+
},
|
|
166
|
+
[]
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
return reducePromise(
|
|
170
|
+
modelFiles,
|
|
171
|
+
async (result: { models: any; errors: ConfigLoadError[] }, modelFile) => {
|
|
172
|
+
let model;
|
|
173
|
+
try {
|
|
174
|
+
model = await parseFile(path.join(dirPath, modelFile));
|
|
175
|
+
} catch (error) {
|
|
176
|
+
return {
|
|
177
|
+
models: result.models,
|
|
178
|
+
errors: result.errors.concat(new ConfigLoadError(`error parsing model, file: ${modelFile}`))
|
|
179
179
|
};
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
180
|
+
}
|
|
181
|
+
const modelName = model?.name;
|
|
182
|
+
if (!modelName) {
|
|
183
|
+
return {
|
|
184
|
+
models: result.models,
|
|
185
|
+
errors: result.errors.concat(new ConfigLoadError(`model does not have a name, file: ${modelFile}`))
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
result.models[modelName] = _.omit(model, 'name');
|
|
189
|
+
result.models[modelName].__metadata = {
|
|
190
|
+
filePath: modelFile
|
|
191
|
+
};
|
|
192
|
+
return result;
|
|
193
|
+
},
|
|
194
|
+
{ models: {}, errors: [] }
|
|
195
|
+
);
|
|
186
196
|
}
|
|
187
197
|
|
|
188
198
|
async function readModelFilesFromDir(modelsDir: string) {
|
|
@@ -197,6 +207,33 @@ async function readModelFilesFromDir(modelsDir: string) {
|
|
|
197
207
|
});
|
|
198
208
|
}
|
|
199
209
|
|
|
210
|
+
async function loadModelsFromExternalSource(config: any, options: any): Promise<{ models: Model[]; errors: ConfigLoadError[] }> {
|
|
211
|
+
const modelsSource = _.get(config, 'modelsSource', {});
|
|
212
|
+
const sourceType = _.get(modelsSource, 'type', 'files');
|
|
213
|
+
if (sourceType === 'files') {
|
|
214
|
+
return { models: [], errors: [] };
|
|
215
|
+
} else if (sourceType === 'contentful') {
|
|
216
|
+
const contentfulModule = _.get(modelsSource, 'module', '@stackbit/cms-contentful');
|
|
217
|
+
const module = await import(contentfulModule);
|
|
218
|
+
try {
|
|
219
|
+
const { models } = await module.fetchAndConvertSchema(options);
|
|
220
|
+
return {
|
|
221
|
+
models: models,
|
|
222
|
+
errors: []
|
|
223
|
+
};
|
|
224
|
+
} catch (error) {
|
|
225
|
+
return {
|
|
226
|
+
models: [],
|
|
227
|
+
errors: [new ConfigLoadError(`Error fetching and converting Contentful schema, error: ${error.message}`, { originalError: error })]
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return {
|
|
232
|
+
models: [],
|
|
233
|
+
errors: [new ConfigLoadError(`modelsSource ${modelsSource} is unsupported`)]
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
200
237
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
201
238
|
async function loadConfigFromDotStackbit(dirPath: string) {
|
|
202
239
|
const stackbitDotPath = path.join(dirPath, '.stackbit');
|
|
@@ -231,6 +268,101 @@ async function loadConfigFromDotStackbit(dirPath: string) {
|
|
|
231
268
|
return _.isEmpty(config) ? null : config;
|
|
232
269
|
}
|
|
233
270
|
|
|
271
|
+
function mergeConfigModelsWithModelsFromFiles(config: any, modelsFromFiles: Record<string, any>) {
|
|
272
|
+
const configModels = config.models ?? {};
|
|
273
|
+
const mergedModels = _.mapValues(modelsFromFiles, (modelFromFile, modelName) => {
|
|
274
|
+
// resolve thumbnails of models loaded from files
|
|
275
|
+
const modelFilePath = modelFromFile.__metadata?.filePath;
|
|
276
|
+
resolveThumbnailPathForModel(modelFromFile, modelFilePath);
|
|
277
|
+
iterateModelFieldsRecursively(modelFromFile, (field: any) => {
|
|
278
|
+
if (isListField(field)) {
|
|
279
|
+
field = normalizeListFieldInPlace(field);
|
|
280
|
+
field = field.items;
|
|
281
|
+
}
|
|
282
|
+
if (isObjectField(field)) {
|
|
283
|
+
resolveThumbnailPathForModel(field, modelFilePath);
|
|
284
|
+
} else if (isEnumField(field)) {
|
|
285
|
+
resolveThumbnailPathForEnumField(field, modelFilePath);
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
const configModel = configModels[modelName];
|
|
290
|
+
if (!configModel) {
|
|
291
|
+
return modelFromFile;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return _.assign({}, modelFromFile, configModel, {
|
|
295
|
+
fields: _.unionBy(configModel?.fields ?? [], modelFromFile?.fields ?? [], 'name')
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
return {
|
|
299
|
+
...config,
|
|
300
|
+
models: Object.assign({}, configModels, mergedModels)
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function mergeConfigWithExternalModels(config: any, externalModels?: Model[]) {
|
|
305
|
+
if (!externalModels || externalModels.length === 0) {
|
|
306
|
+
return config;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const stackbitModels = config?.models ?? {};
|
|
310
|
+
|
|
311
|
+
externalModels = externalModels.map((externalModel) => {
|
|
312
|
+
const stackbitModel = stackbitModels[externalModel.name];
|
|
313
|
+
if (!stackbitModel) {
|
|
314
|
+
return externalModel;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const modelType = stackbitModel.type ? (stackbitModel.type === 'config' ? 'data' : stackbitModel.type) : 'object';
|
|
318
|
+
const urlPath = modelType === 'page' ? stackbitModel?.urlPath ?? '/{slug}' : null;
|
|
319
|
+
const fieldGroups = stackbitModel?.fieldGroups;
|
|
320
|
+
|
|
321
|
+
externalModel = Object.assign(
|
|
322
|
+
externalModel,
|
|
323
|
+
omitByNil({
|
|
324
|
+
__metadata: stackbitModel.__metadata,
|
|
325
|
+
type: modelType,
|
|
326
|
+
urlPath,
|
|
327
|
+
fieldGroups
|
|
328
|
+
})
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
return mapModelFieldsRecursively(externalModel, (field, modelKeyPath) => {
|
|
332
|
+
const stackbitField = getModelFieldForModelKeyPath(stackbitModel, modelKeyPath);
|
|
333
|
+
if (!stackbitField) {
|
|
334
|
+
return field;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const group = 'group' in stackbitField ? stackbitField.group : null;
|
|
338
|
+
const controlType = 'controlType' in stackbitField ? stackbitField.controlType : null;
|
|
339
|
+
let override = {};
|
|
340
|
+
|
|
341
|
+
if (stackbitField?.type === 'style') {
|
|
342
|
+
override = stackbitField;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return Object.assign(
|
|
346
|
+
{},
|
|
347
|
+
field,
|
|
348
|
+
omitByNil({
|
|
349
|
+
group,
|
|
350
|
+
controlType
|
|
351
|
+
}),
|
|
352
|
+
override
|
|
353
|
+
);
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
return {
|
|
358
|
+
...config,
|
|
359
|
+
models: _.mapValues(_.keyBy(externalModels, 'name'), (model) => {
|
|
360
|
+
_.unset(model, 'name');
|
|
361
|
+
return model;
|
|
362
|
+
})
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
234
366
|
function normalizeConfig(config: any): any {
|
|
235
367
|
const pageLayoutKey = _.get(config, 'pageLayoutKey', 'layout');
|
|
236
368
|
const objectTypeKey = _.get(config, 'objectTypeKey', 'type');
|
|
@@ -238,13 +370,23 @@ function normalizeConfig(config: any): any {
|
|
|
238
370
|
const ver = semver.coerce(stackbitYamlVersion);
|
|
239
371
|
const isStackbitYamlV2 = ver ? semver.satisfies(ver, '<0.3.0') : false;
|
|
240
372
|
const models = config?.models || {};
|
|
373
|
+
const gitCMS = isGitCMS(config);
|
|
241
374
|
let referencedModelNames: string[] = [];
|
|
242
375
|
|
|
243
|
-
_.forEach(models, (model) => {
|
|
376
|
+
_.forEach(models, (model, modelName) => {
|
|
244
377
|
if (!model) {
|
|
245
378
|
return;
|
|
246
379
|
}
|
|
247
380
|
|
|
381
|
+
if (!_.has(model, 'type')) {
|
|
382
|
+
model.type = 'object';
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// add model label if not set
|
|
386
|
+
if (!_.has(model, 'label')) {
|
|
387
|
+
model.label = _.startCase(modelName);
|
|
388
|
+
}
|
|
389
|
+
|
|
248
390
|
if (_.has(model, 'fields') && !Array.isArray(model.fields)) {
|
|
249
391
|
model.fields = [];
|
|
250
392
|
}
|
|
@@ -254,13 +396,15 @@ function normalizeConfig(config: any): any {
|
|
|
254
396
|
rename(model, 'template', 'layout');
|
|
255
397
|
|
|
256
398
|
updatePageUrlPath(model);
|
|
257
|
-
updatePageFilePath(model, config);
|
|
258
399
|
|
|
259
|
-
|
|
400
|
+
if (gitCMS) {
|
|
401
|
+
updatePageFilePath(model, config);
|
|
402
|
+
addMarkdownContentField(model);
|
|
260
403
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
404
|
+
// TODO: update schema-editor to not show layout field
|
|
405
|
+
addLayoutFieldToPageModel(model, pageLayoutKey);
|
|
406
|
+
}
|
|
407
|
+
} else if (isDataModel(model) && gitCMS) {
|
|
264
408
|
updateDataFilePath(model, config);
|
|
265
409
|
}
|
|
266
410
|
|
|
@@ -276,8 +420,6 @@ function normalizeConfig(config: any): any {
|
|
|
276
420
|
assignLabelFieldIfNeeded(model);
|
|
277
421
|
}
|
|
278
422
|
|
|
279
|
-
resolveThumbnailPathForModel(model, model?.__metadata?.filePath);
|
|
280
|
-
|
|
281
423
|
iterateModelFieldsRecursively(model, (field: any) => {
|
|
282
424
|
// add field label if label is not set
|
|
283
425
|
if (!_.has(field, 'label')) {
|
|
@@ -285,18 +427,12 @@ function normalizeConfig(config: any): any {
|
|
|
285
427
|
}
|
|
286
428
|
|
|
287
429
|
if (isListField(field)) {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
_.set(field, 'items.type', 'string');
|
|
291
|
-
}
|
|
292
|
-
field = getListItemsField(field);
|
|
430
|
+
field = normalizeListFieldInPlace(field);
|
|
431
|
+
field = field.items;
|
|
293
432
|
}
|
|
294
433
|
|
|
295
434
|
if (isObjectField(field)) {
|
|
296
435
|
assignLabelFieldIfNeeded(field);
|
|
297
|
-
resolveThumbnailPathForModel(field, model?.__metadata?.filePath);
|
|
298
|
-
} else if (isEnumField(field)) {
|
|
299
|
-
resolveThumbnailPathForEnumField(field, model?.__metadata?.filePath);
|
|
300
436
|
} else if (isCustomModelField(field, models)) {
|
|
301
437
|
// stackbit v0.2.0 compatibility
|
|
302
438
|
// convert the old custom model field type: { type: 'action' }
|
|
@@ -326,7 +462,9 @@ function normalizeConfig(config: any): any {
|
|
|
326
462
|
}
|
|
327
463
|
}
|
|
328
464
|
|
|
329
|
-
|
|
465
|
+
if (gitCMS) {
|
|
466
|
+
referencedModelNames = _.union(referencedModelNames, getReferencedModelNames(field));
|
|
467
|
+
}
|
|
330
468
|
});
|
|
331
469
|
});
|
|
332
470
|
|
|
@@ -461,6 +599,9 @@ function resolveThumbnailPathForEnumField(enumField: FieldEnum, modelFilePath: s
|
|
|
461
599
|
}
|
|
462
600
|
|
|
463
601
|
function resolveThumbnailPath(thumbnail: string, modelDirPath: string) {
|
|
602
|
+
if (thumbnail.startsWith('//') || /https?:\/\//.test(thumbnail)) {
|
|
603
|
+
return thumbnail;
|
|
604
|
+
}
|
|
464
605
|
if (thumbnail.startsWith('/')) {
|
|
465
606
|
if (modelDirPath.endsWith('@stackbit/components/models')) {
|
|
466
607
|
modelDirPath = modelDirPath.replace(/\/models$/, '');
|
|
@@ -480,7 +621,7 @@ function resolveThumbnailPath(thumbnail: string, modelDirPath: string) {
|
|
|
480
621
|
*/
|
|
481
622
|
function getReferencedModelNames(field: any) {
|
|
482
623
|
if (isListField(field)) {
|
|
483
|
-
field =
|
|
624
|
+
field = getListFieldItems(field);
|
|
484
625
|
}
|
|
485
626
|
// TODO: add type field to model fields inside container update/create object logic rather adding type to schema
|
|
486
627
|
// 'object' models referenced by 'model' fields should have 'type' field
|
|
@@ -501,7 +642,10 @@ function validateAndExtendContentModels(config: any): ConfigValidationResult {
|
|
|
501
642
|
const contentModels = config.contentModels ?? {};
|
|
502
643
|
const models = config.models ?? {};
|
|
503
644
|
|
|
504
|
-
|
|
645
|
+
const externalModels = !isGitCMS(config);
|
|
646
|
+
const emptyContentModels = _.isEmpty(contentModels);
|
|
647
|
+
|
|
648
|
+
if (externalModels || emptyContentModels) {
|
|
505
649
|
return {
|
|
506
650
|
valid: true,
|
|
507
651
|
value: config,
|
|
@@ -512,17 +656,17 @@ function validateAndExtendContentModels(config: any): ConfigValidationResult {
|
|
|
512
656
|
const validationResult = validateContentModels(contentModels, models);
|
|
513
657
|
|
|
514
658
|
if (_.isEmpty(models)) {
|
|
515
|
-
return
|
|
659
|
+
return {
|
|
660
|
+
valid: validationResult.valid,
|
|
661
|
+
value: config,
|
|
662
|
+
errors: validationResult.errors
|
|
663
|
+
};
|
|
516
664
|
}
|
|
517
665
|
|
|
518
666
|
const extendedModels = _.mapValues(models, (model, modelName) => {
|
|
519
667
|
const contentModel = validationResult.value.contentModels[modelName];
|
|
520
668
|
if (!contentModel) {
|
|
521
|
-
return
|
|
522
|
-
// if a model does not define a type, use the default "object" type
|
|
523
|
-
type: model.type || 'object',
|
|
524
|
-
..._.omit(model, 'type')
|
|
525
|
-
};
|
|
669
|
+
return model;
|
|
526
670
|
}
|
|
527
671
|
if (_.get(contentModel, '__metadata.invalid')) {
|
|
528
672
|
return model;
|
|
@@ -557,10 +701,43 @@ function validateAndExtendContentModels(config: any): ConfigValidationResult {
|
|
|
557
701
|
}
|
|
558
702
|
|
|
559
703
|
function normalizeValidationResult(validationResult: ConfigValidationResult): NormalizedValidationResult {
|
|
704
|
+
validationResult = filterAndOrderConfigFields(validationResult);
|
|
560
705
|
convertModelGroupsToModelList(validationResult);
|
|
561
706
|
return convertModelsToArray(validationResult);
|
|
562
707
|
}
|
|
563
708
|
|
|
709
|
+
function filterAndOrderConfigFields(validationResult: ConfigValidationResult): ConfigValidationResult {
|
|
710
|
+
// TODO: see if we move filtering and sorting to Joi
|
|
711
|
+
return {
|
|
712
|
+
...validationResult,
|
|
713
|
+
value: _.pick(validationResult.value, [
|
|
714
|
+
'stackbitVersion',
|
|
715
|
+
'ssgName',
|
|
716
|
+
'ssgVersion',
|
|
717
|
+
'cmsName',
|
|
718
|
+
'import',
|
|
719
|
+
'buildCommand',
|
|
720
|
+
'publishDir',
|
|
721
|
+
'nodeVersion',
|
|
722
|
+
'devCommand',
|
|
723
|
+
'staticDir',
|
|
724
|
+
'uploadDir',
|
|
725
|
+
'assets',
|
|
726
|
+
'pagesDir',
|
|
727
|
+
'dataDir',
|
|
728
|
+
'pageLayoutKey',
|
|
729
|
+
'objectTypeKey',
|
|
730
|
+
'styleObjectModelName',
|
|
731
|
+
'excludePages',
|
|
732
|
+
'logicFields',
|
|
733
|
+
'contentModels',
|
|
734
|
+
'modelsSource',
|
|
735
|
+
'models',
|
|
736
|
+
'presets'
|
|
737
|
+
])
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
|
|
564
741
|
function convertModelGroupsToModelList(validationResult: ConfigValidationResult) {
|
|
565
742
|
const models = validationResult.value?.models ?? {};
|
|
566
743
|
|
|
@@ -590,7 +767,7 @@ function convertModelGroupsToModelList(validationResult: ConfigValidationResult)
|
|
|
590
767
|
_.forEach(models, (model) => {
|
|
591
768
|
iterateModelFieldsRecursively(model, (field: any) => {
|
|
592
769
|
if (isListField(field)) {
|
|
593
|
-
field =
|
|
770
|
+
field = field.items;
|
|
594
771
|
}
|
|
595
772
|
if (field.groups) {
|
|
596
773
|
let key: string | null = null;
|
|
@@ -632,6 +809,10 @@ function convertModelsToArray(validationResult: ConfigValidationResult): Normali
|
|
|
632
809
|
}
|
|
633
810
|
);
|
|
634
811
|
|
|
812
|
+
if (!isGitCMS(config)) {
|
|
813
|
+
addImageModel(modelArray);
|
|
814
|
+
}
|
|
815
|
+
|
|
635
816
|
const convertedErrors = _.map(validationResult.errors, (error: ConfigValidationError) => {
|
|
636
817
|
if (error.fieldPath[0] === 'models' && typeof error.fieldPath[1] == 'string') {
|
|
637
818
|
const modelName = error.fieldPath[1];
|
|
@@ -652,3 +833,20 @@ function convertModelsToArray(validationResult: ConfigValidationResult): Normali
|
|
|
652
833
|
errors: convertedErrors
|
|
653
834
|
};
|
|
654
835
|
}
|
|
836
|
+
|
|
837
|
+
function addImageModel(models: Model[]) {
|
|
838
|
+
models.push({
|
|
839
|
+
type: 'image',
|
|
840
|
+
name: '__image_model',
|
|
841
|
+
label: 'Image',
|
|
842
|
+
labelField: 'title',
|
|
843
|
+
fields: [
|
|
844
|
+
{ name: 'title', type: 'string' },
|
|
845
|
+
{ name: 'url', type: 'string' }
|
|
846
|
+
]
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function isGitCMS(config: any) {
|
|
851
|
+
return !config.cmsName || config.cmsName === 'git';
|
|
852
|
+
}
|
|
@@ -20,7 +20,10 @@ import {
|
|
|
20
20
|
FieldGroupItem,
|
|
21
21
|
YamlObjectModel,
|
|
22
22
|
ContentModelMap,
|
|
23
|
-
ContentModel
|
|
23
|
+
ContentModel,
|
|
24
|
+
ModelsSourceFiles,
|
|
25
|
+
ModelsSourceContentful,
|
|
26
|
+
ModelsSourceSanity
|
|
24
27
|
} from './config-types';
|
|
25
28
|
|
|
26
29
|
function getConfigFromValidationState(state: Joi.State): YamlConfig {
|
|
@@ -200,6 +203,11 @@ const styleObjectModelNotObject = 'styleObjectModelName.model.type';
|
|
|
200
203
|
const styleObjectModelNameSchema = Joi.string()
|
|
201
204
|
.allow('', null)
|
|
202
205
|
.custom((value, { error, state }) => {
|
|
206
|
+
const config = getConfigFromValidationState(state);
|
|
207
|
+
const externalModels = config.cmsName && ['contentful', 'sanity'].includes(config.cmsName);
|
|
208
|
+
if (externalModels) {
|
|
209
|
+
return value;
|
|
210
|
+
}
|
|
203
211
|
const models = getModelsFromValidationState(state);
|
|
204
212
|
const modelNames = Object.keys(models);
|
|
205
213
|
if (!modelNames.includes(value)) {
|
|
@@ -245,11 +253,32 @@ const importSchema = Joi.alternatives().conditional('.type', {
|
|
|
245
253
|
]
|
|
246
254
|
});
|
|
247
255
|
|
|
248
|
-
const
|
|
249
|
-
type: 'files',
|
|
256
|
+
const modelsSourceFilesSchema = Joi.object<ModelsSourceFiles>({
|
|
257
|
+
type: Joi.string().valid(Joi.override, 'files').required(),
|
|
250
258
|
modelDirs: Joi.array().items(Joi.string()).required()
|
|
251
259
|
});
|
|
252
260
|
|
|
261
|
+
const modelsSourceContentfulSchema = Joi.object<ModelsSourceContentful>({
|
|
262
|
+
type: Joi.string().valid(Joi.override, 'contentful').required(),
|
|
263
|
+
module: Joi.string()
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
const modelsSourceSanitySchema = Joi.object<ModelsSourceSanity>({
|
|
267
|
+
type: Joi.string().valid(Joi.override, 'sanity').required(),
|
|
268
|
+
sanityStudioPath: Joi.string().required(),
|
|
269
|
+
module: Joi.string()
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
const modelsSourceSchema = Joi.object<ModelsSource>({
|
|
273
|
+
type: Joi.string().valid('files', 'contentful', 'sanity').required()
|
|
274
|
+
}).when('.type', {
|
|
275
|
+
switch: [
|
|
276
|
+
{ is: 'files', then: modelsSourceFilesSchema },
|
|
277
|
+
{ is: 'contentful', then: modelsSourceContentfulSchema },
|
|
278
|
+
{ is: 'sanity', then: modelsSourceSanitySchema }
|
|
279
|
+
]
|
|
280
|
+
});
|
|
281
|
+
|
|
253
282
|
const assetsSchema = Joi.object<Assets>({
|
|
254
283
|
referenceType: Joi.string().valid('static', 'relative').required(),
|
|
255
284
|
assetsDir: Joi.string().allow('').when('referenceType', {
|
|
@@ -309,7 +338,8 @@ const fieldCommonPropsSchema = Joi.object({
|
|
|
309
338
|
group: inGroups,
|
|
310
339
|
const: Joi.any(),
|
|
311
340
|
hidden: Joi.boolean(),
|
|
312
|
-
readOnly: Joi.boolean()
|
|
341
|
+
readOnly: Joi.boolean(),
|
|
342
|
+
localized: Joi.boolean()
|
|
313
343
|
}).oxor('const', 'default');
|
|
314
344
|
|
|
315
345
|
const numberFieldPartialSchema = Joi.object({
|
|
@@ -485,10 +515,7 @@ const baseModelSchema = Joi.object<YamlBaseModel>({
|
|
|
485
515
|
filePath: Joi.string()
|
|
486
516
|
}),
|
|
487
517
|
type: Joi.string().valid('page', 'data', 'config', 'object').required(),
|
|
488
|
-
label: Joi.string()
|
|
489
|
-
is: Joi.exist(),
|
|
490
|
-
then: Joi.optional()
|
|
491
|
-
}),
|
|
518
|
+
label: Joi.string(),
|
|
492
519
|
description: Joi.string(),
|
|
493
520
|
thumbnail: Joi.string(),
|
|
494
521
|
extends: Joi.array().items(validObjectModelNames).single(),
|