@stackbit/cms-core 0.1.3 → 0.1.4-alpha.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 (57) hide show
  1. package/dist/common/common-types.d.ts +1 -9
  2. package/dist/common/common-types.d.ts.map +1 -1
  3. package/dist/consts.d.ts +1 -24
  4. package/dist/consts.d.ts.map +1 -1
  5. package/dist/consts.js +5 -25
  6. package/dist/consts.js.map +1 -1
  7. package/dist/content-store-types.d.ts +42 -39
  8. package/dist/content-store-types.d.ts.map +1 -1
  9. package/dist/content-store-utils.d.ts +10 -0
  10. package/dist/content-store-utils.d.ts.map +1 -0
  11. package/dist/content-store-utils.js +139 -0
  12. package/dist/content-store-utils.js.map +1 -0
  13. package/dist/content-store.d.ts +18 -5
  14. package/dist/content-store.d.ts.map +1 -1
  15. package/dist/content-store.js +177 -962
  16. package/dist/content-store.js.map +1 -1
  17. package/dist/index.d.ts +2 -1
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +3 -2
  20. package/dist/index.js.map +1 -1
  21. package/dist/types/search-filter.d.ts +42 -0
  22. package/dist/types/search-filter.d.ts.map +1 -0
  23. package/dist/types/search-filter.js +3 -0
  24. package/dist/types/search-filter.js.map +1 -0
  25. package/dist/utils/create-update-csi-docs.d.ts +69 -0
  26. package/dist/utils/create-update-csi-docs.d.ts.map +1 -0
  27. package/dist/utils/create-update-csi-docs.js +386 -0
  28. package/dist/utils/create-update-csi-docs.js.map +1 -0
  29. package/dist/utils/csi-to-store-docs-converter.d.ts +15 -0
  30. package/dist/utils/csi-to-store-docs-converter.d.ts.map +1 -0
  31. package/dist/utils/csi-to-store-docs-converter.js +287 -0
  32. package/dist/utils/csi-to-store-docs-converter.js.map +1 -0
  33. package/dist/utils/search-utils.d.ts +21 -0
  34. package/dist/utils/search-utils.d.ts.map +1 -0
  35. package/dist/utils/search-utils.js +323 -0
  36. package/dist/utils/search-utils.js.map +1 -0
  37. package/dist/utils/store-to-api-docs-converter.d.ts +5 -0
  38. package/dist/utils/store-to-api-docs-converter.d.ts.map +1 -0
  39. package/dist/utils/store-to-api-docs-converter.js +247 -0
  40. package/dist/utils/store-to-api-docs-converter.js.map +1 -0
  41. package/package.json +7 -5
  42. package/src/common/common-types.ts +1 -10
  43. package/src/consts.ts +1 -26
  44. package/src/content-store-types.ts +59 -45
  45. package/src/content-store-utils.ts +150 -0
  46. package/src/content-store.ts +168 -1090
  47. package/src/index.ts +3 -2
  48. package/src/types/search-filter.ts +53 -0
  49. package/src/utils/create-update-csi-docs.ts +457 -0
  50. package/src/utils/csi-to-store-docs-converter.ts +366 -0
  51. package/src/utils/search-utils.ts +437 -0
  52. package/src/utils/store-to-api-docs-converter.ts +246 -0
  53. package/dist/content-source-interface.d.ts +0 -338
  54. package/dist/content-source-interface.d.ts.map +0 -1
  55. package/dist/content-source-interface.js +0 -28
  56. package/dist/content-source-interface.js.map +0 -1
  57. package/src/content-source-interface.ts +0 -495
package/src/index.ts CHANGED
@@ -1,11 +1,12 @@
1
1
  export * as stackbit from './stackbit';
2
2
  export * as annotator from './annotator';
3
3
  export * as utils from './utils';
4
+ export * as searchUtils from './utils/search-utils';
4
5
  export * as consts from './consts';
5
6
  export * from './common/common-schema';
6
7
  export * from './common/common-types';
7
8
  export * from './content-store';
8
- export * as ContentSourceTypes from './content-source-interface';
9
9
  export * as ContentStoreTypes from './content-store-types';
10
10
  export { default as encodeData } from './encoder';
11
- export * from './encoder';
11
+ export * from './encoder';
12
+ export * from './types/search-filter';
@@ -0,0 +1,53 @@
1
+ type Distribute<U> = U extends any ? U[] : never;
2
+
3
+ type ValueType = string | number | Date | boolean;
4
+
5
+ type BaseFilterItem = {
6
+ field: string;
7
+ isMeta?: boolean;
8
+ };
9
+
10
+ type EqualFilterItem = BaseFilterItem & {
11
+ operator: 'eq' | 'neq';
12
+ value: ValueType;
13
+ };
14
+
15
+ type UndefinedFilterItem = BaseFilterItem & {
16
+ operator: 'is-undefined' | 'is-not-undefined';
17
+ }
18
+
19
+ type CompareFilterItem = BaseFilterItem & {
20
+ operator: 'gte' | 'lte';
21
+ value: ValueType;
22
+ };
23
+
24
+ type IncludeStringFilterItem = BaseFilterItem & {
25
+ operator: 'includes' | 'not-includes';
26
+ value: string;
27
+ };
28
+
29
+ type IncludeListFilterItem = BaseFilterItem & {
30
+ operator: 'in' | 'nin';
31
+ values: Distribute<ValueType>;
32
+ };
33
+
34
+ type IncludeAllFilterItem = BaseFilterItem & {
35
+ operator: 'all';
36
+ values: Distribute<ValueType>;
37
+ };
38
+
39
+ type BetweenFilterItem = BaseFilterItem & {
40
+ operator: 'between';
41
+ startValue: ValueType;
42
+ endValue: ValueType;
43
+ };
44
+
45
+ export type SearchFilterItem = EqualFilterItem | UndefinedFilterItem | CompareFilterItem | IncludeStringFilterItem | IncludeListFilterItem | IncludeAllFilterItem | BetweenFilterItem;
46
+
47
+ export type SearchFilter = LogicalOperator;
48
+
49
+ type LogicalOperator = LogicalAndOperator;
50
+
51
+ type LogicalAndOperator = {
52
+ 'and': SearchFilterItem[];
53
+ };
@@ -0,0 +1,457 @@
1
+ import _ from 'lodash';
2
+ import slugify from 'slugify';
3
+
4
+ import { Model } from '@stackbit/sdk';
5
+ import { Model as CSIModel, Field, FieldObjectProps, FieldSpecificProps, UpdateOperationListFieldItem } from '@stackbit/types';
6
+ import { mapPromise } from '@stackbit/utils';
7
+ import * as CSITypes from '@stackbit/types';
8
+
9
+ import * as ContentStoreTypes from '../content-store-types';
10
+
11
+ export type CreateDocumentCallback = ({
12
+ updateOperationFields,
13
+ modelName
14
+ }: {
15
+ updateOperationFields: Record<string, CSITypes.UpdateOperationField>;
16
+ modelName: string;
17
+ }) => Promise<CSITypes.Document>;
18
+
19
+ export function getCreateDocumentThunk({
20
+ csiModelMap,
21
+ locale,
22
+ userContext,
23
+ contentSourceInstance
24
+ }: {
25
+ csiModelMap: Record<string, CSIModel>;
26
+ locale?: string;
27
+ userContext: unknown;
28
+ contentSourceInstance: CSITypes.ContentSourceInterface;
29
+ }): CreateDocumentCallback {
30
+ return async ({ updateOperationFields, modelName }) => {
31
+ // When passing model and modelMap to contentSourceInstance, we have to pass
32
+ // the original models (i.e., csiModel and csiModelMap) that we've received
33
+ // from that contentSourceInstance. We can't pass internal models as they
34
+ // might
35
+ const csiModel = csiModelMap[modelName];
36
+ if (!csiModel) {
37
+ throw new Error(`no model with name '${modelName}' was found`);
38
+ }
39
+ return await contentSourceInstance.createDocument({
40
+ updateOperationFields: updateOperationFields,
41
+ model: csiModel,
42
+ modelMap: csiModelMap,
43
+ locale,
44
+ userContext
45
+ });
46
+ };
47
+ }
48
+
49
+ /**
50
+ * Receives a plain `object`, and creates a map of `CSITypes.UpdateOperationField`
51
+ * by recursively iterating the `object` fields. Then invokes the `createDocument`
52
+ * callback to delegate the creation if CSI document.
53
+ *
54
+ * If the `object` has fields of type `reference` or `model` with the special
55
+ * `$$type` property holding the model name of the nested object, this function
56
+ * will recursively create new documents and nested objects for these fields.
57
+ * Other fields of will be used to populate the fields of the new document or
58
+ * nested object.
59
+ *
60
+ * @example
61
+ * {
62
+ * title: 'hello world',
63
+ * button: {
64
+ * $$type: 'Button', // a new nested object of type Button will be
65
+ * label: 'Click me' // created with 'label' field set to 'Click me'
66
+ * }
67
+ * }
68
+ *
69
+ * If the `object` has fields of type `reference` or `image` with the special
70
+ * `$$ref` property holding an ID of an existing document, this function will
71
+ * link an existing documents or assets with that ID.
72
+ *
73
+ * @example
74
+ * {
75
+ * title: 'hello world',
76
+ * author: {
77
+ * $$ref: 'xyz' // the 'author' field will be linked to a document
78
+ * // with ID 'xyz'
79
+ * }
80
+ * }
81
+ *
82
+ * Returns an object with two fields:
83
+ * 1. `document` holding the created CSITypes.Document for the passed object.
84
+ * 2. `newRefDocuments` holding list of created CSITypes.Document that were
85
+ * created recursively for `reference` fields having the `$$type` property.
86
+ */
87
+ export async function createDocumentRecursively({
88
+ object,
89
+ modelName,
90
+ modelMap,
91
+ createDocument
92
+ }: {
93
+ object?: Record<string, any>;
94
+ modelName: string;
95
+ modelMap: Record<string, Model>;
96
+ createDocument: CreateDocumentCallback;
97
+ }): Promise<{ document: CSITypes.Document; newRefDocuments: CSITypes.Document[] }> {
98
+ const model = modelMap[modelName];
99
+ if (!model) {
100
+ throw new Error(`no model with name '${modelName}' was found`);
101
+ }
102
+ if (model.type === 'page') {
103
+ const tokens = extractTokensFromString(String(model.urlPath));
104
+ const slugField = _.last(tokens);
105
+ if (object && slugField && slugField in object) {
106
+ const slugFieldValue = object[slugField];
107
+ object[slugField] = sanitizeSlug(slugFieldValue);
108
+ }
109
+ }
110
+
111
+ const nestedResult = await createNestedObjectRecursively({
112
+ object,
113
+ modelFields: model.fields ?? [],
114
+ fieldPath: [],
115
+ modelMap,
116
+ createDocument
117
+ });
118
+
119
+ const document = await createDocument({
120
+ updateOperationFields: nestedResult.fields,
121
+ modelName: modelName
122
+ });
123
+ return {
124
+ document: document,
125
+ newRefDocuments: nestedResult.newRefDocuments
126
+ };
127
+ }
128
+
129
+ function extractTokensFromString(input: string): string[] {
130
+ return input.match(/(?<={)[^}]+(?=})/g) || [];
131
+ }
132
+
133
+ function sanitizeSlug(slug: string) {
134
+ return slug
135
+ .split('/')
136
+ .map((part) => slugify(part, { lower: true }))
137
+ .join('/');
138
+ }
139
+
140
+ async function createNestedObjectRecursively({
141
+ object,
142
+ modelFields,
143
+ fieldPath,
144
+ modelMap,
145
+ createDocument
146
+ }: {
147
+ object?: Record<string, any>;
148
+ modelFields: Field[];
149
+ fieldPath: (string | number)[];
150
+ modelMap: Record<string, Model>;
151
+ createDocument: CreateDocumentCallback;
152
+ }): Promise<{
153
+ fields: Record<string, CSITypes.UpdateOperationField>;
154
+ newRefDocuments: CSITypes.Document[];
155
+ }> {
156
+ object = object ?? {};
157
+ const result: {
158
+ fields: Record<string, CSITypes.UpdateOperationField>;
159
+ newRefDocuments: CSITypes.Document[];
160
+ } = {
161
+ fields: {},
162
+ newRefDocuments: []
163
+ };
164
+ const objectFieldNames = Object.keys(object);
165
+ for (const modelField of modelFields) {
166
+ const fieldName = modelField.name;
167
+ let value;
168
+ if (fieldName in object) {
169
+ value = object[fieldName];
170
+ _.pull(objectFieldNames, fieldName);
171
+ } else if (modelField.const) {
172
+ value = modelField.const;
173
+ } else if (!_.isNil(modelField.default)) {
174
+ value = modelField.default;
175
+ }
176
+ if (!_.isNil(value)) {
177
+ const fieldResult = await createNestedField({
178
+ value,
179
+ modelField,
180
+ fieldPath: fieldPath.concat(fieldName),
181
+ modelMap,
182
+ createDocument
183
+ });
184
+ result.fields[fieldName] = fieldResult.field;
185
+ result.newRefDocuments = result.newRefDocuments.concat(fieldResult.newRefDocuments);
186
+ }
187
+ }
188
+ if (objectFieldNames.length > 0) {
189
+ throw new Error(`no model fields found when creating a document with fields: '${objectFieldNames.join(', ')}'`);
190
+ }
191
+
192
+ return result;
193
+ }
194
+
195
+ async function createNestedField({
196
+ value,
197
+ modelField,
198
+ fieldPath,
199
+ modelMap,
200
+ createDocument
201
+ }: {
202
+ value: any;
203
+ modelField: FieldSpecificProps;
204
+ fieldPath: (string | number)[];
205
+ modelMap: Record<string, Model>;
206
+ createDocument: CreateDocumentCallback;
207
+ }): Promise<{ field: CSITypes.UpdateOperationField; newRefDocuments: CSITypes.Document[] }> {
208
+ if (modelField.type === 'object') {
209
+ const result = await createNestedObjectRecursively({
210
+ object: value,
211
+ modelFields: modelField.fields,
212
+ fieldPath,
213
+ modelMap,
214
+ createDocument
215
+ });
216
+ return {
217
+ field: {
218
+ type: 'object',
219
+ fields: result.fields
220
+ },
221
+ newRefDocuments: result.newRefDocuments
222
+ };
223
+ } else if (modelField.type === 'model') {
224
+ let { $$type, ...rest } = value;
225
+ const modelNames = modelField.models;
226
+ // for backward compatibility check if the object has 'type' instead of '$$type' because older projects use
227
+ // the 'type' property in default values
228
+ if (!$$type && 'type' in rest) {
229
+ $$type = rest.type;
230
+ rest = _.omit(rest, 'type');
231
+ }
232
+ const modelName = $$type ?? (modelNames.length === 1 ? modelNames[0] : null);
233
+ if (!modelName) {
234
+ throw new Error(`no $$type was specified for nested model`);
235
+ }
236
+ const model = modelMap[modelName];
237
+ if (!model) {
238
+ throw new Error(`no model with name '${modelName}' was found`);
239
+ }
240
+ const result = await createNestedObjectRecursively({
241
+ object: rest,
242
+ modelFields: model.fields ?? [],
243
+ fieldPath,
244
+ modelMap,
245
+ createDocument
246
+ });
247
+ return {
248
+ field: {
249
+ type: 'model',
250
+ modelName: modelName,
251
+ fields: result.fields
252
+ },
253
+ newRefDocuments: result.newRefDocuments
254
+ };
255
+ } else if (modelField.type === 'image') {
256
+ let refId: string | undefined;
257
+ if (_.isPlainObject(value)) {
258
+ refId = value.$$ref;
259
+ } else {
260
+ refId = value;
261
+ }
262
+ if (!refId) {
263
+ throw new Error(`reference field must specify a value`);
264
+ }
265
+ return {
266
+ field: {
267
+ type: 'reference',
268
+ refType: 'asset',
269
+ refId: refId
270
+ },
271
+ newRefDocuments: []
272
+ };
273
+ } else if (modelField.type === 'reference') {
274
+ let { $$ref: refId = null, $$type: modelName = null, ...rest } = _.isPlainObject(value) ? value : { $$ref: value };
275
+ if (refId) {
276
+ return {
277
+ field: {
278
+ type: 'reference',
279
+ refType: 'document',
280
+ refId: refId
281
+ },
282
+ newRefDocuments: []
283
+ };
284
+ } else {
285
+ const modelNames = modelField.models;
286
+ if (!modelName) {
287
+ // for backward compatibility check if the object has 'type' instead of '$$type' because older projects use
288
+ // the 'type' property in default values
289
+ if ('type' in rest) {
290
+ modelName = rest.type;
291
+ rest = _.omit(rest, 'type');
292
+ } else if (modelNames.length === 1) {
293
+ modelName = modelNames[0];
294
+ }
295
+ }
296
+ const { document, newRefDocuments } = await createDocumentRecursively({
297
+ object: rest,
298
+ modelName,
299
+ modelMap,
300
+ createDocument
301
+ });
302
+ return {
303
+ field: {
304
+ type: 'reference',
305
+ refType: 'document',
306
+ refId: document.id
307
+ },
308
+ newRefDocuments: [document, ...newRefDocuments]
309
+ };
310
+ }
311
+ } else if (modelField.type === 'list') {
312
+ if (!Array.isArray(value)) {
313
+ throw new Error(`value for list field must be array`);
314
+ }
315
+ const itemsField = modelField.items;
316
+ if (!itemsField) {
317
+ throw new Error(`list field does not define items`);
318
+ }
319
+ const arrayResult = await mapPromise(
320
+ value,
321
+ async (item, index): Promise<{ field: UpdateOperationListFieldItem; newRefDocuments: CSITypes.Document[] }> => {
322
+ const result = await createNestedField({
323
+ value: item,
324
+ modelField: itemsField,
325
+ fieldPath: fieldPath.concat(index),
326
+ modelMap,
327
+ createDocument
328
+ });
329
+ if (result.field.type === 'list') {
330
+ throw new Error('list cannot have list as immediate child');
331
+ }
332
+ return {
333
+ field: result.field,
334
+ newRefDocuments: result.newRefDocuments
335
+ };
336
+ }
337
+ );
338
+ return {
339
+ field: {
340
+ type: 'list',
341
+ items: arrayResult.map((result) => result.field)
342
+ },
343
+ newRefDocuments: arrayResult.reduce((result: CSITypes.Document[], { newRefDocuments }) => result.concat(newRefDocuments), [])
344
+ };
345
+ }
346
+ return {
347
+ field: {
348
+ type: modelField.type,
349
+ value: value
350
+ },
351
+ newRefDocuments: []
352
+ };
353
+ }
354
+
355
+ export async function convertOperationField({
356
+ operationField,
357
+ fieldPath,
358
+ modelField,
359
+ modelMap,
360
+ createDocument
361
+ }: {
362
+ operationField: ContentStoreTypes.UpdateOperationField;
363
+ fieldPath: (string | number)[];
364
+ modelField: Field;
365
+ modelMap: Record<string, Model>;
366
+ createDocument: CreateDocumentCallback;
367
+ }): Promise<CSITypes.UpdateOperationField> {
368
+ // for insert operations, the modelField will be of the list, so get the modelField of the list items
369
+ const modelFieldOrListItems: FieldSpecificProps = modelField.type === 'list' ? modelField.items! : modelField;
370
+ switch (operationField.type) {
371
+ case 'object': {
372
+ const result = await createNestedObjectRecursively({
373
+ object: operationField.object,
374
+ modelFields: (modelFieldOrListItems as FieldObjectProps).fields,
375
+ fieldPath: fieldPath,
376
+ modelMap,
377
+ createDocument
378
+ });
379
+ return {
380
+ type: operationField.type,
381
+ fields: result.fields
382
+ };
383
+ }
384
+ case 'model': {
385
+ const model = modelMap[operationField.modelName];
386
+ if (!model) {
387
+ throw new Error(`error updating document, could not find document model: '${operationField.modelName}'`);
388
+ }
389
+ const result = await createNestedObjectRecursively({
390
+ object: operationField.object,
391
+ modelFields: model.fields!,
392
+ fieldPath,
393
+ modelMap,
394
+ createDocument
395
+ });
396
+ return {
397
+ type: operationField.type,
398
+ modelName: operationField.modelName,
399
+ fields: result.fields
400
+ };
401
+ }
402
+ case 'list': {
403
+ if (modelField.type !== 'list') {
404
+ throw new Error(`'the operation field type '${operationField.type}' does not match the model field type '${modelField.type}'`);
405
+ }
406
+ const result: UpdateOperationListFieldItem[] = await mapPromise(
407
+ operationField.items,
408
+ async (item): Promise<UpdateOperationListFieldItem> => {
409
+ const result = await createNestedField({
410
+ value: item,
411
+ modelField: modelField.items!,
412
+ fieldPath,
413
+ modelMap,
414
+ createDocument
415
+ });
416
+ if (result.field.type === 'list') {
417
+ throw new Error('list cannot have list as immediate child');
418
+ }
419
+ return result.field;
420
+ }
421
+ );
422
+ return {
423
+ type: operationField.type,
424
+ items: result
425
+ };
426
+ }
427
+ case 'string':
428
+ // When inserting new string value into a list, the client does not
429
+ // send value. Set an empty string value.
430
+ if (typeof operationField.value !== 'string') {
431
+ return {
432
+ type: operationField.type,
433
+ value: ''
434
+ };
435
+ }
436
+ return operationField as CSITypes.UpdateOperationField;
437
+ case 'enum':
438
+ // When inserting new enum value into a list, the client does not
439
+ // send value. Set first option as the value.
440
+ if (typeof operationField.value !== 'string') {
441
+ if (modelFieldOrListItems.type !== 'enum') {
442
+ throw new Error(`'the operation field type 'enum' does not match the model field type '${modelFieldOrListItems.type}'`);
443
+ }
444
+ const option = modelFieldOrListItems.options[0]!;
445
+ const optionValue = typeof option === 'object' ? option.value : option;
446
+ return {
447
+ type: operationField.type,
448
+ value: optionValue
449
+ };
450
+ }
451
+ return operationField as CSITypes.UpdateOperationField;
452
+ case 'image':
453
+ return operationField as CSITypes.UpdateOperationField;
454
+ default:
455
+ return operationField as CSITypes.UpdateOperationField;
456
+ }
457
+ }