diff --git a/.github/readme/blueprint.md b/.github/readme/blueprint.md index db630a5..0d5536d 100644 --- a/.github/readme/blueprint.md +++ b/.github/readme/blueprint.md @@ -1,90 +1,121 @@ -Contentstack App SDK Readme +# Contentstack App SDK Readme + The Contentstack App SDK allows you to customize your applications. This document will help you integrate the App SDK with your application. -Getting started +## Getting started Include the compiled version of the extension client library by adding the following line to your application. +```html +``` To include the App SDK in your project, you need to run the following command: +```sh npm install @contentstack/app-sdk +``` + Alternatively, you can use the following command within the script tag to install the App SDK: +```html +``` + +### Initializing the App SDK -Initializing the App SDK To Initialize the App SDK you need to run the following command: +```js ContentstackAppSdk.init().then(function (appSdk) { -// add code here + // add code here }); -For more information, please refer to our App SDK API Reference document. +``` + +For more information, please refer to our [App SDK API Reference](https://github.com/contentstack/app-sdk-docs#contentstack-app-sdk-api-reference) document. + +## Download the Boilerplate -Download the Boilerplate You can extend or customize the functionality of Contentstack CMS with Marketplace apps. To simplify and speed up the building process, boilerplates describe repetitive elements in a project. This boilerplate will help you build custom applications for your organization or stack. -Download the boilerplate. +Download the [boilerplate](https://github.com/contentstack/marketplace-app-boilerplate/archive/refs/heads/master.zip). + +## UI Locations and Examples -UI Locations and Examples UI Locations allow you to extend Contentstack's functionality. Through these UI locations, you can customize Contentstack's default behavior and UI. Integration of third-party applications is possible using different UI locations. The Contentstack App SDK currently supports the following UI Locations: -Custom Field Location -Dashboard Location -Asset Sidebar Location -App Config Location -RTE Location -Sidebar Location -Field Modifier Location -Full Page Location -Custom Field Location -Custom Field Location allows you to create custom fields that can be used in your content types. You can integrate with various business applications, such as Bynder, Cloudinary, Shopify, by adding them as a custom field to your stack's content type. - -Dashboard Location -With the Dashboard Location, you can create widgets for your stack dashboard. Integration with Google Analytics provides meaningful insights about your website. - -Asset Sidebar Location +- [Custom Field Location](https://www.contentstack.com/docs/developers/developer-hub/custom-field-location) +- [Dashboard Location](https://www.contentstack.com/docs/developers/developer-hub/dashboard-location) +- [Asset Sidebar Location](https://www.contentstack.com/docs/developers/developer-hub/asset-sidebar-location) +- [App Config Location](https://www.contentstack.com/docs/developers/developer-hub/app-config-location) +- [RTE Location](https://www.contentstack.com/docs/developers/developer-hub/rte-location) +- [Sidebar Location](https://www.contentstack.com/docs/developers/developer-hub/sidebar-location) +- [Field Modifier Location](https://www.contentstack.com/docs/developers/developer-hub/field-modifier-location/) +- [Full Page Location](https://www.contentstack.com/docs/developers/developer-hub/full-page-location) + +### Custom Field Location + +Custom Field Location allows you to create custom fields that can be used in your content types. You can integrate with various business applications, such as [Bynder](https://www.contentstack.com/docs/developers/marketplace-apps/bynder), [Cloudinary](https://www.contentstack.com/docs/developers/marketplace-apps/cloudinary), [Shopify](https://www.contentstack.com/docs/developers/marketplace-apps/shopify), by adding them as a custom field to your stack's content type. + +### Dashboard Location + +With the Dashboard Location, you can create widgets for your stack dashboard. Integration with [Google Analytics](https://www.contentstack.com/docs/developers/marketplace-apps/google-analytics/) provides meaningful insights about your website. + +### Asset Sidebar Location + Using the Asset Sidebar Location, you can create customized sidebar widgets to extend the functionality of your assets. -Manage, transform, and optimize your stack's assets efficiently using the Image Preset Builder. +Manage, transform, and optimize your stack's assets efficiently using the [Image Preset Builder](https://www.contentstack.com/docs/developers/marketplace-apps/image-preset-builder). + +### App Config Location -App Config Location App Config UI Location allows you to manage all the app settings centrally. Once configured, all other locations (where the app is installed) can access these settings. -RTE Location +### RTE Location + The RTE Location allows you to create custom plugins to expand the functionality of your JSON Rich Text Editor. Using the Audience and Variables plugin, you can tailor your content as per your requirements. -Sidebar Location -The Sidebar Location provides powerful tools for analyzing and recommending ideas for your entry. Use the Smartling sidebar location to help translate your content. +### Sidebar Location + +The Sidebar Location provides powerful tools for analyzing and recommending ideas for your entry. Use the [Smartling](https://help.smartling.com/hc/en-us/articles/4865477629083) sidebar location to help translate your content. + +### Field Modifier Location -Field Modifier Location The Field Modifier Location is a type of UI location which extends the capabilities of entry fields. With the Field Modifier UI location, you can allow the different apps to appear on defined field data types such as Text, Number, JSON, Boolean, File, Reference fields etc. -Full Page Location -The Full Page location is a type of UI location that lets you view full page apps such as Release Preview within your stack. +### Full Page Location + +The Full Page location is a type of UI location that lets you view full page apps such as [Release Preview](https://www.contentstack.com/docs/developers/marketplace-apps/release-preview) within your stack. + +## Using Contentstack styles -Using Contentstack styles Install the Venus UI library package to style your app according to the Contentstack UI: +```sh npm i @contentstack/venus-components --save -For more information on styling your application, refer to our style guide. +``` + +For more information on styling your application, refer to our [style guide](https://www.contentstack.com/docs/developers/venus-component-library/). + +## More information + +- [App SDK API Reference](https://github.com/contentstack/app-sdk-docs#readme) +- [Marketplace Platform Guides](https://www.contentstack.com/docs/developers/marketplace-platform-guides/) +- [Marketplace Apps](https://www.contentstack.com/docs/developers/marketplace-apps/) +- [Contentstack App Development](https://www.contentstack.com/docs/developers/developer-hub/) + +## App SDK v2.0.0 Migration Guide + +This guide provides instructions for migrating your application to App SDK version 2.0.0. It covers changes in metadata responses, field modifier and full page location updates, and the transition from the `_extension` property to `_uiLocation`. If you are upgrading your app to the latest version, make sure to follow these steps for a smooth transition. -More information -App SDK API Reference -Marketplace Platform Guides -Marketplace Apps -Contentstack App Development -App SDK v2.0.0 Migration Guide -This guide provides instructions for migrating your application to App SDK version 2.0.0. It covers changes in metadata responses, field modifier and full page location updates, and the transition from the \_extension property to \_uiLocation. If you are upgrading your app to the latest version, make sure to follow these steps for a smooth transition. +[Read the Migration Guide](./docs/app-sdk-v2-migration.md) -Read the Migration Guide +## License -License -Licensed under MIT. +Licensed under [MIT](https://opensource.org/licenses/MIT). diff --git a/README.md b/README.md index 91b66b0..1b902e4 100644 --- a/README.md +++ b/README.md @@ -1,121 +1,145 @@ -# Contentstack App SDK Readme - -The Contentstack App SDK allows you to customize your applications. This document will help you integrate the App SDK with your application. - -## Getting started - -Include the compiled version of the extension client library by adding the following line to your application. - -```html - -``` - -To include the App SDK in your project, you need to run the following command: - -```sh -npm install @contentstack/app-sdk -``` - -Alternatively, you can use the following command within the script tag to install the App SDK: - -```html - -``` - -### Initializing the App SDK - -To Initialize the App SDK you need to run the following command: - -```js -ContentstackAppSdk.init().then(function (appSdk) { -// add code here -}); -``` - -For more information, please refer to our [App SDK API Reference](https://github.com/contentstack/app-sdk-docs#contentstack-app-sdk-api-reference) document. - -## Download the Boilerplate - -You can extend or customize the functionality of Contentstack CMS with Marketplace apps. To simplify and speed up the building process, boilerplates describe repetitive elements in a project. This boilerplate will help you build custom applications for your organization or stack. - -Download the [boilerplate](https://github.com/contentstack/marketplace-app-boilerplate/archive/refs/heads/master.zip). - -## UI Locations and Examples - -UI Locations allow you to extend Contentstack's functionality. Through these UI locations, you can customize Contentstack's default behavior and UI. Integration of third-party applications is possible using different UI locations. - -The Contentstack App SDK currently supports the following UI Locations: - -- [Custom Field Location](https://www.contentstack.com/docs/developers/developer-hub/custom-field-location) -- [Dashboard Location](https://www.contentstack.com/docs/developers/developer-hub/dashboard-location) -- [Asset Sidebar Location](https://www.contentstack.com/docs/developers/developer-hub/asset-sidebar-location) -- [App Config Location](https://www.contentstack.com/docs/developers/developer-hub/app-config-location) -- [RTE Location](https://www.contentstack.com/docs/developers/developer-hub/rte-location) -- [Sidebar Location](https://www.contentstack.com/docs/developers/developer-hub/sidebar-location) -- [Field Modifier Location](https://www.contentstack.com/docs/developers/developer-hub/field-modifier-location/) -- [Full Page Location](https://www.contentstack.com/docs/developers/developer-hub/full-page-location) - -### Custom Field Location - -Custom Field Location allows you to create custom fields that can be used in your content types. You can integrate with various business applications, such as [Bynder](https://www.contentstack.com/docs/developers/marketplace-apps/bynder), [Cloudinary](https://www.contentstack.com/docs/developers/marketplace-apps/cloudinary), [Shopify](https://www.contentstack.com/docs/developers/marketplace-apps/shopify), by adding them as a custom field to your stack's content type. - -### Dashboard Location - -With the Dashboard Location, you can create widgets for your stack dashboard. Integration with [Google Analytics](https://www.contentstack.com/docs/developers/marketplace-apps/google-analytics/) provides meaningful insights about your website. - -### Asset Sidebar Location - -Using the Asset Sidebar Location, you can create customized sidebar widgets to extend the functionality of your assets. - -Manage, transform, and optimize your stack's assets efficiently using the [Image Preset Builder](https://www.contentstack.com/docs/developers/marketplace-apps/image-preset-builder). - -### App Config Location - -App Config UI Location allows you to manage all the app settings centrally. Once configured, all other locations (where the app is installed) can access these settings. - -### RTE Location - -The RTE Location allows you to create custom plugins to expand the functionality of your JSON Rich Text Editor. Using the Audience and Variables plugin, you can tailor your content as per your requirements. - -### Sidebar Location - -The Sidebar Location provides powerful tools for analyzing and recommending ideas for your entry. Use the [Smartling](https://help.smartling.com/hc/en-us/articles/4865477629083) sidebar location to help translate your content. - -### Field Modifier Location - -The Field Modifier Location is a type of UI location which extends the capabilities of entry fields. With the Field Modifier UI location, you can allow the different apps to appear on defined field data types such as Text, Number, JSON, Boolean, File, Reference fields etc. - -### Full Page Location - -The Full Page location is a type of UI location that lets you view full page apps such as [Release Preview](https://www.contentstack.com/docs/developers/marketplace-apps/release-preview) within your stack. - -## Using Contentstack styles - -Install the Venus UI library package to style your app according to the Contentstack UI: - -```sh -npm i @contentstack/venus-components --save -``` - -For more information on styling your application, refer to our [style guide](https://www.contentstack.com/docs/developers/venus-component-library/). - -## More information - -- [App SDK API Reference](https://github.com/contentstack/app-sdk-docs#readme) -- [Marketplace Platform Guides](https://www.contentstack.com/docs/developers/marketplace-platform-guides/) -- [Marketplace Apps](https://www.contentstack.com/docs/developers/marketplace-apps/) -- [Contentstack App Development](https://www.contentstack.com/docs/developers/developer-hub/) - -## App SDK v2.0.0 Migration Guide - -This guide provides instructions for migrating your application to App SDK version 2.0.0. It covers changes in metadata responses, field modifier and full page location updates, and the transition from the `_extension` property to `_uiLocation`. If you are upgrading your app to the latest version, make sure to follow these steps for a smooth transition. - -[Read the Migration Guide](./docs/app-sdk-v2-migration.md) - -## License - -Licensed under [MIT](https://opensource.org/licenses/MIT). + +[](#contentstack-app-sdk-readme) + +# Contentstack App SDK Readme + +The Contentstack App SDK allows you to customize your applications. This document will help you integrate the App SDK with your application. + + +[](#getting-started) + +## Getting started + +Include the compiled version of the extension client library by adding the following line to your application. + +```html + +``` + +To include the App SDK in your project, you need to run the following command: + +```sh +npm install @contentstack/app-sdk +``` + +Alternatively, you can use the following command within the script tag to install the App SDK: + +```html + +``` + +### Initializing the App SDK + +To Initialize the App SDK you need to run the following command: + +```js +ContentstackAppSdk.init().then(function (appSdk) { +// add code here +}); +``` + +For more information, please refer to our [App SDK API Reference](https://github.com/contentstack/app-sdk-docs#contentstack-app-sdk-api-reference) document. + + +[](#download-the-boilerplate) + +## Download the Boilerplate + +You can extend or customize the functionality of Contentstack CMS with Marketplace apps. To simplify and speed up the building process, boilerplates describe repetitive elements in a project. This boilerplate will help you build custom applications for your organization or stack. + +Download the [boilerplate](https://github.com/contentstack/marketplace-app-boilerplate/archive/refs/heads/master.zip). + + +[](#ui-locations-and-examples) + +## UI Locations and Examples + +UI Locations allow you to extend Contentstack's functionality. Through these UI locations, you can customize Contentstack's default behavior and UI. Integration of third-party applications is possible using different UI locations. + +The Contentstack App SDK currently supports the following UI Locations: + +- [Custom Field Location](https://www.contentstack.com/docs/developers/developer-hub/custom-field-location) +- [Dashboard Location](https://www.contentstack.com/docs/developers/developer-hub/dashboard-location) +- [Asset Sidebar Location](https://www.contentstack.com/docs/developers/developer-hub/asset-sidebar-location) +- [App Config Location](https://www.contentstack.com/docs/developers/developer-hub/app-config-location) +- [RTE Location](https://www.contentstack.com/docs/developers/developer-hub/rte-location) +- [Sidebar Location](https://www.contentstack.com/docs/developers/developer-hub/sidebar-location) +- [Field Modifier Location](https://www.contentstack.com/docs/developers/developer-hub/field-modifier-location/) +- [Full Page Location](https://www.contentstack.com/docs/developers/developer-hub/full-page-location) + +### Custom Field Location + +Custom Field Location allows you to create custom fields that can be used in your content types. You can integrate with various business applications, such as [Bynder](https://www.contentstack.com/docs/developers/marketplace-apps/bynder), [Cloudinary](https://www.contentstack.com/docs/developers/marketplace-apps/cloudinary), [Shopify](https://www.contentstack.com/docs/developers/marketplace-apps/shopify), by adding them as a custom field to your stack's content type. + +### Dashboard Location + +With the Dashboard Location, you can create widgets for your stack dashboard. Integration with [Google Analytics](https://www.contentstack.com/docs/developers/marketplace-apps/google-analytics/) provides meaningful insights about your website. + +### Asset Sidebar Location + +Using the Asset Sidebar Location, you can create customized sidebar widgets to extend the functionality of your assets. + +Manage, transform, and optimize your stack's assets efficiently using the [Image Preset Builder](https://www.contentstack.com/docs/developers/marketplace-apps/image-preset-builder). + +### App Config Location + +App Config UI Location allows you to manage all the app settings centrally. Once configured, all other locations (where the app is installed) can access these settings. + +### RTE Location + +The RTE Location allows you to create custom plugins to expand the functionality of your JSON Rich Text Editor. Using the Audience and Variables plugin, you can tailor your content as per your requirements. + +### Sidebar Location + +The Sidebar Location provides powerful tools for analyzing and recommending ideas for your entry. Use the [Smartling](https://help.smartling.com/hc/en-us/articles/4865477629083) sidebar location to help translate your content. + +### Field Modifier Location + +The Field Modifier Location is a type of UI location which extends the capabilities of entry fields. With the Field Modifier UI location, you can allow the different apps to appear on defined field data types such as Text, Number, JSON, Boolean, File, Reference fields etc. + +### Full Page Location + +The Full Page location is a type of UI location that lets you view full page apps such as [Release Preview](https://www.contentstack.com/docs/developers/marketplace-apps/release-preview) within your stack. + + +[](#using-contentstack-styles) + +## Using Contentstack styles + +Install the Venus UI library package to style your app according to the Contentstack UI: + +```sh +npm i @contentstack/venus-components --save +``` + +For more information on styling your application, refer to our [style guide](https://www.contentstack.com/docs/developers/venus-component-library/). + + +[](#more-information) + +## More information + +- [App SDK API Reference](https://github.com/contentstack/app-sdk-docs#readme) +- [Marketplace Platform Guides](https://www.contentstack.com/docs/developers/marketplace-platform-guides/) +- [Marketplace Apps](https://www.contentstack.com/docs/developers/marketplace-apps/) +- [Contentstack App Development](https://www.contentstack.com/docs/developers/developer-hub/) + + +[](#app-sdk-v200-migration-guide) + +## App SDK v2.0.0 Migration Guide + +This guide provides instructions for migrating your application to App SDK version 2.0.0. It covers changes in metadata responses, field modifier and full page location updates, and the transition from the `_extension` property to `_uiLocation`. If you are upgrading your app to the latest version, make sure to follow these steps for a smooth transition. + +[Read the Migration Guide](./docs/app-sdk-v2-migration.md) + + +[](#license) + +## License + +Licensed under [MIT](https://opensource.org/licenses/MIT). diff --git a/__test__/entry.test.ts b/__test__/entry.test.ts index dfd3bc2..99fa986 100644 --- a/__test__/entry.test.ts +++ b/__test__/entry.test.ts @@ -195,21 +195,108 @@ describe("Entry", () => { }); }); - it("set field data restriction", async () => { + describe("entry.setData", () => { + it("merges fields on success", async () => { + jest.spyOn(connection, "sendToParent").mockResolvedValue({ + data: { success: true }, + }); + const r = await entry.setData({ title: "merged-title" } as any); + expect(connection.sendToParent).toHaveBeenCalledWith( + "setEntryData", + { data: { title: "merged-title" } } + ); + expect((entry.getData() as any).title).toEqual("merged-title"); + expect((r as any).title).toEqual("merged-title"); + }); + + it("does not attach _setDataRequestId (correlation deferred)", async () => { + jest.spyOn(connection, "sendToParent").mockResolvedValue({ + data: { success: true }, + }); + await entry.setData({ title: "x" } as any); + expect(connection.sendToParent).toHaveBeenCalledWith("setEntryData", { + data: { title: "x" }, + }); + }); + + it("rejects on VALIDATION_ERROR", async () => { + jest.spyOn(connection, "sendToParent").mockResolvedValue({ + data: { + code: "VALIDATION_ERROR", + message: "x", + details: [], + }, + }); + await expect( + entry.setData({ title: "x" } as any) + ).rejects.toMatchObject({ code: "VALIDATION_ERROR" }); + }); + + it("rejects on SETDATA_RESOLUTION_ERROR without merging entry", async () => { + const before = { ...(entry.getData() as any) }; + jest.spyOn(connection, "sendToParent").mockResolvedValue({ + data: { + code: "SETDATA_RESOLUTION_ERROR", + message: "resolve failed", + failures: [], + }, + }); + await expect( + entry.setData({ file_field: "bltx" } as any) + ).rejects.toMatchObject({ code: "SETDATA_RESOLUTION_ERROR" }); + expect(entry.getData()).toEqual(before); + }); + + it("rejects when no entry data", async () => { + const dataNoEntry = JSON.parse(JSON.stringify(testData)); + dataNoEntry.entry = undefined; + const badEntry = new Entry(dataNoEntry as any, connection as any, emitter); + await expect(badEntry.setData({} as any)).rejects.toThrow( + "not available in this location" + ); + }); + }); + + it("setData on nested multiple group field calls parent (bridge validates)", async () => { const uid = "group.group.group"; const field = entry.getField(uid); + jest.spyOn(connection, "sendToParent").mockResolvedValue({ + data: { success: true }, + }); + await field.setData([{ single_line: "x" }] as any); + expect(connection.sendToParent).toHaveBeenCalledWith("setData", { + data: [{ single_line: "x" }], + uid, + self: false, + }); + }); - await expect(field.setData({ d: "dummy" })).rejects.toThrowError( - "Cannot call set data for current field type" - ); + it("non-self setData on group calls parent", async () => { + const uid = "group.group.group"; + const field = entry.getField(uid); + jest.spyOn(connection, "sendToParent").mockResolvedValue({ + data: { success: true }, + }); + await field.setData([{ single_line: "x" }] as any); + expect(connection.sendToParent).toHaveBeenCalledWith("setData", { + data: [{ single_line: "x" }], + uid, + self: false, + }); }); - it("set field data restriction for modular blocks, one complete block", async () => { + it("setData on modular blocks field calls parent", async () => { const uid = "modular_blocks.0"; const field = entry.getField(uid); - await expect(field.setData({ d: "dummy" })).rejects.toThrowError( - "Cannot call set data for current field type" - ); + jest.spyOn(connection, "sendToParent").mockResolvedValue({ + data: { success: true }, + }); + await field.setData({ d: "dummy" }); + expect(connection.sendToParent).toHaveBeenCalledWith("setData", { + data: { d: "dummy" }, + uid, + self: false, + }); }); it("getField Invalid Uid", function () { diff --git a/__test__/field.test.ts b/__test__/field.test.ts index afb5882..0c98f69 100644 --- a/__test__/field.test.ts +++ b/__test__/field.test.ts @@ -1,4 +1,5 @@ import Field from "../src/field"; +import { SetDataValidationError } from "../src/utils/setDataErrors"; import testData from "./data/testData.json"; import fileFieldData from "./data/fileField.json"; import helpers from "./helpers"; @@ -75,6 +76,54 @@ describe("Field", () => { field.setFocus(); expect(connection.sendToParent).toHaveBeenCalledWith("focus"); }); + + it("setData rejects on bridge VALIDATION_ERROR", async () => { + jest.spyOn(connection, "sendToParent").mockResolvedValue({ + data: { + code: "VALIDATION_ERROR", + message: "bad", + details: [], + }, + }); + await expect(field.setData("bad")).rejects.toBeInstanceOf( + SetDataValidationError + ); + await expect(field.setData("bad")).rejects.toMatchObject({ + code: "VALIDATION_ERROR", + message: "bad", + }); + }); + + it("setData rejects on host SETDATA_RESOLUTION_ERROR", async () => { + jest.spyOn(connection, "sendToParent").mockResolvedValue({ + data: { + code: "SETDATA_RESOLUTION_ERROR", + message: "resolve failed", + failures: [{ kind: "asset", uid: "bltx", reason: "x" }], + }, + }); + await expect(field.setData("bltx")).rejects.toMatchObject({ + code: "SETDATA_RESOLUTION_ERROR", + failures: [{ kind: "asset", uid: "bltx", reason: "x" }], + }); + }); + + it("setData resolves with warnings when bridge returns warnings", async () => { + const warn = [ + { + field: "title", + fieldUid: "title", + fieldLabel: "Title", + fieldType: "text", + reasons: [{ reason: "MIN_LENGTH", message: "too short" }], + }, + ]; + jest.spyOn(connection, "sendToParent").mockResolvedValue({ + data: { warnings: warn }, + }); + const out: any = await field.setData("ab"); + expect(out.warnings).toEqual(warn); + }); }); describe("File", () => { diff --git a/__test__/setDataBridgeResponse.test.ts b/__test__/setDataBridgeResponse.test.ts new file mode 100644 index 0000000..2bca9d1 --- /dev/null +++ b/__test__/setDataBridgeResponse.test.ts @@ -0,0 +1,73 @@ +import { + SETDATA_RESOLUTION_ERROR_CODE, + getResolutionErrorPayload, + getSetDataWarnings, + getValidationErrorPayload, + isResolutionErrorPayload, + isValidationErrorPayload, +} from "../src/utils/setDataBridgeResponse"; + +describe("setDataBridgeResponse", () => { + it("detects VALIDATION_ERROR flat and nested", () => { + expect( + isValidationErrorPayload({ + data: { code: "VALIDATION_ERROR", message: "x", details: [] }, + }) + ).toBe(true); + expect( + isValidationErrorPayload({ + data: { data: { code: "VALIDATION_ERROR" } }, + }) + ).toBe(true); + }); + + it("getValidationErrorPayload prefers nested error object", () => { + const e = getValidationErrorPayload({ + data: { data: { code: "VALIDATION_ERROR", details: [1] } }, + }); + expect(e.code).toBe("VALIDATION_ERROR"); + expect(e.details).toEqual([1]); + }); + + it("getSetDataWarnings reads top-level or nested", () => { + const w = [{ field: "a" }]; + expect( + getSetDataWarnings({ data: { warnings: w } }) + ).toEqual(w); + expect( + getSetDataWarnings({ data: { data: { warnings: w } } }) + ).toEqual(w); + }); + + it("detects SETDATA_RESOLUTION_ERROR flat and nested", () => { + expect( + isResolutionErrorPayload({ + data: { + code: SETDATA_RESOLUTION_ERROR_CODE, + message: "m", + failures: [], + }, + }) + ).toBe(true); + expect( + isResolutionErrorPayload({ + data: { data: { code: SETDATA_RESOLUTION_ERROR_CODE } }, + }) + ).toBe(true); + }); + + it("getResolutionErrorPayload prefers nested error object", () => { + const e = getResolutionErrorPayload({ + data: { + data: { + code: SETDATA_RESOLUTION_ERROR_CODE, + failures: [{ kind: "asset", uid: "bltx", reason: "x" }], + }, + }, + }); + expect(e.code).toBe(SETDATA_RESOLUTION_ERROR_CODE); + expect(e.failures).toEqual([ + { kind: "asset", uid: "bltx", reason: "x" }, + ]); + }); +}); diff --git a/__test__/setDataRequestCorrelation.test.ts b/__test__/setDataRequestCorrelation.test.ts new file mode 100644 index 0000000..b043fc9 --- /dev/null +++ b/__test__/setDataRequestCorrelation.test.ts @@ -0,0 +1,128 @@ +import EventEmitter from "wolfy87-eventemitter"; +import { jest } from "@jest/globals"; + +import type { SetDataValidationEvent } from "../src/types/setDataValidation.types"; +import { + SET_DATA_VALIDATION_EMITTER_EVENT, + SET_DATA_VALIDATION_WIRE_NAME, +} from "../src/types/setDataValidation.types"; +import { parseSetDataValidationPayload } from "../src/utils/setDataRequestCorrelation"; + +/** Mirrors `uiLocation` extensionEvent handling for SET_DATA_VALIDATION. */ +function emitSetDataValidationFromExtensionEvent( + event: { data?: { name?: string; data?: unknown } }, + emitter: EventEmitter +): void { + if (event.data?.name === SET_DATA_VALIDATION_WIRE_NAME) { + const parsed = parseSetDataValidationPayload(event.data.data); + if (parsed) { + emitter.emitEvent(SET_DATA_VALIDATION_EMITTER_EVENT, [parsed]); + } + } +} + +describe("setDataValidationExtensionEvent", () => { + it("dispatches field validation to emitter", () => { + const emitter = new EventEmitter(); + const cb = jest.fn(); + emitter.on(SET_DATA_VALIDATION_EMITTER_EVENT, cb); + + emitSetDataValidationFromExtensionEvent( + { + data: { + name: "SET_DATA_VALIDATION", + data: { + requestId: "req-1", + source: "field", + fieldUid: "title", + status: "success", + }, + }, + }, + emitter + ); + + expect(cb).toHaveBeenCalledTimes(1); + expect(cb.mock.calls[0][0]).toMatchObject({ + requestId: "req-1", + source: "field", + status: "success", + }); + }); + + it("dispatches when requestId is omitted (simplified path)", () => { + const emitter = new EventEmitter(); + const cb = jest.fn(); + emitter.on(SET_DATA_VALIDATION_EMITTER_EVENT, cb); + + emitSetDataValidationFromExtensionEvent( + { + data: { + name: "SET_DATA_VALIDATION", + data: { + source: "field", + fieldUid: "title", + status: "success", + }, + }, + }, + emitter + ); + + expect(cb).toHaveBeenCalledTimes(1); + expect(cb.mock.calls[0][0]).toMatchObject({ + source: "field", + fieldUid: "title", + status: "success", + }); + expect( + (cb.mock.calls[0][0] as SetDataValidationEvent).requestId + ).toBeUndefined(); + }); + + it("dispatches entry batch validation without stale filtering", () => { + const emitter = new EventEmitter(); + const cb = jest.fn(); + emitter.on(SET_DATA_VALIDATION_EMITTER_EVENT, cb); + + emitSetDataValidationFromExtensionEvent( + { + data: { + name: "SET_DATA_VALIDATION", + data: { + requestId: "older", + source: "entry", + status: "error", + errors: [{ fieldUid: "a", message: "bad" }], + }, + }, + }, + emitter + ); + + expect(cb).toHaveBeenCalledTimes(1); + }); + + it("drops malformed payloads", () => { + const emitter = new EventEmitter(); + const cb = jest.fn(); + emitter.on(SET_DATA_VALIDATION_EMITTER_EVENT, cb); + + emitSetDataValidationFromExtensionEvent( + { + data: { + name: "SET_DATA_VALIDATION", + data: { + source: "field", + fieldUid: "title", + status: "error", + errors: [], + }, + }, + }, + emitter + ); + + expect(cb).not.toHaveBeenCalled(); + }); +}); diff --git a/blueprint.json b/blueprint.json index 2888181..9868ce0 100644 --- a/blueprint.json +++ b/blueprint.json @@ -6,6 +6,6 @@ }, "line": "none", "subresourceIntegrity": { - "js": "sha512-emopr/zIeDgm48mJPZsk0xuAautCD26qEG0RV5c8/R/MMhGG+N1TUZf2HxOVmS9BexQ6beP8YT5Q9MLf+ZK0Hw==" + "js": "sha512-mPQQ5ZV/ovd9XSNp4OK6PVCtqFc+oTah9qacLs8uydezibUGxCuxfmVj/+QSop80rDpYa92duh41SpTh8mE44A==" } } diff --git a/package-lock.json b/package-lock.json index c5e5ebb..09fc11e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@contentstack/app-sdk", - "version": "2.3.6", + "version": "2.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@contentstack/app-sdk", - "version": "2.3.6", + "version": "2.4.0", "license": "MIT", "dependencies": { "axios": "^1.7.9", @@ -3098,6 +3098,165 @@ "node": ">=20.0.0" } }, + "node_modules/@peculiar/asn1-cms": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.6.1.tgz", + "integrity": "sha512-vdG4fBF6Lkirkcl53q6eOdn3XYKt+kJTG59edgRZORlg/3atWWEReRCx5rYE1ZzTTX6vLK5zDMjHh7vbrcXGtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "@peculiar/asn1-x509-attr": "^2.6.1", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-csr": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.6.1.tgz", + "integrity": "sha512-WRWnKfIocHyzFYQTka8O/tXCiBquAPSrRjXbOkHbO4qdmS6loffCEGs+rby6WxxGdJCuunnhS2duHURhjyio6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-ecc": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.6.1.tgz", + "integrity": "sha512-+Vqw8WFxrtDIN5ehUdvlN2m73exS2JVG0UAyfVB31gIfor3zWEAQPD+K9ydCxaj3MLen9k0JhKpu9LqviuCE1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pfx": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.6.1.tgz", + "integrity": "sha512-nB5jVQy3MAAWvq0KY0R2JUZG8bO/bTLpnwyOzXyEh/e54ynGTatAR+csOnXkkVD9AFZ2uL8Z7EV918+qB1qDvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.6.1", + "@peculiar/asn1-pkcs8": "^2.6.1", + "@peculiar/asn1-rsa": "^2.6.1", + "@peculiar/asn1-schema": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pkcs8": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.6.1.tgz", + "integrity": "sha512-JB5iQ9Izn5yGMw3ZG4Nw3Xn/hb/G38GYF3lf7WmJb8JZUydhVGEjK/ZlFSWhnlB7K/4oqEs8HnfFIKklhR58Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pkcs9": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.6.1.tgz", + "integrity": "sha512-5EV8nZoMSxeWmcxWmmcolg22ojZRgJg+Y9MX2fnE2bGRo5KQLqV5IL9kdSQDZxlHz95tHvIq9F//bvL1OeNILw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.6.1", + "@peculiar/asn1-pfx": "^2.6.1", + "@peculiar/asn1-pkcs8": "^2.6.1", + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "@peculiar/asn1-x509-attr": "^2.6.1", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-rsa": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.6.1.tgz", + "integrity": "sha512-1nVMEh46SElUt5CB3RUTV4EG/z7iYc7EoaDY5ECwganibQPkZ/Y2eMsTKB/LeyrUJ+W/tKoD9WUqIy8vB+CEdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-schema": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.6.0.tgz", + "integrity": "sha512-xNLYLBFTBKkCzEZIw842BxytQQATQv+lDTCEMZ8C196iJcJJMBUZxrhSTxLaohMyKK8QlzRNTRkUmanucnDSqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "asn1js": "^3.0.6", + "pvtsutils": "^1.3.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-x509": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.6.1.tgz", + "integrity": "sha512-O9jT5F1A2+t3r7C4VT7LYGXqkGLK7Kj1xFpz7U0isPrubwU5PbDoyYtx6MiGst29yq7pXN5vZbQFKRCP+lLZlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "asn1js": "^3.0.6", + "pvtsutils": "^1.3.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-x509-attr": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.6.1.tgz", + "integrity": "sha512-tlW6cxoHwgcQghnJwv3YS+9OO1737zgPogZ+CgWRUK4roEwIPzRH4JEiG770xe5HX2ATfCpmX60gurfWIF9dcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/x509": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.14.3.tgz", + "integrity": "sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.6.0", + "@peculiar/asn1-csr": "^2.6.0", + "@peculiar/asn1-ecc": "^2.6.0", + "@peculiar/asn1-pkcs9": "^2.6.0", + "@peculiar/asn1-rsa": "^2.6.0", + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "pvtsutils": "^1.3.6", + "reflect-metadata": "^0.2.2", + "tslib": "^2.8.1", + "tsyringe": "^4.10.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@sinonjs/commons": { "version": "1.8.6", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", @@ -4377,6 +4536,22 @@ "proxy-from-env": "^1.1.0" } }, + "node_modules/axios/node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/babel-jest": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-27.5.1.tgz", @@ -4562,9 +4737,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", - "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "version": "2.10.10", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.10.tgz", + "integrity": "sha512-sUoJ3IMxx4AyRqO4MLeHlnGDkyXRoUG0/AI9fjK+vS72ekpV0yWVY7O0BVjmBcRtkNcsAO2QDZ4tdKKGoI6YaQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -4639,6 +4814,27 @@ "ms": "2.0.0" } }, + "node_modules/body-parser/node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/body-parser/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -4646,6 +4842,16 @@ "dev": true, "license": "MIT" }, + "node_modules/body-parser/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/bonjour-service": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.3.0.tgz", @@ -4839,9 +5045,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001778", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001778.tgz", - "integrity": "sha512-PN7uxFL+ExFJO61aVmP1aIEG4i9whQd4eoSCebav62UwDyp5OHh06zN4jqKSMePVgxHifCw1QJxdRkA1Pisekg==", + "version": "1.0.30001781", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001781.tgz", + "integrity": "sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw==", "dev": true, "funding": [ { @@ -5167,14 +5373,11 @@ } }, "node_modules/commander": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-11.0.0.tgz", - "integrity": "sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ==", + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=16" - } + "license": "MIT" }, "node_modules/common-path-prefix": { "version": "3.0.0", @@ -5771,9 +5974,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.313", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.313.tgz", - "integrity": "sha512-QBMrTWEf00GXZmJyx2lbYD45jpI3TUFnNIzJ5BBc8piGUDwMPa1GV6HJWTZVvY/eiN3fSopl7NRbgGp9sZ9LTA==", + "version": "1.5.323", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.323.tgz", + "integrity": "sha512-oQm+FxbazvN2WICCbvJgj3IYPKV8awip57+W5VP+Aatk4kFU4pDYCPHZOX22Z27zpw8uttBehEqgK+VTJAYrVw==", "dev": true, "license": "ISC" }, @@ -5818,9 +6021,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.20.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz", - "integrity": "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==", + "version": "5.20.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", + "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", "dev": true, "license": "MIT", "dependencies": { @@ -6574,9 +6777,9 @@ } }, "node_modules/flatted": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.1.tgz", - "integrity": "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, @@ -6600,22 +6803,6 @@ } } }, - "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -7489,6 +7676,25 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-network-error": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.1.tgz", @@ -8875,6 +9081,16 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/lint-staged/node_modules/commander": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.0.0.tgz", + "integrity": "sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, "node_modules/lint-staged/node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -10336,8 +10552,7 @@ "type": "consulting", "url": "https://feross.org/support" } - ], - "license": "MIT" + ] }, "node_modules/range-parser": { "version": "1.2.1", @@ -10365,6 +10580,37 @@ "node": ">= 0.8" } }, + "node_modules/raw-body/node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/raw-body/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/react": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", @@ -10874,7 +11120,15 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true, - "license": "MIT" + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true }, "node_modules/serve-index": { "version": "1.9.2", @@ -11494,9 +11748,9 @@ "license": "MIT" }, "node_modules/tapable": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", - "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.2.tgz", + "integrity": "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==", "dev": true, "license": "MIT", "engines": { @@ -11525,9 +11779,9 @@ } }, "node_modules/terser": { - "version": "5.46.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz", - "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", + "version": "5.46.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.1.tgz", + "integrity": "sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -11577,13 +11831,6 @@ } } }, - "node_modules/terser/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, - "license": "MIT" - }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", diff --git a/package.json b/package.json index dec4df0..2749b8b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@contentstack/app-sdk", - "version": "2.3.6", + "version": "2.4.0", "types": "dist/src/index.d.ts", "description": "The Contentstack App SDK allows you to customize your Contentstack applications.", "main": "dist/index.js", diff --git a/src/entry.ts b/src/entry.ts index d56c45b..8e2597b 100755 --- a/src/entry.ts +++ b/src/entry.ts @@ -16,8 +16,19 @@ import { } from "./types/entry.types"; import { ContentType, PublishDetails, Schema } from "./types/stack.types"; import { GenericObjectType } from "./types/common.types"; -import EventRegistry from "./EventRegistry"; -import { onData, onError } from "./utils/utils"; +import { + getResolutionErrorPayload, + getSetDataWarnings, + getValidationErrorPayload, + isResolutionErrorPayload, + isValidationErrorPayload, +} from "./utils/setDataBridgeResponse"; +import { + SetDataResolutionError, + SetDataValidationError, +} from "./utils/setDataErrors"; +import type { SetDataValidationEvent } from "./types/setDataValidation.types"; +import { SET_DATA_VALIDATION_EMITTER_EVENT } from "./types/setDataValidation.types"; /** Class representing an entry from Contentstack UI. Not available for Dashboard UI Location. */ @@ -92,6 +103,57 @@ class Entry { return this._data; } + /** + * Updates multiple fields on the current entry in a single call (partial merge). + * Requires host support for `setEntryData` (app-extension-component 2.7.0+). + */ + async setData(data: GenericObjectType): Promise { + if (!this._data) { + throw new Error( + "entry.setData() is not available in this location" + ); + } + try { + const payload = { data }; + const response = await this._connection.sendToParent< + GenericObjectType & { + code?: string; + warnings?: unknown[]; + } + >("setEntryData", payload); + const envelope = { data: response?.data }; + if (isValidationErrorPayload(envelope)) { + throw SetDataValidationError.fromBridgePayload( + getValidationErrorPayload(envelope) + ); + } + if (isResolutionErrorPayload(envelope)) { + throw SetDataResolutionError.fromBridgePayload( + getResolutionErrorPayload(envelope) + ); + } + Object.assign(this._data, data); + const result: GenericObjectType = { ...data }; + const warnings = getSetDataWarnings(envelope); + if (warnings.length > 0) { + (result as GenericObjectType & { warnings?: unknown[] }).warnings = + warnings; + } + return result; + } catch (e) { + if ( + e instanceof SetDataValidationError || + e instanceof SetDataResolutionError + ) { + throw e; + } + const suffix = e instanceof Error ? ` ${e.message}` : ""; + throw new Error( + `entry.setData() requires host support (app-extension-component >= 2.7.0).${suffix}` + ); + } + } + /** * Retrieves the draft data of the current unsaved entry. * Returns an empty object if there are no changes. @@ -133,7 +195,7 @@ class Entry { /** * Gets the field object for the saved data, which allows you to interact with the field. * This object will have all the same methods and properties of appSDK.location.CustomField.field. - * Note: For fields initialized using the getFields function, the setData function currently works only for the following fields: as single_line, multi_line, RTE, markdown, select, number, boolean, date, link, and Custom Field UI Location of data type text, number, boolean, and date. + * Note: Non-self fields (`entry.getField`) delegate `setData` to the host bridge for all field types. * @example * var field = entry.getField('field_uid'); * var fieldSchema = field.schema; @@ -321,5 +383,26 @@ class Entry { throw Error("Callback must be a function"); } } + + /** + * Post-apply async validation for programmatic `field.setData` / `entry.setData` + * (`SET_DATA_VALIDATION` via extensionEvent). Sync outcomes stay on the Promise; see TRD §6. + */ + onSetDataValidation(callback: (event: SetDataValidationEvent) => void) { + const entryObj = this; + if (callback && typeof callback === "function") { + entryObj._emitter.on( + SET_DATA_VALIDATION_EMITTER_EVENT, + (event: SetDataValidationEvent) => { + callback(event); + } + ); + this._emitter.emitEvent("_eventRegistration", [ + { name: SET_DATA_VALIDATION_EMITTER_EVENT }, + ]); + } else { + throw Error("Callback must be a function"); + } + } } export default Entry; diff --git a/src/field.ts b/src/field.ts index 23b8939..de13d2e 100755 --- a/src/field.ts +++ b/src/field.ts @@ -3,14 +3,19 @@ import postRobot from "post-robot"; import { IFieldInitData, IFieldModifierLocationInitData } from "./types"; import { GenericObjectType } from "./types/common.types"; import { Schema } from "./types/stack.types"; - -const excludedDataTypesForSetField = [ - "file", - "reference", - "blocks", - "group", - "global_field", -]; +import { + getSetDataWarnings, + getValidationErrorPayload, + isValidationErrorPayload, + getResolutionErrorPayload, + isResolutionErrorPayload, +} from "./utils/setDataBridgeResponse"; +import { + SetDataResolutionError, + SetDataValidationError, +} from "./utils/setDataErrors"; +import type { SetDataValidationEvent } from "./types/setDataValidation.types"; +import { SET_DATA_VALIDATION_EMITTER_EVENT } from "./types/setDataValidation.types"; function separateResolvedData(field: Field, value: GenericObjectType) { let resolvedData = value; @@ -108,34 +113,38 @@ class Field { * @return {external:Promise} A promise object which is resolved when data is set for a field. Note: The data set by this function will only be saved when user saves the entry. */ - setData(data: any): Promise { + async setData(data: any): Promise { const currentFieldObj = this; - const dataObj = { + const dataObj: { + data: any; + uid: string; + self: boolean; + } = { data, uid: currentFieldObj.uid, self: currentFieldObj._self, }; - if ( - !currentFieldObj._self && - (excludedDataTypesForSetField.indexOf(currentFieldObj.data_type) !== - -1 || - !currentFieldObj.data_type) - ) { - return Promise.reject( - new Error("Cannot call set data for current field type") + const response = await this._connection.sendToParent("setData", dataObj); + if (isValidationErrorPayload(response)) { + throw SetDataValidationError.fromBridgePayload( + getValidationErrorPayload(response) ); } - - return this._connection - .sendToParent("setData", dataObj) - .then(() => { - this._data = data; - return Promise.resolve(currentFieldObj); - }) - .catch((e: Error) => { - return Promise.reject(e); - }); + if (isResolutionErrorPayload(response)) { + throw SetDataResolutionError.fromBridgePayload( + getResolutionErrorPayload(response) + ); + } + this._data = data; + const warnings = getSetDataWarnings(response); + if (warnings.length > 0) { + return { + ...currentFieldObj, + warnings, + } as Field; + } + return currentFieldObj; } /** @@ -176,6 +185,45 @@ class Field { throw Error("Callback must be a function"); } } + + /** + * Subscribe to post-apply / async setData validation for **this field** (wire `SET_DATA_VALIDATION`). + * Full entry lifecycle is available on {@link Entry#onSetDataValidation}. + */ + onSetDataValidation(callback: (event: SetDataValidationEvent) => void) { + const fieldObj = this; + if (callback && typeof callback === "function") { + fieldObj._emitter.on( + SET_DATA_VALIDATION_EMITTER_EVENT, + (event: SetDataValidationEvent) => { + if (event.source !== "field") { + return; + } + const uid = fieldObj.uid; + if ( + event.fieldUid !== undefined && + event.fieldUid !== uid + ) { + return; + } + if ( + event.errors?.some( + (e) => + e.fieldUid != null && e.fieldUid !== uid + ) + ) { + return; + } + callback(event); + } + ); + fieldObj._emitter.emitEvent("_eventRegistration", [ + { name: SET_DATA_VALIDATION_EMITTER_EVENT }, + ]); + } else { + throw Error("Callback must be a function"); + } + } } export default Field; diff --git a/src/fieldModifierLocation/field.ts b/src/fieldModifierLocation/field.ts index 5b43640..ac0e044 100644 --- a/src/fieldModifierLocation/field.ts +++ b/src/fieldModifierLocation/field.ts @@ -4,14 +4,17 @@ import postRobot from "post-robot"; import { IFieldInitData, IFieldModifierLocationInitData } from "../types"; import { GenericObjectType } from "../types/common.types"; import { Schema } from "../types/stack.types"; - -const excludedDataTypesForSetField = [ - "file", - "reference", - "blocks", - "group", - "global_field", -]; +import { + getSetDataWarnings, + getValidationErrorPayload, + isValidationErrorPayload, + getResolutionErrorPayload, + isResolutionErrorPayload, +} from "../utils/setDataBridgeResponse"; +import { + SetDataResolutionError, + SetDataValidationError, +} from "../utils/setDataErrors"; function separateResolvedData( field: FieldModifierLocationField, @@ -114,32 +117,36 @@ class FieldModifierLocationField { */ async setData(data: any): Promise { const currentFieldObj = this; - const dataObj = { + const dataObj: { + data: any; + uid: string; + self: boolean; + } = { data, uid: currentFieldObj.uid, self: currentFieldObj._self, }; - if ( - !currentFieldObj._self && - (excludedDataTypesForSetField.indexOf(currentFieldObj.data_type) !== - -1 || - !currentFieldObj.data_type) - ) { - return Promise.reject( - new Error("Cannot call set data for current field type") + const response = await this._connection.sendToParent("setData", dataObj); + if (isValidationErrorPayload(response)) { + throw SetDataValidationError.fromBridgePayload( + getValidationErrorPayload(response) ); } - - return this._connection - .sendToParent("setData", dataObj) - .then(() => { - this._data = data; - return Promise.resolve(currentFieldObj); - }) - .catch((e: Error) => { - return Promise.reject(e); - }); + if (isResolutionErrorPayload(response)) { + throw SetDataResolutionError.fromBridgePayload( + getResolutionErrorPayload(response) + ); + } + this._data = data; + const warnings = getSetDataWarnings(response); + if (warnings.length > 0) { + return { + ...currentFieldObj, + warnings, + } as FieldModifierLocationField; + } + return currentFieldObj; } /** diff --git a/src/index.ts b/src/index.ts index e988c8d..9676fa9 100755 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,6 @@ import postRobot from "post-robot"; +import { SetDataResolutionError, SetDataValidationError } from "./utils/setDataErrors"; import { InitializationData } from "./types"; import { IRteParam } from "./RTE/types"; import { PluginDefinition, PluginBuilder, registerPlugins } from "./rtePlugin"; @@ -104,6 +105,7 @@ class ContentstackAppSDK { export default ContentstackAppSDK; export { PluginBuilder }; +export { SetDataResolutionError, SetDataValidationError }; // CommonJS compatibility if (typeof module !== 'undefined' && module.exports) { diff --git a/src/types/complexFields.types.ts b/src/types/complexFields.types.ts new file mode 100644 index 0000000..0825f41 --- /dev/null +++ b/src/types/complexFields.types.ts @@ -0,0 +1,33 @@ +/** Value shape for reference field setData */ +export type ReferenceValue = { + uid: string; + _content_type_uid: string; +}; + +/** Single or multiple asset UIDs for file field setData */ +export type FileFieldValue = string | string[] | null; + +export type ValidationReasonDetail = { + reason: string; + message: string; +}; + +export type ValidationErrorDetail = { + field: string; + fieldUid: string; + fieldLabel: string; + fieldType: string; + reasons: ValidationReasonDetail[]; +}; + +export type ConstraintViolation = ValidationErrorDetail; + +export type ValidationError = { + code: "VALIDATION_ERROR"; + message: string; + details: ValidationErrorDetail[]; +}; + +export type SetEntryDataResult = Record & { + warnings?: ConstraintViolation[]; +}; diff --git a/src/types/setDataValidation.types.ts b/src/types/setDataValidation.types.ts new file mode 100644 index 0000000..e1a1f80 --- /dev/null +++ b/src/types/setDataValidation.types.ts @@ -0,0 +1,27 @@ +/** + * Inbound payload on `extensionEvent` when {@link SET_DATA_VALIDATION_WIRE_NAME} is used. + * Align with docs/PLAN-async-setdata-validation-extensionEvent.md. + */ +export type SetDataValidationErrorItem = { + fieldUid?: string; + message: string; + code?: string; + details?: unknown; +}; + +export type SetDataValidationEvent = { + /** Present when the host echoes correlation id; omitted in simplified validation path. */ + requestId?: string; + source: "field" | "entry"; + fieldUid?: string; + status: "error" | "success"; + errors?: SetDataValidationErrorItem[]; +}; + +/** Wire-level discriminator on `extensionEvent` (parent → iframe). */ +export const SET_DATA_VALIDATION_WIRE_NAME = "SET_DATA_VALIDATION"; + +/** + * Internal {@link wolfy87-eventemitter} event key after stale filtering — camelCase like `entrySave`, not the wire name. + */ +export const SET_DATA_VALIDATION_EMITTER_EVENT = "setDataValidation"; diff --git a/src/uiLocation.ts b/src/uiLocation.ts index 1a9ccf0..c3b4fc5 100755 --- a/src/uiLocation.ts +++ b/src/uiLocation.ts @@ -31,10 +31,15 @@ import { RegionType, } from "./types"; import { GenericObjectType } from "./types/common.types"; +import { + SET_DATA_VALIDATION_EMITTER_EVENT, + SET_DATA_VALIDATION_WIRE_NAME, +} from "./types/setDataValidation.types"; import { User } from "./types/user.types"; import { formatAppRegion, onData, onError } from "./utils/utils"; import Window from "./window"; import { dispatchApiRequest, dispatchAdapter } from "./utils/adapter"; +import { parseSetDataValidationPayload } from "./utils/setDataRequestCorrelation"; import { ContentstackEndpoints } from "./types/api.type"; const emitter = new EventEmitter(); @@ -399,6 +404,17 @@ class UiLocation { { data: event.data.data }, ]); } + + if (event.data.name === SET_DATA_VALIDATION_WIRE_NAME) { + const parsed = parseSetDataValidationPayload( + event.data.data + ); + if (parsed) { + emitter.emitEvent(SET_DATA_VALIDATION_EMITTER_EVENT, [ + parsed, + ]); + } + } }); } catch (err) { console.error("Extension Event", err); diff --git a/src/utils/setDataBridgeResponse.ts b/src/utils/setDataBridgeResponse.ts new file mode 100644 index 0000000..a876664 --- /dev/null +++ b/src/utils/setDataBridgeResponse.ts @@ -0,0 +1,77 @@ +/** + * Interprets post-robot `sendToParent("setData", ...)` results from the CMS bridge + * (app-extension-component). Shapes may be flat on `data` or nested under `data.data` + * depending on post-robot serialization. + */ + +type ResponseEnvelope = { data?: unknown }; + +function asRecord(v: unknown): Record | undefined { + return v && typeof v === "object" && !Array.isArray(v) + ? (v as Record) + : undefined; +} + +/** Host failed to resolve file/reference UIDs (CMA fetch) before applying setData. */ +export const SETDATA_RESOLUTION_ERROR_CODE = "SETDATA_RESOLUTION_ERROR" as const; + +/** Resolution failure shape returned by the CMS host when asset/entry fetch fails. */ +export function isResolutionErrorPayload(response: ResponseEnvelope): boolean { + const payload = asRecord(response?.data); + if (payload?.code === SETDATA_RESOLUTION_ERROR_CODE) return true; + const inner = asRecord(payload?.data); + return inner?.code === SETDATA_RESOLUTION_ERROR_CODE; +} + +/** Resolution error object for Promise.reject (top-level or nested under data). */ +export function getResolutionErrorPayload( + response: ResponseEnvelope +): Record { + const payload = asRecord(response?.data); + if (payload?.code === SETDATA_RESOLUTION_ERROR_CODE) { + return payload; + } + const inner = asRecord(payload?.data); + if (inner?.code === SETDATA_RESOLUTION_ERROR_CODE) { + return inner; + } + return payload ?? {}; +} + +/** Tier 1 validation failure shape from the bridge. */ +export function isValidationErrorPayload(response: ResponseEnvelope): boolean { + const payload = asRecord(response?.data); + if (!payload) return false; + if (payload.code === "VALIDATION_ERROR") return true; + const inner = asRecord(payload.data); + return inner?.code === "VALIDATION_ERROR"; +} + +/** Validation error object for Promise.reject (top-level or nested). */ +export function getValidationErrorPayload( + response: ResponseEnvelope +): Record { + const payload = asRecord(response?.data); + if (payload?.code === "VALIDATION_ERROR") { + return payload; + } + const inner = asRecord(payload?.data); + if (inner?.code === "VALIDATION_ERROR") { + return inner; + } + return payload ?? {}; +} + +/** Tier 2 warnings array if present on the bridge / host response. */ +export function getSetDataWarnings(response: ResponseEnvelope): unknown[] { + const payload = asRecord(response?.data); + if (!payload) return []; + if (Array.isArray(payload.warnings)) { + return payload.warnings as unknown[]; + } + const inner = asRecord(payload.data); + if (inner && Array.isArray(inner.warnings)) { + return inner.warnings as unknown[]; + } + return []; +} diff --git a/src/utils/setDataErrors.ts b/src/utils/setDataErrors.ts new file mode 100644 index 0000000..08d16ef --- /dev/null +++ b/src/utils/setDataErrors.ts @@ -0,0 +1,54 @@ +/** + * Typed errors for setData / setEntryData failures returned from the CMS bridge + * (app-extension-component). The bridge may ACK success while embedding + * VALIDATION_ERROR or SETDATA_RESOLUTION_ERROR in the payload; the SDK turns + * those into real Error instances so callers get clear stack traces and + * `instanceof` checks work after `await` / `.catch`. + */ + +const VALIDATION_CODE = "VALIDATION_ERROR" as const; +const RESOLUTION_CODE = "SETDATA_RESOLUTION_ERROR" as const; + +export class SetDataValidationError extends Error { + readonly code: typeof VALIDATION_CODE = VALIDATION_CODE; + readonly details: unknown; + + constructor(message: string, details?: unknown) { + super(message); + this.name = "SetDataValidationError"; + this.details = details; + Object.setPrototypeOf(this, new.target.prototype); + } + + static fromBridgePayload( + payload: Record + ): SetDataValidationError { + const message = + typeof payload.message === "string" + ? payload.message + : "setData validation failed"; + return new SetDataValidationError(message, payload.details); + } +} + +export class SetDataResolutionError extends Error { + readonly code: typeof RESOLUTION_CODE = RESOLUTION_CODE; + readonly failures: unknown; + + constructor(message: string, failures?: unknown) { + super(message); + this.name = "SetDataResolutionError"; + this.failures = failures; + Object.setPrototypeOf(this, new.target.prototype); + } + + static fromBridgePayload( + payload: Record + ): SetDataResolutionError { + const message = + typeof payload.message === "string" + ? payload.message + : "setData resolution failed"; + return new SetDataResolutionError(message, payload.failures); + } +} diff --git a/src/utils/setDataRequestCorrelation.ts b/src/utils/setDataRequestCorrelation.ts new file mode 100644 index 0000000..9f90a55 --- /dev/null +++ b/src/utils/setDataRequestCorrelation.ts @@ -0,0 +1,65 @@ +import type { + SetDataValidationErrorItem, + SetDataValidationEvent, +} from "../types/setDataValidation.types"; + +export function parseSetDataValidationPayload( + raw: unknown +): SetDataValidationEvent | null { + if (!raw || typeof raw !== "object") { + return null; + } + const o = raw as Record; + const requestIdRaw = o.requestId; + if (requestIdRaw !== undefined && requestIdRaw !== null) { + if (typeof requestIdRaw !== "string" || requestIdRaw.length === 0) { + return null; + } + } + const source = o.source; + const status = o.status; + if (source !== "field" && source !== "entry") { + return null; + } + if (status !== "error" && status !== "success") { + return null; + } + if (status === "error") { + const errors = o.errors; + if (!Array.isArray(errors) || errors.length === 0) { + return null; + } + for (const item of errors) { + if ( + !item || + typeof item !== "object" || + typeof (item as { message?: unknown }).message !== "string" + ) { + return null; + } + } + } else { + const errors = o.errors; + if (Array.isArray(errors) && errors.length > 0) { + return null; + } + } + if (o.fieldUid !== undefined && typeof o.fieldUid !== "string") { + return null; + } + + const parsed: SetDataValidationEvent = { + source, + status, + }; + if (typeof requestIdRaw === "string" && requestIdRaw.length > 0) { + parsed.requestId = requestIdRaw; + } + if (typeof o.fieldUid === "string") { + parsed.fieldUid = o.fieldUid; + } + if (status === "error") { + parsed.errors = o.errors as SetDataValidationErrorItem[]; + } + return parsed; +} diff --git a/src/window.ts b/src/window.ts index e57d8a0..05d7b2a 100755 --- a/src/window.ts +++ b/src/window.ts @@ -33,6 +33,11 @@ class Window { this.type = type; this.state = state; this._emitter = emitter; + + this.enableResizing = this.enableResizing.bind(this); + this.updateHeight = this.updateHeight.bind(this); + this.enableAutoResizing = this.enableAutoResizing.bind(this); + this.disableAutoResizing = this.disableAutoResizing.bind(this); } /** @@ -112,7 +117,7 @@ class Window { } this._autoResizingEnabled = true; //@ts-ignore - observer = new MutationObserver(this.updateHeight.bind(this)); + observer = new MutationObserver(this.updateHeight); observer.observe(window.document.body, config); return this; }