From 0eb806eb328f319670b657344b6bc21119c4af87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Zori=C4=87?= Date: Tue, 19 Nov 2024 14:32:21 +0100 Subject: [PATCH] fix(api-headless-cms): add missing date field storage transform --- .../contentAPI/dynamicZoneField.test.ts | 74 ++++++-- .../mocks/pageWithDynamicZonesModel.ts | 168 ++++++++++++++++++ .../__tests__/plugins/storage/date.test.ts | 150 ++++++++++++++++ .../__tests__/plugins/storage/dynamicZone.ts | 167 +++++++++++++++++ .../testHelpers/usePageManageHandler.ts | 12 ++ .../testHelpers/usePageReadHandler.ts | 12 ++ .../CmsModelDefaultFieldConverterPlugin.ts | 5 +- ...CmsModelDynamicZoneFieldConverterPlugin.ts | 9 +- .../CmsModelObjectFieldConverterPlugin.ts | 13 +- .../src/fieldConverters/index.ts | 3 + .../plugins/CmsModelFieldConverterPlugin.ts | 3 + .../src/plugins/StorageTransformPlugin.ts | 3 + .../src}/storage/date.ts | 14 +- .../src/storage/dynamicZone.ts | 91 ++++++++++ .../api-headless-cms/src/storage/index.ts | 9 +- .../api-headless-cms/src/storage/object.ts | 9 +- .../src/utils/entryStorage.ts | 8 +- 17 files changed, 713 insertions(+), 37 deletions(-) create mode 100644 packages/api-headless-cms/__tests__/plugins/storage/date.test.ts create mode 100644 packages/api-headless-cms/__tests__/plugins/storage/dynamicZone.ts rename packages/{api-headless-cms-ddb/src/dynamoDb => api-headless-cms/src}/storage/date.ts (82%) create mode 100644 packages/api-headless-cms/src/storage/dynamicZone.ts diff --git a/packages/api-headless-cms/__tests__/contentAPI/dynamicZoneField.test.ts b/packages/api-headless-cms/__tests__/contentAPI/dynamicZoneField.test.ts index 8d021058cd8..b1be97fd09d 100644 --- a/packages/api-headless-cms/__tests__/contentAPI/dynamicZoneField.test.ts +++ b/packages/api-headless-cms/__tests__/contentAPI/dynamicZoneField.test.ts @@ -24,10 +24,18 @@ const contentEntryQueryData = { }, { title: "Hero Title #1", + date: "2021-01-01", + time: "12:00:00", + dateTimeWithTimezone: "2021-01-01T12:00:00+01:00", + dateTimeWithoutTimezone: "2021-01-01T12:00:00.000Z", __typename: `${singularPageApiName}_Content_Hero` }, { title: "Hero Title #2", + date: "2021-02-05", + time: "14:00:00", + dateTimeWithTimezone: "2021-02-05T12:00:00+01:00", + dateTimeWithoutTimezone: "2021-02-05T12:00:00.000Z", __typename: `${singularPageApiName}_Content_Hero` }, { @@ -36,10 +44,18 @@ const contentEntryQueryData = { __typename: `${singularPageApiName}_Content_Objecting_NestedObject`, objectNestedObject: [ { - nestedObjectNestedTitle: "Content Objecting nested title #1" + nestedObjectNestedTitle: "Content Objecting nested title #1", + date: "2021-01-01", + time: "12:00:00", + dateTimeWithTimezone: "2021-01-01T12:00:00+01:00", + dateTimeWithoutTimezone: "2021-01-01T12:00:00.000Z" }, { - nestedObjectNestedTitle: "Content Objecting nested title #2" + nestedObjectNestedTitle: "Content Objecting nested title #2", + date: "2021-02-05", + time: "14:00:00", + dateTimeWithTimezone: "2021-02-05T12:00:00+01:00", + dateTimeWithoutTimezone: "2021-02-05T12:00:00.000Z" } ], objectTitle: "Objective title #1" @@ -77,10 +93,18 @@ const contentEntryQueryData = { nestedObject: { objectNestedObject: [ { - nestedObjectNestedTitle: "Objective nested title #1" + nestedObjectNestedTitle: "Objective nested title #1", + date: "2021-01-01", + time: "12:00:00", + dateTimeWithTimezone: "2021-01-01T12:00:00+01:00", + dateTimeWithoutTimezone: "2021-01-01T12:00:00.000Z" }, { - nestedObjectNestedTitle: "Objective nested title #2" + nestedObjectNestedTitle: "Objective nested title #2", + date: "2021-02-05", + time: "14:00:00", + dateTimeWithTimezone: "2021-02-05T12:00:00+01:00", + dateTimeWithoutTimezone: "2021-02-05T12:00:00.000Z" } ], objectTitle: "Objective title #1", @@ -138,10 +162,22 @@ const contentEntryMutationData = { SimpleText: { text: "Simple Text #1" } }, { - Hero: { title: "Hero Title #1" } + Hero: { + title: "Hero Title #1", + date: "2021-01-01", + time: "12:00:00", + dateTimeWithTimezone: "2021-01-01T12:00:00+01:00", + dateTimeWithoutTimezone: "2021-01-01T12:00:00.000Z" + } }, { - Hero: { title: "Hero Title #2" } + Hero: { + title: "Hero Title #2", + date: "2021-02-05", + time: "14:00:00", + dateTimeWithTimezone: "2021-02-05T12:00:00+01:00", + dateTimeWithoutTimezone: "2021-02-05T12:00:00.000Z" + } }, { Objecting: { @@ -149,10 +185,18 @@ const contentEntryMutationData = { objectTitle: "Objective title #1", objectNestedObject: [ { - nestedObjectNestedTitle: "Content Objecting nested title #1" + nestedObjectNestedTitle: "Content Objecting nested title #1", + date: "2021-01-01", + time: "12:00:00", + dateTimeWithTimezone: "2021-01-01T12:00:00+01:00", + dateTimeWithoutTimezone: "2021-01-01T12:00:00.000Z" }, { - nestedObjectNestedTitle: "Content Objecting nested title #2" + nestedObjectNestedTitle: "Content Objecting nested title #2", + date: "2021-02-05", + time: "14:00:00", + dateTimeWithTimezone: "2021-02-05T12:00:00+01:00", + dateTimeWithoutTimezone: "2021-02-05T12:00:00.000Z" } ] }, @@ -210,10 +254,18 @@ const contentEntryMutationData = { ], objectNestedObject: [ { - nestedObjectNestedTitle: "Objective nested title #1" + nestedObjectNestedTitle: "Objective nested title #1", + date: "2021-01-01", + time: "12:00:00", + dateTimeWithTimezone: "2021-01-01T12:00:00+01:00", + dateTimeWithoutTimezone: "2021-01-01T12:00:00.000Z" }, { - nestedObjectNestedTitle: "Objective nested title #2" + nestedObjectNestedTitle: "Objective nested title #2", + date: "2021-02-05", + time: "14:00:00", + dateTimeWithTimezone: "2021-02-05T12:00:00+01:00", + dateTimeWithoutTimezone: "2021-02-05T12:00:00.000Z" } ] } @@ -480,7 +532,7 @@ describe("dynamicZone field", () => { } }); - const tplIsConverted = (tpl: T) => "_templateId" in tpl; + const tplIsConverted = (tpl: T) => "_templateId" in tpl; expect(eventEntryContent.beforeCreate?.values.content.every(tplIsConverted)).toEqual(true); expect(eventEntryContent.afterCreate?.values.content.every(tplIsConverted)).toEqual(true); diff --git a/packages/api-headless-cms/__tests__/contentAPI/mocks/pageWithDynamicZonesModel.ts b/packages/api-headless-cms/__tests__/contentAPI/mocks/pageWithDynamicZonesModel.ts index 3d61f2f87a1..9064836a13a 100644 --- a/packages/api-headless-cms/__tests__/contentAPI/mocks/pageWithDynamicZonesModel.ts +++ b/packages/api-headless-cms/__tests__/contentAPI/mocks/pageWithDynamicZonesModel.ts @@ -92,6 +92,62 @@ export const pageModel: CmsModel = { } ], fieldId: "title" + }, + { + settings: { + type: "date" + }, + renderer: { + name: "date-time-input" + }, + label: "Date", + id: "eyqi5168", + type: "datetime", + validation: [], + fieldId: "date", + storageId: "datetime@eyqi5168" + }, + { + settings: { + type: "time" + }, + renderer: { + name: "date-time-input" + }, + label: "Time", + id: "zwnirh2r", + type: "datetime", + validation: [], + fieldId: "time", + storageId: "datetime@zwnirh2r" + }, + { + settings: { + type: "dateTimeWithTimezone" + }, + renderer: { + name: "date-time-input" + }, + label: "Date Time With Timezone", + id: "wwn7s1rp", + type: "datetime", + validation: [], + fieldId: "dateTimeWithTimezone", + storageId: "datetime@wwn7s1rp" + }, + { + settings: { + type: "dateTimeWithoutTimezone" + }, + renderer: { + name: "date-time-input" + }, + label: "Date Time Without Timezone", + id: "521a5932", + type: "datetime", + validation: [], + fieldId: "dateTimeWithoutTimezone", + storageId: "datetime@521a5932" } ], validation: [ @@ -186,6 +242,62 @@ export const pageModel: CmsModel = { message: `"nestedObject.objectNestedObject.nestedObjectNestedTitle" is required.` } ] + }, + { + settings: { + type: "date" + }, + renderer: { + name: "date-time-input" + }, + label: "Date", + id: "6he8oex2", + type: "datetime", + validation: [], + fieldId: "date", + storageId: "datetime@6he8oex2" + }, + { + settings: { + type: "time" + }, + renderer: { + name: "date-time-input" + }, + label: "Time", + id: "3k56vyr9", + type: "datetime", + validation: [], + fieldId: "time", + storageId: "datetime@3k56vyr9" + }, + { + settings: { + type: "dateTimeWithTimezone" + }, + renderer: { + name: "date-time-input" + }, + label: "Date Time With Timezone", + id: "dualjcdv", + type: "datetime", + validation: [], + fieldId: "dateTimeWithTimezone", + storageId: "datetime@dualjcdv" + }, + { + settings: { + type: "dateTimeWithoutTimezone" + }, + renderer: { + name: "date-time-input" + }, + label: "Date Time Without Timezone", + id: "7y789p8j", + type: "datetime", + validation: [], + fieldId: "dateTimeWithoutTimezone", + storageId: "datetime@7y789p8j" } ] } @@ -466,6 +578,62 @@ export const pageModel: CmsModel = { message: `"nestedObjectNestedTitle" is required.` } ] + }, + { + settings: { + type: "date" + }, + renderer: { + name: "date-time-input" + }, + label: "Date", + id: "6he8oex2", + type: "datetime", + validation: [], + fieldId: "date", + storageId: "datetime@6he8oex2" + }, + { + settings: { + type: "time" + }, + renderer: { + name: "date-time-input" + }, + label: "Time", + id: "3k56vyr9", + type: "datetime", + validation: [], + fieldId: "time", + storageId: "datetime@3k56vyr9" + }, + { + settings: { + type: "dateTimeWithTimezone" + }, + renderer: { + name: "date-time-input" + }, + label: "Date Time With Timezone", + id: "dualjcdv", + type: "datetime", + validation: [], + fieldId: "dateTimeWithTimezone", + storageId: "datetime@dualjcdv" + }, + { + settings: { + type: "dateTimeWithoutTimezone" + }, + renderer: { + name: "date-time-input" + }, + label: "Date Time Without Timezone", + id: "7y789p8j", + type: "datetime", + validation: [], + fieldId: "dateTimeWithoutTimezone", + storageId: "datetime@7y789p8j" } ] } diff --git a/packages/api-headless-cms/__tests__/plugins/storage/date.test.ts b/packages/api-headless-cms/__tests__/plugins/storage/date.test.ts new file mode 100644 index 00000000000..a4a7847e9d8 --- /dev/null +++ b/packages/api-headless-cms/__tests__/plugins/storage/date.test.ts @@ -0,0 +1,150 @@ +import { ToStorageParams } from "~/plugins/StorageTransformPlugin"; +import { createDateStorageTransformPlugin } from "~/storage/date"; + +const createDefaultArgs = ({ storageId = "storageId", type = "", multipleValues = false }) => { + return { + field: { + storageId, + settings: { + type + }, + multipleValues + } + }; +}; + +const defaultDateArgs = createDefaultArgs({ + type: "date" +}); +const defaultDateMultipleArgs = createDefaultArgs({ + type: "date", + multipleValues: true +}); +const defaultTimeArgs = createDefaultArgs({ + type: "time" +}); +const defaultDateTimeWithTimezoneArgs = createDefaultArgs({ + type: "dateTimeWithTimezone" +}); + +describe("dateStoragePlugin", () => { + const correctSingleToStorageDateValues = [ + [new Date("2021-03-31T13:34:55.000Z"), "2021-03-31T13:34:55.000Z"], + [new Date("2021-02-22T01:01:01.003Z"), "2021-02-22T01:01:01.003Z"], + ["2021-01-01T01:01:52.003Z", "2021-01-01T01:01:52.003Z"] + ]; + test.each(correctSingleToStorageDateValues)( + "toStorage should transform single value for storage", + async (value, expected) => { + const plugin = createDateStorageTransformPlugin(); + + const result = await plugin.toStorage({ + ...defaultDateArgs, + value + } as ToStorageParams); + + expect(result).toEqual(expected); + } + ); + + const correctMultipleToStorageDateValues = [ + [ + [new Date("2021-03-31T13:34:55.000Z"), new Date("2021-03-31T14:34:55.000Z")], + ["2021-03-31T13:34:55.000Z", "2021-03-31T14:34:55.000Z"] + ], + [ + [new Date("2021-02-22T01:01:01.003Z"), new Date("2021-02-22T02:01:01.003Z")], + ["2021-02-22T01:01:01.003Z", "2021-02-22T02:01:01.003Z"] + ], + [ + ["2021-01-01T01:01:52.003Z", "2021-01-01T05:01:52.003Z"], + ["2021-01-01T01:01:52.003Z", "2021-01-01T05:01:52.003Z"] + ] + ]; + test.each(correctMultipleToStorageDateValues)( + "toStorage should transform multiple value for storage", + async (value, expected) => { + const plugin = createDateStorageTransformPlugin(); + + const result = await plugin.toStorage({ + ...defaultDateMultipleArgs, + value + } as ToStorageParams); + + expect(result).toEqual(expected); + } + ); + + const correctSingleFromStorageDateValues: [string, Date][] = [ + ["2021-03-31T13:34:55.000Z", new Date("2021-03-31T13:34:55.000Z")], + ["2021-02-22T01:01:01.003Z", new Date("2021-02-22T01:01:01.003Z")], + ["2021-01-01T01:01:52.003Z", new Date("2021-01-01T01:01:52.003Z")] + ]; + + test.each(correctSingleFromStorageDateValues)( + "fromStorage should transform single value for output", + async (value, expected) => { + const plugin = createDateStorageTransformPlugin(); + + const result = await plugin.fromStorage({ + ...defaultDateArgs, + value + } as ToStorageParams); + + expect(result).toEqual(expected); + } + ); + + const correctMultipleFromStorageDateValues: [string[], Date[]][] = [ + [ + ["2021-03-31T13:34:55.000Z", "2021-03-31T14:34:55.000Z"], + [new Date("2021-03-31T13:34:55.000Z"), new Date("2021-03-31T14:34:55.000Z")] + ], + [ + ["2021-02-22T01:01:01.003Z", "2021-02-22T02:01:01.003Z"], + [new Date("2021-02-22T01:01:01.003Z"), new Date("2021-02-22T02:01:01.003Z")] + ], + [ + ["2021-01-01T01:01:52.003Z", "2021-01-01T14:01:52.003Z"], + [new Date("2021-01-01T01:01:52.003Z"), new Date("2021-01-01T14:01:52.003Z")] + ] + ]; + + test.each(correctMultipleFromStorageDateValues)( + "fromStorage should transform multiple value for output", + async (value, expected) => { + const plugin = createDateStorageTransformPlugin(); + + const result = await plugin.fromStorage({ + ...defaultDateMultipleArgs, + value + } as ToStorageParams); + + expect(result).toEqual(expected); + } + ); + + it("should not convert time field value", async () => { + const plugin = createDateStorageTransformPlugin(); + const value = "11:34:58"; + + const result = await plugin.toStorage({ + ...defaultTimeArgs, + value + } as ToStorageParams); + + expect(result).toEqual(value); + }); + + it("should not convert dateTime with tz field value", async () => { + const plugin = createDateStorageTransformPlugin(); + const value = "2021-04-08T13:34:59+0100"; + + const result = await plugin.toStorage({ + ...defaultDateTimeWithTimezoneArgs, + value + } as ToStorageParams); + + expect(result).toEqual(value); + }); +}); diff --git a/packages/api-headless-cms/__tests__/plugins/storage/dynamicZone.ts b/packages/api-headless-cms/__tests__/plugins/storage/dynamicZone.ts new file mode 100644 index 00000000000..6a45bc2c3c9 --- /dev/null +++ b/packages/api-headless-cms/__tests__/plugins/storage/dynamicZone.ts @@ -0,0 +1,167 @@ +import { pageModel } from "../../contentAPI/mocks/pageWithDynamicZonesModel"; +import { CmsModel, CmsModelDynamicZoneField } from "~/types"; +import { createDynamicZoneStorageTransform } from "~/storage/dynamicZone"; +import { createStorageTransform } from "~/storage/index"; +import { getStoragePluginFactory } from "~/utils/entryStorage"; +import { PluginsContainer } from "@webiny/plugins"; + +const field = pageModel.fields.find(f => f.id === "peeeyhtc") as CmsModelDynamicZoneField; + +const inputValue = [ + { + text: "Simple Text #1", + _templateId: "81qiz2v453wx9uque0gox" + }, + { + title: "Hero Title #1", + date: "2024-11-05", + time: "11:05:59", + dateTimeWithoutTimezone: new Date("2024-11-05T11:05:59.000Z"), + dateTimeWithTimezone: "2024-11-05T11:05:59.000+01:00", + _templateId: "cv2zf965v324ivdc7e1vt" + }, + { + title: "Hero Title #2", + date: "2024-11-05", + time: "11:05:59", + dateTimeWithoutTimezone: new Date("2024-11-06T11:05:59.000Z"), + dateTimeWithTimezone: "2024-11-06T11:05:59.000+01:00", + _templateId: "cv2zf965v324ivdc7e1vt" + }, + { + nestedObject: { + objectTitle: "Objective title #1", + objectNestedObject: [ + { + nestedObjectNestedTitle: "Content Objecting nested title #1", + date: "2024-11-05", + time: "11:05:59", + dateTimeWithoutTimezone: new Date("2024-11-05T11:05:59.000Z"), + dateTimeWithTimezone: "2024-11-05T11:05:59.000+01:00" + }, + { + nestedObjectNestedTitle: "Content Objecting nested title #2", + date: "2024-11-05", + time: "11:05:59", + dateTimeWithoutTimezone: new Date("2024-11-06T11:05:59.000Z"), + dateTimeWithTimezone: "2024-11-06T11:05:59.000+01:00" + } + ] + }, + dynamicZone: { + authors: [ + { + id: "john-doe#0001", + entryId: "john-doe", + modelId: "author" + } + ], + _templateId: "0emukbsvmzpozx2lzk883" + }, + _templateId: "9ht43gurhegkbdfsaafyads" + }, + { + author: { + id: "john-doe#0001", + entryId: "john-doe", + modelId: "author" + }, + authors: [ + { + id: "john-doe#0001", + entryId: "john-doe", + modelId: "author" + } + ], + _templateId: "qi81z2v453wx9uque0gox" + } +]; +const expectedValue = [ + { + text: "Simple Text #1", + _templateId: "81qiz2v453wx9uque0gox" + }, + { + title: "Hero Title #1", + date: "2024-11-05", + time: "11:05:59", + dateTimeWithoutTimezone: "2024-11-05T11:05:59.000Z", + dateTimeWithTimezone: "2024-11-05T11:05:59.000+01:00", + _templateId: "cv2zf965v324ivdc7e1vt" + }, + { + title: "Hero Title #2", + date: "2024-11-05", + time: "11:05:59", + dateTimeWithoutTimezone: "2024-11-06T11:05:59.000Z", + dateTimeWithTimezone: "2024-11-06T11:05:59.000+01:00", + _templateId: "cv2zf965v324ivdc7e1vt" + }, + { + nestedObject: { + objectTitle: "Objective title #1", + objectNestedObject: [ + { + nestedObjectNestedTitle: "Content Objecting nested title #1", + date: "2024-11-05", + time: "11:05:59", + dateTimeWithoutTimezone: "2024-11-05T11:05:59.000Z", + dateTimeWithTimezone: "2024-11-05T11:05:59.000+01:00" + }, + { + nestedObjectNestedTitle: "Content Objecting nested title #2", + date: "2024-11-05", + time: "11:05:59", + dateTimeWithoutTimezone: "2024-11-06T11:05:59.000Z", + dateTimeWithTimezone: "2024-11-06T11:05:59.000+01:00" + } + ] + }, + dynamicZone: { + authors: [ + { + id: "john-doe#0001", + entryId: "john-doe", + modelId: "author" + } + ], + _templateId: "0emukbsvmzpozx2lzk883" + }, + _templateId: "9ht43gurhegkbdfsaafyads" + }, + { + author: { + id: "john-doe#0001", + entryId: "john-doe", + modelId: "author" + }, + authors: [ + { + id: "john-doe#0001", + entryId: "john-doe", + modelId: "author" + } + ], + _templateId: "qi81z2v453wx9uque0gox" + } +]; + +describe("dynamic zone storage transform", () => { + const plugins = new PluginsContainer(createStorageTransform()); + const plugin = createDynamicZoneStorageTransform(); + const getStoragePlugin = getStoragePluginFactory({ + plugins + }); + + it("should properly transform data to storage", async () => { + const result = await plugin.toStorage({ + field, + value: inputValue, + getStoragePlugin, + model: pageModel as CmsModel, + plugins + }); + + expect(result).toEqual(expectedValue); + }); +}); diff --git a/packages/api-headless-cms/__tests__/testHelpers/usePageManageHandler.ts b/packages/api-headless-cms/__tests__/testHelpers/usePageManageHandler.ts index e99b92625ce..6c52fe1a32b 100644 --- a/packages/api-headless-cms/__tests__/testHelpers/usePageManageHandler.ts +++ b/packages/api-headless-cms/__tests__/testHelpers/usePageManageHandler.ts @@ -9,6 +9,10 @@ const pageFields = ` content { ...on ${singularPageApiName}_Content_Hero { title + date + time + dateTimeWithoutTimezone + dateTimeWithTimezone _templateId __typename } @@ -23,6 +27,10 @@ const pageFields = ` objectTitle objectNestedObject { nestedObjectNestedTitle + date + time + dateTimeWithoutTimezone + dateTimeWithTimezone } } dynamicZone { @@ -68,6 +76,10 @@ const pageFields = ` objectBody objectNestedObject { nestedObjectNestedTitle + date + time + dateTimeWithoutTimezone + dateTimeWithTimezone } } __typename diff --git a/packages/api-headless-cms/__tests__/testHelpers/usePageReadHandler.ts b/packages/api-headless-cms/__tests__/testHelpers/usePageReadHandler.ts index 1c64e41062b..f2986f8542a 100644 --- a/packages/api-headless-cms/__tests__/testHelpers/usePageReadHandler.ts +++ b/packages/api-headless-cms/__tests__/testHelpers/usePageReadHandler.ts @@ -9,6 +9,10 @@ const pageFields = ` content { ...on ${singularPageApiName}_Content_Hero { title + date + time + dateTimeWithTimezone + dateTimeWithoutTimezone __typename } ...on ${singularPageApiName}_Content_SimpleText { @@ -21,6 +25,10 @@ const pageFields = ` objectTitle objectNestedObject { nestedObjectNestedTitle + date + time + dateTimeWithTimezone + dateTimeWithoutTimezone } } dynamicZone { @@ -69,6 +77,10 @@ const pageFields = ` objectBody objectNestedObject { nestedObjectNestedTitle + date + time + dateTimeWithTimezone + dateTimeWithoutTimezone } } __typename diff --git a/packages/api-headless-cms/src/fieldConverters/CmsModelDefaultFieldConverterPlugin.ts b/packages/api-headless-cms/src/fieldConverters/CmsModelDefaultFieldConverterPlugin.ts index f694243b19a..7a70a3edc20 100644 --- a/packages/api-headless-cms/src/fieldConverters/CmsModelDefaultFieldConverterPlugin.ts +++ b/packages/api-headless-cms/src/fieldConverters/CmsModelDefaultFieldConverterPlugin.ts @@ -2,6 +2,7 @@ import { CmsModelFieldConverterPlugin, ConvertParams } from "~/plugins/CmsModelFieldConverterPlugin"; +import { GenericRecord } from "@webiny/api/types"; export class CmsModelDefaultFieldConverterPlugin extends CmsModelFieldConverterPlugin { public override name = "cms.field.converter.default"; @@ -10,7 +11,7 @@ export class CmsModelDefaultFieldConverterPlugin extends CmsModelFieldConverterP return "*"; } - public override convertToStorage({ field, value }: ConvertParams): any { + public override convertToStorage({ field, value }: ConvertParams): GenericRecord { /** * Do not convert if no value was passed. */ @@ -25,7 +26,7 @@ export class CmsModelDefaultFieldConverterPlugin extends CmsModelFieldConverterP }; } - public override convertFromStorage({ field, value }: ConvertParams): any { + public override convertFromStorage({ field, value }: ConvertParams): GenericRecord { /** * Do not convert if no value was passed. */ diff --git a/packages/api-headless-cms/src/fieldConverters/CmsModelDynamicZoneFieldConverterPlugin.ts b/packages/api-headless-cms/src/fieldConverters/CmsModelDynamicZoneFieldConverterPlugin.ts index 2e74d384a09..52b693dcdf9 100644 --- a/packages/api-headless-cms/src/fieldConverters/CmsModelDynamicZoneFieldConverterPlugin.ts +++ b/packages/api-headless-cms/src/fieldConverters/CmsModelDynamicZoneFieldConverterPlugin.ts @@ -5,10 +5,9 @@ import { } from "~/plugins/CmsModelFieldConverterPlugin"; import { CmsDynamicZoneTemplate, CmsEntryValues, CmsModelDynamicZoneField } from "~/types"; import { ConverterCollection } from "~/utils/converters/ConverterCollection"; +import { GenericRecord } from "@webiny/api/types"; -interface DynamicZoneValue { - [key: string]: any; -} +type DynamicZoneValue = GenericRecord; interface ProcessValue { templates: CmsDynamicZoneTemplate[]; @@ -87,7 +86,7 @@ export class CmsModelDynamicZoneFieldConverterPlugin extends CmsModelFieldConver return undefined; } - return template.fields.reduce>( + return template.fields.reduce>( (values, field) => { const converter = converterCollection.getConverter(field.type); const converted = converter.convertToStorage({ @@ -169,7 +168,7 @@ export class CmsModelDynamicZoneFieldConverterPlugin extends CmsModelFieldConver ); } - return template.fields.reduce>( + return template.fields.reduce>( (values, field) => { const converter = converterCollection.getConverter(field.type); const converted = converter.convertFromStorage({ diff --git a/packages/api-headless-cms/src/fieldConverters/CmsModelObjectFieldConverterPlugin.ts b/packages/api-headless-cms/src/fieldConverters/CmsModelObjectFieldConverterPlugin.ts index e26525878ca..f400e6fab34 100644 --- a/packages/api-headless-cms/src/fieldConverters/CmsModelObjectFieldConverterPlugin.ts +++ b/packages/api-headless-cms/src/fieldConverters/CmsModelObjectFieldConverterPlugin.ts @@ -4,10 +4,11 @@ import { } from "~/plugins/CmsModelFieldConverterPlugin"; import { CmsEntryValues, CmsModelFieldWithParent } from "~/types"; import { ConverterCollection } from "~/utils/converters/ConverterCollection"; +import { GenericRecord } from "@webiny/api/types"; interface ProcessChildFieldsParams { fields: CmsModelFieldWithParent[]; - value?: Record | null; + value?: GenericRecord | null; converterCollection: ConverterCollection; } @@ -43,7 +44,7 @@ export class CmsModelObjectFieldConverterPlugin extends CmsModelFieldConverterPl }; } return { - [field.storageId]: value.map((itemValue: any) => { + [field.storageId]: value.map((itemValue: GenericRecord) => { return this.processChildFieldsToStorage({ fields: childFields.map(child => { return { @@ -96,7 +97,7 @@ export class CmsModelObjectFieldConverterPlugin extends CmsModelFieldConverterPl if (Array.isArray(value[field.fieldId]) === false) { return output; } - const values = value[field.fieldId].map((childValue: any) => { + const values = value[field.fieldId].map((childValue: GenericRecord) => { return converterCollection.convertToStorage({ fields: childFields.map(child => { return { @@ -167,7 +168,7 @@ export class CmsModelObjectFieldConverterPlugin extends CmsModelFieldConverterPl }; } return { - [field.fieldId]: value.map((itemValue: any) => { + [field.fieldId]: value.map((itemValue: GenericRecord) => { return this.processChildFieldsFromStorage({ fields: childFields.map(child => { return { @@ -218,7 +219,9 @@ export class CmsModelObjectFieldConverterPlugin extends CmsModelFieldConverterPl if (childFields.length > 0) { if (field.multipleValues) { - const inputValues = value[field.storageId] as unknown as Record[]; + const inputValues = value[ + field.storageId + ] as unknown as GenericRecord[]; if (!inputValues || Array.isArray(inputValues) === false) { return output; } diff --git a/packages/api-headless-cms/src/fieldConverters/index.ts b/packages/api-headless-cms/src/fieldConverters/index.ts index 2c508bf7323..41cab490cf5 100644 --- a/packages/api-headless-cms/src/fieldConverters/index.ts +++ b/packages/api-headless-cms/src/fieldConverters/index.ts @@ -1,3 +1,6 @@ +/** + * Field converters are used to convert the fieldId to storageId and vice versa. + */ import { CmsModelObjectFieldConverterPlugin } from "~/fieldConverters/CmsModelObjectFieldConverterPlugin"; import { CmsModelDefaultFieldConverterPlugin } from "~/fieldConverters/CmsModelDefaultFieldConverterPlugin"; import { CmsModelDynamicZoneFieldConverterPlugin } from "~/fieldConverters/CmsModelDynamicZoneFieldConverterPlugin"; diff --git a/packages/api-headless-cms/src/plugins/CmsModelFieldConverterPlugin.ts b/packages/api-headless-cms/src/plugins/CmsModelFieldConverterPlugin.ts index a3ddf934d6e..02a370e8b5e 100644 --- a/packages/api-headless-cms/src/plugins/CmsModelFieldConverterPlugin.ts +++ b/packages/api-headless-cms/src/plugins/CmsModelFieldConverterPlugin.ts @@ -1,3 +1,6 @@ +/** + * Field converters are used to convert the fieldId to storageId and vice versa. + */ import { Plugin } from "@webiny/plugins"; import { CmsEntryValues, CmsModelFieldWithParent } from "~/types"; import { ConverterCollection } from "~/utils/converters/ConverterCollection"; diff --git a/packages/api-headless-cms/src/plugins/StorageTransformPlugin.ts b/packages/api-headless-cms/src/plugins/StorageTransformPlugin.ts index 4236821da29..07478226d2d 100644 --- a/packages/api-headless-cms/src/plugins/StorageTransformPlugin.ts +++ b/packages/api-headless-cms/src/plugins/StorageTransformPlugin.ts @@ -1,3 +1,6 @@ +/** + * Storage transforms are used to transform the data before it is saved to the database and after it is read from the database. + */ import { Plugin } from "@webiny/plugins/Plugin"; import { CmsModel, CmsModelField, CmsModelFieldType } from "~/types"; import { PluginsContainer } from "@webiny/plugins"; diff --git a/packages/api-headless-cms-ddb/src/dynamoDb/storage/date.ts b/packages/api-headless-cms/src/storage/date.ts similarity index 82% rename from packages/api-headless-cms-ddb/src/dynamoDb/storage/date.ts rename to packages/api-headless-cms/src/storage/date.ts index 3cf13cfc56c..693e9f6dd77 100644 --- a/packages/api-headless-cms-ddb/src/dynamoDb/storage/date.ts +++ b/packages/api-headless-cms/src/storage/date.ts @@ -2,8 +2,9 @@ * File is @internal */ import WebinyError from "@webiny/error"; -import { StorageTransformPlugin } from "@webiny/api-headless-cms"; -import { CmsModelField } from "@webiny/api-headless-cms/types"; +import { CmsModelField } from "~/types"; +import { GenericRecord } from "@webiny/api/types"; +import { StorageTransformPlugin } from "~/plugins"; const excludeTypes = ["time", "dateTimeWithTimezone"]; @@ -31,8 +32,8 @@ const convertFromStorage = ( } }; -const convertValueToStorage = (field: CmsModelField, value: any): string => { - if (value instanceof Date || (value as Record).toISOString) { +const convertValueToStorage = (field: CmsModelField, value: Date | string | unknown): string => { + if (value instanceof Date || (value as GenericRecord)?.toISOString) { return (value as Date).toISOString(); } else if (typeof value === "string") { return value as string; @@ -47,6 +48,7 @@ const convertValueToStorage = (field: CmsModelField, value: any): string => { export const createDateStorageTransformPlugin = () => { return new StorageTransformPlugin({ fieldType: "datetime", + name: "headless-cms.storage-transform.date.default", fromStorage: async ({ value, field }) => { const { type } = field.settings || {}; if (!value || !type || excludeTypes.includes(type)) { @@ -62,10 +64,10 @@ export const createDateStorageTransformPlugin = () => { if (field.multipleValues) { const multipleValues = value as (string | Date | null | undefined)[]; return (multipleValues || []) - .filter(v => !!v) .map(v => { return convertValueToStorage(field, v); - }); + }) + .filter(v => !!v); } return convertValueToStorage(field, value); } diff --git a/packages/api-headless-cms/src/storage/dynamicZone.ts b/packages/api-headless-cms/src/storage/dynamicZone.ts new file mode 100644 index 00000000000..ec2f1cd37af --- /dev/null +++ b/packages/api-headless-cms/src/storage/dynamicZone.ts @@ -0,0 +1,91 @@ +import pMap from "p-map"; +import { StorageTransformPlugin, ToStorageParams } from "~/plugins"; +import { GenericRecord } from "@webiny/api/types"; +import { CmsModel, CmsModelDynamicZoneField, CmsModelField } from "~/types"; +import { PluginsContainer } from "@webiny/plugins"; +import pReduce from "p-reduce"; + +interface IProcessFromStorageParams { + model: CmsModel; + field: CmsModelDynamicZoneField; + value: GenericRecord; + getStoragePlugin: ToStorageParams["getStoragePlugin"]; + plugins: PluginsContainer; +} + +const processToStorage = async (params: IProcessFromStorageParams): Promise => { + const { model, field: parentField, value: input, getStoragePlugin, plugins } = params; + + const output: GenericRecord = structuredClone(input); + + if (!output._templateId) { + return output; + } + const template = parentField.settings.templates.find(t => t.id === output._templateId); + if (!template || !template.fields.length) { + return output; + } + + return await pReduce( + template.fields, + async (values, field) => { + const value = values[field.fieldId]; + + if (!value) { + delete values[field.fieldId]; + return values; + } + const plugin = getStoragePlugin(field.type); + if (!plugin) { + console.error(`Missing storage plugin for field type "${field.type}".`); + delete values[field.fieldId]; + return values; + } + values[field.fieldId] = await plugin.toStorage({ + plugins, + getStoragePlugin, + model, + field, + value + }); + + return values; + }, + output + ); +}; + +export const createDynamicZoneStorageTransform = (): StorageTransformPlugin => { + return new StorageTransformPlugin({ + name: "headless-cms.storage-transform.dynamicZone.default", + fieldType: "dynamicZone", + toStorage: async ({ field, value, getStoragePlugin, model, plugins }) => { + if (!value) { + return null; + } else if (field.multipleValues) { + if (!Array.isArray(value)) { + return value; + } + return await pMap(value as GenericRecord[], async value => { + return processToStorage({ + model, + field: field as CmsModelDynamicZoneField, + value, + getStoragePlugin, + plugins + }); + }); + } + return processToStorage({ + model, + field: field as CmsModelDynamicZoneField, + value, + getStoragePlugin, + plugins + }); + }, + fromStorage: async ({ value }) => { + return value; + } + }); +}; diff --git a/packages/api-headless-cms/src/storage/index.ts b/packages/api-headless-cms/src/storage/index.ts index 6183b842dc8..2d2d40b3709 100644 --- a/packages/api-headless-cms/src/storage/index.ts +++ b/packages/api-headless-cms/src/storage/index.ts @@ -1,11 +1,18 @@ +/** + * Storage transforms are used to transform the data before it is saved to the database and after it is read from the database. + */ import { createDefaultStorageTransform } from "./default"; import { createObjectStorageTransform } from "./object"; import { createJsonStorageTransform } from "./json"; +import { createDynamicZoneStorageTransform } from "./dynamicZone"; +import { createDateStorageTransformPlugin } from "./date"; export const createStorageTransform = () => { return [ createDefaultStorageTransform(), + createDateStorageTransformPlugin(), createObjectStorageTransform(), - createJsonStorageTransform() + createJsonStorageTransform(), + createDynamicZoneStorageTransform() ]; }; diff --git a/packages/api-headless-cms/src/storage/object.ts b/packages/api-headless-cms/src/storage/object.ts index f21ab6e0202..7099dc0e79a 100644 --- a/packages/api-headless-cms/src/storage/object.ts +++ b/packages/api-headless-cms/src/storage/object.ts @@ -4,17 +4,18 @@ import { CmsModel, CmsModelField } from "~/types"; import { PluginsContainer } from "@webiny/plugins"; import { StorageTransformPlugin } from "~/plugins/StorageTransformPlugin"; import { getBaseFieldType } from "~/utils/getBaseFieldType"; +import { GenericRecord } from "@webiny/api/types"; interface ProcessValueParams { fields: CmsModelField[]; - sourceValue: Record; + sourceValue: GenericRecord; getStoragePlugin: (fieldType: string) => StorageTransformPlugin; plugins: PluginsContainer; model: CmsModel; operation: "toStorage" | "fromStorage"; } interface ProcessValue { - (params: ProcessValueParams): Promise>; + (params: ProcessValueParams): Promise; } const processValue: ProcessValue = async params => { @@ -52,7 +53,7 @@ export const createObjectStorageTransform = (): StorageTransformPlugin => { const fields = (field.settings?.fields || []) as CmsModelField[]; if (field.multipleValues) { - return await pMap(value as Record[], value => + return await pMap(value as GenericRecord[], value => processValue({ sourceValue: value, getStoragePlugin, @@ -81,7 +82,7 @@ export const createObjectStorageTransform = (): StorageTransformPlugin => { const fields = (field.settings?.fields || []) as CmsModelField[]; if (field.multipleValues) { - return pMap(value as Record[], value => + return pMap(value as GenericRecord[], value => processValue({ sourceValue: value, getStoragePlugin, diff --git a/packages/api-headless-cms/src/utils/entryStorage.ts b/packages/api-headless-cms/src/utils/entryStorage.ts index 2156e46b225..01ba7f072da 100644 --- a/packages/api-headless-cms/src/utils/entryStorage.ts +++ b/packages/api-headless-cms/src/utils/entryStorage.ts @@ -3,11 +3,11 @@ import { StorageTransformPlugin } from "~/plugins/StorageTransformPlugin"; import { CmsContext, CmsEntry, CmsModel, CmsModelField } from "~/types"; import { getBaseFieldType } from "~/utils/getBaseFieldType"; -interface GetStoragePluginFactory { +export interface GetStoragePluginFactory { (context: Pick): (fieldType: string) => StorageTransformPlugin; } -const getStoragePluginFactory: GetStoragePluginFactory = context => { +export const getStoragePluginFactory: GetStoragePluginFactory = context => { let defaultStoragePlugin: StorageTransformPlugin; const plugins = context.plugins @@ -32,9 +32,11 @@ const getStoragePluginFactory: GetStoragePluginFactory = context => { return collection; }, {} as Record); - return (fieldType: string) => { + const fn = (fieldType: string) => { return plugins[fieldType] || defaultStoragePlugin; }; + fn.plugins = plugins; + return fn; }; /**