From b49813ea337aa58cb08947dbea276c0de9c5a776 Mon Sep 17 00:00:00 2001 From: Jannis Leifeld Date: Tue, 13 Jun 2023 16:02:55 +0200 Subject: [PATCH] NEXT-28243 - improve dataset handling with selector --- e2e/channel.spec.ts | 48 +++++++- e2e/testpage/app.ts | 13 ++ jest.config.js | 2 +- package-lock.json | 28 ++--- src/_internals/sdkVersion.js | 7 ++ src/_internals/validator/index.ts | 11 +- src/channel.spec.ts | 47 +++++++- src/channel.ts | 127 ++++++++++++++++---- src/data/_internals/selectData.ts | 76 ++++++++++++ src/data/composables/useSharedState.spec.ts | 27 ++++- src/data/index.ts | 59 +++++++-- src/messages.types.ts | 5 +- src/privileges/privilege-resolver.ts | 12 +- tsconfig.json | 3 +- vite.config.ts | 2 +- 15 files changed, 406 insertions(+), 61 deletions(-) create mode 100644 src/_internals/sdkVersion.js create mode 100644 src/data/_internals/selectData.ts diff --git a/e2e/channel.spec.ts b/e2e/channel.spec.ts index 055df8a2..9bc9435c 100644 --- a/e2e/channel.spec.ts +++ b/e2e/channel.spec.ts @@ -585,7 +585,7 @@ test.describe('Privilege tests', () => { await mainFrame.evaluate(() => { window.sw_internal.setExtensions({ - foo: { + example: { baseUrl: 'http://localhost:8182', permissions: { read: ['product'] @@ -651,7 +651,7 @@ test.describe('Privilege tests', () => { } }); - expect(response.errorMessage).toEqual(`Error: Your app is missing the privileges create:product, delete:product, update:product, create:product, delete:product, update:product, create:manufacturer, delete:manufacturer, read:manufacturer, update:manufacturer for action "_collectionTest".`); + expect(response.errorMessage).toEqual(`Error: Your app is missing the privileges create:product, delete:product, update:product, create:manufacturer, delete:manufacturer, read:manufacturer, update:manufacturer for action "_collectionTest".`); expect(response.isMissingPrivilesErrorInstance).toBe(true); }); @@ -660,7 +660,7 @@ test.describe('Privilege tests', () => { await mainFrame.evaluate(() => { window.sw_internal.setExtensions({ - foo: { + example: { baseUrl: 'http://localhost:8182', permissions: { read: ['product'] @@ -727,7 +727,7 @@ test.describe('Privilege tests', () => { // publish dataset await mainFrame.evaluate(async () => { window.sw_internal.setExtensions({ - foo: { + example: { baseUrl: 'http://localhost:8182', permissions: { read: ['manufacturer'] @@ -775,7 +775,7 @@ test.describe('Privilege tests', () => { // publish dataset await mainFrame.evaluate(async () => { window.sw_internal.setExtensions({ - foo: { + example: { baseUrl: 'http://localhost:8182', permissions: { read: ['product'] @@ -831,7 +831,7 @@ test.describe('data handling', () => { expect(data['e2e-test']).toBe('test-string'); }); - test('dataset subscriber', async ({ page }) => { + test('dataset subscriber without selector', async ({ page }) => { const { mainFrame, subFrame } = await setup({ page }); // subscribe to dataset publish @@ -860,6 +860,42 @@ test.describe('data handling', () => { expect(data).toBe('test-string'); }); + test('dataset subscriber with selector', async ({ page }) => { + const { mainFrame, subFrame } = await setup({ page }); + + // subscribe to dataset publish + await subFrame.evaluate(async () => { + return await window.sw.data.subscribe('product_detail', (data) => { + // @ts-expect-error + window.result = { data: data.data }; + }, { + selectors: ['name'] + }); + }) + + // publish dataset + await mainFrame.evaluate(async () => { + await window.sw.data.register({ + id: 'product_detail', + data: { + name: 'T-Shirt', + description: 'An awesome T-Shirt' + }, + }); + }) + + // get receiving value + const { data } = await subFrame.evaluate(() => { + // @ts-expect-error + return window.result; + }) + + // check if receiving value matches + expect(data).toEqual({ + name: 'T-Shirt', + }); + }); + test('dataset update', async ({ page }) => { const { mainFrame, subFrame } = await setup({ page }); diff --git a/e2e/testpage/app.ts b/e2e/testpage/app.ts index 95744194..5f91d8a7 100644 --- a/e2e/testpage/app.ts +++ b/e2e/testpage/app.ts @@ -33,3 +33,16 @@ window.sw_internal = { Entity: EntityClass, MissingPrivilegesError: MissingPrivilegesError, } + + +window.sw_internal.setExtensions({ + example: { + baseUrl: 'http://localhost:8182', + permissions: { + create: ['test', 'foo', 'product'], + update: ['test', 'foo', 'product'], + delete: ['test', 'foo', 'product'], + read: ['test', 'foo', 'product'], + } + }, +}); \ No newline at end of file diff --git a/jest.config.js b/jest.config.js index 5770ed99..d82d3289 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,6 +1,6 @@ /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ module.exports = { - preset: 'ts-jest', + preset: 'ts-jest/presets/js-with-ts', testEnvironment: 'jsdom', globals: { 'ts-jest': { diff --git a/package-lock.json b/package-lock.json index 6dadd29a..abc3de02 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@shopware-ag/admin-extension-sdk", - "version": "3.0.8", + "version": "3.0.10", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@shopware-ag/admin-extension-sdk", - "version": "3.0.8", + "version": "3.0.10", "license": "MIT", "dependencies": { "localforage": "^1.10.0", @@ -2419,9 +2419,9 @@ "dev": true }, "node_modules/acorn": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz", - "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==", + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.9.0.tgz", + "integrity": "sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -5069,9 +5069,9 @@ "dev": true }, "node_modules/fast-glob": { - "version": "3.2.11", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", - "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", + "version": "3.2.12", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", + "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", "dev": true, "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -13296,9 +13296,9 @@ "dev": true }, "acorn": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz", - "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==", + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.9.0.tgz", + "integrity": "sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ==", "dev": true }, "acorn-globals": { @@ -15179,9 +15179,9 @@ "dev": true }, "fast-glob": { - "version": "3.2.11", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", - "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", + "version": "3.2.12", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", + "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", "dev": true, "requires": { "@nodelib/fs.stat": "^2.0.2", diff --git a/src/_internals/sdkVersion.js b/src/_internals/sdkVersion.js new file mode 100644 index 00000000..bbb87176 --- /dev/null +++ b/src/_internals/sdkVersion.js @@ -0,0 +1,7 @@ +/** + * JS file is needed because TypeScript can't use JSON imports + * for UMD builds. + */ +import { version } from '../../package.json'; + +export default version; diff --git a/src/_internals/validator/index.ts b/src/_internals/validator/index.ts index 2cc9b56c..a2741381 100644 --- a/src/_internals/validator/index.ts +++ b/src/_internals/validator/index.ts @@ -22,6 +22,7 @@ export default function validate({ const extension = findExtensionByBaseUrl(origin); if (!extension) { + console.warn(`No extension found for origin ${origin}`); return null; } @@ -32,11 +33,17 @@ export default function validate({ if (key === '__type__' && ['__EntityCollection__', '__Entity__'].includes(value as string)) { const entityName = parentEntry.__entityName__ as string; + if (!entityName) { + return; + } + [...privilegesToCheck].sort().forEach(privilege => { const permissionsForPrivilege = extension.permissions[privilege]; if ( - !permissionsForPrivilege || - !permissionsForPrivilege.includes(entityName) + (!permissionsForPrivilege || + !permissionsForPrivilege.includes(entityName)) + && + !privilegeErrors.includes(`${privilege}:${entityName}`) ) { privilegeErrors.push(`${privilege}:${entityName}`); } diff --git a/src/channel.spec.ts b/src/channel.spec.ts index 6cb17ad9..d7b76b08 100644 --- a/src/channel.spec.ts +++ b/src/channel.spec.ts @@ -1,11 +1,56 @@ import flushPromises from 'flush-promises'; -import { send, handle, createSender, createHandler, subscribe, publish, setExtensions } from './channel'; +import type { + send as sendType, + handle as handleType, + createSender as createSenderType, + createHandler as createHandlerType, + subscribe as subscribeType, + publish as publishType, + setExtensions as setExtensionsType, +} from './channel'; import MissingPrivilegesError from './privileges/missing-privileges-error'; // Channel send timout + 1000 jest.setTimeout(8000); +let send: typeof sendType; +let handle: typeof handleType; +let createSender: typeof createSenderType; +let createHandler: typeof createHandlerType; +let subscribe: typeof subscribeType; +let publish: typeof publishType; +let setExtensions: typeof setExtensionsType; + describe('Test the channel bridge from iFrame to admin', () => { + beforeAll(async () => { + window.addEventListener('message', (event: MessageEvent) => { + if (event.origin === '') { + event.stopImmediatePropagation(); + const eventWithOrigin: MessageEvent = new MessageEvent('message', { + data: event.data, + origin: window.location.href, + }); + window.dispatchEvent(eventWithOrigin); + } + }); + + const channel = await import('./channel'); + send = channel.send; + handle = channel.handle; + createSender = channel.createSender; + createHandler = channel.createHandler; + subscribe = channel.subscribe; + publish = channel.publish; + setExtensions = channel.setExtensions; + + setExtensions({ + 'test-extension': { + baseUrl: 'http://localhost', + permissions: {}, + }, + }); + }); + beforeEach(() => { // reset extensions setExtensions({}); diff --git a/src/channel.ts b/src/channel.ts index 67a023b7..eee06925 100644 --- a/src/channel.ts +++ b/src/channel.ts @@ -8,6 +8,11 @@ import MissingPrivilegesError from './privileges/missing-privileges-error'; import SerializerFactory from './_internals/serializer'; import createError from './_internals/error-handling/error-factory'; import validate from './_internals/validator/index'; +import type { datasetRegistration } from './data'; +import { selectData } from './data/_internals/selectData'; +import sdkVersion from './_internals/sdkVersion'; + +const packageVersion = sdkVersion as string; const { serialize, deserialize } = SerializerFactory({ handle: handle, @@ -18,10 +23,18 @@ export type extensions = { [key: string]: extension, } -export let adminExtensions: extensions = {}; +export const adminExtensions: extensions = {}; export function setExtensions(extensions: extensions): void { - adminExtensions = extensions; + Object.entries(extensions).forEach(([key, value]) => { + // @ts-expect-error = we fill up the values later + adminExtensions[key] = {}; + + Object.entries(value).forEach(([valueKey, valueContent]) => { + // @ts-expect-error = we defined the key beforehand + adminExtensions[key][valueKey] = valueContent; + }); + }); } /** @@ -64,6 +77,14 @@ export type ShopwareMessageResponseData = new Set(); + +const subscriberRegistry: Set<{ + id: string, + selectors: string[] | undefined, + source: Window, + origin: string, }> = new Set(); /** @@ -367,10 +388,19 @@ export function handle export function publish( type: MESSAGE_TYPE, data: ShopwareMessageTypes[MESSAGE_TYPE]['responseType'], + sources: { + source: Window, + origin: string, + sdkVersion: string | undefined, + }[] = [...sourceRegistry].map(({source, origin, sdkVersion}) => ({ + source, + origin, + sdkVersion, + })) ) :void { - [...sourceRegistry].forEach(({source, origin}) => { + sources.forEach(({ source, origin }) => { // Disable error handling because not every window need to react to the data // eslint-disable-next-line @typescript-eslint/no-empty-function return send(type, data, source, origin).catch(() => {}); @@ -421,7 +451,9 @@ export function createSender */ export function createHandler(messageType: MESSAGE_TYPE) { // eslint-disable-next-line @typescript-eslint/explicit-function-return-type - return (method: (data: MessageDataType) => Promise | ShopwareMessageTypes[MESSAGE_TYPE]['responseType']) => { + return (method: (data: MessageDataType, additionalInformation: { + _event_: MessageEvent, + }) => Promise | ShopwareMessageTypes[MESSAGE_TYPE]['responseType']) => { return handle(messageType, method); }; } @@ -457,7 +489,7 @@ const datasets = new Map(); (async (): Promise => { // Handle registrations at current window - handle('__registerWindow__', (_, additionalOptions) => { + handle('__registerWindow__', ({ sdkVersion }, additionalOptions) => { let source: Window | undefined; let origin: string | undefined; @@ -472,35 +504,82 @@ const datasets = new Map(); sourceRegistry.add({ source, origin, + sdkVersion, }); + }); - // Register all existing datasets for apps that come to late for the "synchronous" registration - datasets.forEach((dataset, id ) => { - // eslint-disable-next-line @typescript-eslint/no-empty-function - send('datasetSubscribe', {id, data: dataset}, source, origin).catch(() => {}); + handle('datasetSubscribeRegistration', (data, { _event_ }) => { + let source: Window | undefined; + let origin: string | undefined; + + if (_event_.source) { + source = _event_.source as Window; + origin = _event_.origin; + } else { + source = window; + origin = window.origin; + } + + subscriberRegistry.add({ + id: data.id, + source: source, + origin: origin, + selectors: data.selectors, }); - }); - // New dataset registered - handle('datasetRegistration', (data) => { - datasets.set(data.id, data.data); + // When initial data exists directly send it to the subscriber + const dataset = datasets.get(data.id); - publish('datasetSubscribe', data); + if (dataset) { + const selectedData = selectData(dataset, data.selectors, 'datasetSubscribe', origin); - return { - id: data.id, - data: data.data, - }; - }); + if (selectedData instanceof MissingPrivilegesError) { + console.error(selectedData); + return; + } - handle('datasetSubscribe', (data) => { - return datasets.get(data.id) ?? null; + void send('datasetSubscribe', { + id: data.id, + data: selectedData, + selectors: data.selectors, + }, source, origin); + } }); // Register at parent window - await send('__registerWindow__', {}); + await send('__registerWindow__', { + sdkVersion: packageVersion, + }); })().catch((e) => console.error(e)); +// New dataset registered +export async function processDataRegistration(data: Omit): Promise { + datasets.set(data.id, data.data); + + // Only publish whole data to sources that don't have a sdkVersion (for backwards compatibility) + publish('datasetSubscribe', data, [ + ...[...sourceRegistry].filter(({ sdkVersion }) => !sdkVersion), + ]); + + // Publish selected data to sources that are inside the subscriberRegistry + subscriberRegistry.forEach(({ id, selectors, source, origin }) => { + if (id !== data.id) { + return; + } + + const selectedData = selectData(data.data, selectors, 'datasetSubscribe', origin); + + if (selectedData instanceof MissingPrivilegesError) { + console.error(selectedData); + } + + // eslint-disable-next-line @typescript-eslint/no-empty-function + send('datasetSubscribe', { id, data: selectedData, selectors }, source, origin).catch(() => {}); + }); + + return Promise.resolve(); +} + /** * Add utils to global window object for * testing @@ -509,14 +588,18 @@ const datasets = new Map(); interface Window { _swsdk: { sourceRegistry: typeof sourceRegistry, + subscriberRegistry: typeof subscriberRegistry, datasets: typeof datasets, + adminExtensions: typeof adminExtensions, }, } } window._swsdk = { sourceRegistry, + subscriberRegistry, datasets, + adminExtensions, }; /** diff --git a/src/data/_internals/selectData.ts b/src/data/_internals/selectData.ts new file mode 100644 index 00000000..63ed28c0 --- /dev/null +++ b/src/data/_internals/selectData.ts @@ -0,0 +1,76 @@ +import { toPath, get } from 'lodash'; +import type { ShopwareMessageTypes } from '../../messages.types'; +import MissingPrivilegesError from '../../privileges/missing-privileges-error'; +import type { privilegeString } from '../../privileges/privilege-resolver'; +import { findExtensionByBaseUrl } from '../../privileges/privilege-resolver'; + +export function selectData( + sourceData: unknown, + selectors?: string[], + messageType: keyof ShopwareMessageTypes = 'datasetSubscribe', + origin?: string, +): unknown { + const extension = findExtensionByBaseUrl(origin ?? ''); + const permissionErrors: Array = []; + + if (!selectors) { + return sourceData; + } + + const selectedData = selectors.reduce<{ + [key: string]: unknown, + }>((acc, selector) => { + // Check permissions for path entries + ['', ...toPath(selector)].forEach((path) => { + const value = path !== '' ? get(sourceData, path) as unknown : sourceData; + let entityName = ''; + + // @ts-expect-error - we just check if the value is an entity + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + if (value && value.__identifier__ && value.__identifier__() === 'Entity') { + // @ts-expect-error - we know that the value is an entity + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call + entityName = value.getEntityName(); + } + + // @ts-expect-error - we just check if the value is an entityCollection + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + if (value && value.__identifier__ && value.__identifier__() === 'EntityCollection') { + // @ts-expect-error - we know that the value is an entityCollection + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call + entityName = value.entity; + } + + if (!entityName) { + return; + } + + const permissionsToCheck = ['read'] as const; + + permissionsToCheck.forEach((privilege) => { + const permissionsForPrivilege = extension?.permissions[privilege]; + + if ( + !permissionsForPrivilege || + !permissionsForPrivilege.includes(entityName) + ) { + permissionErrors.push(`${privilege}:${entityName}`); + } + }); + }); + + const value = get(sourceData, selector) as unknown; + + if (value !== undefined) { + acc[selector] = value; + } + + return acc; + }, {}); + + if (permissionErrors.length) { + return new MissingPrivilegesError(messageType, permissionErrors); + } + + return selectedData; +} diff --git a/src/data/composables/useSharedState.spec.ts b/src/data/composables/useSharedState.spec.ts index ab1099df..921f124f 100644 --- a/src/data/composables/useSharedState.spec.ts +++ b/src/data/composables/useSharedState.spec.ts @@ -1,4 +1,4 @@ -import { useSharedState } from './useSharedState'; +import type { useSharedState as useSharedStateType } from './useSharedState'; import { BroadcastChannel } from 'worker_threads'; import Vue from 'vue'; import flushPromises from 'flush-promises'; @@ -7,6 +7,8 @@ import localforage from 'localforage'; Vue.config.devtools = false; Vue.config.productionTip = false; +let useSharedState: typeof useSharedStateType; + function mockLoadComposableInApp(composable: () => any) { let result: any; @@ -29,6 +31,29 @@ describe('useSharedState composable', () => { storeName: 'persistentSharedValueStore', }); + beforeAll(async () => { + window.addEventListener('message', (event: MessageEvent) => { + if (event.origin === '') { + event.stopImmediatePropagation(); + const eventWithOrigin: MessageEvent = new MessageEvent('message', { + data: event.data, + origin: window.location.href, + }); + window.dispatchEvent(eventWithOrigin); + } + }); + + useSharedState = await (await import('./useSharedState')).useSharedState; + const setExtensions = await (await import('../../channel')).setExtensions; + + setExtensions({ + 'test-extension': { + baseUrl: 'http://localhost', + permissions: {}, + }, + }); + }) + beforeEach(async () => { // @ts-expect-error - Mocking BroadcastChannel global.BroadcastChannel = BroadcastChannel; diff --git a/src/data/index.ts b/src/data/index.ts index 76fc5b28..5a909f86 100644 --- a/src/data/index.ts +++ b/src/data/index.ts @@ -1,4 +1,4 @@ -import { createHandler, createSender, subscribe as createSubscriber } from '../channel'; +import { createHandler, createSender, processDataRegistration, send, subscribe as createSubscriber } from '../channel'; import Criteria from './Criteria'; import Entity from './_internals/Entity'; import EntityCollection from './_internals/EntityCollection'; @@ -6,16 +6,39 @@ import repository from './repository'; // Internal function to create a filterable subscriber function createFilteredSubscriber(type: 'datasetSubscribe' | 'datasetUpdate') { - return (id: string, callback: (data: {id: string, data: unknown}) => void | Promise): unknown => { - const wrapper = (data: {id: string, data: unknown}): void => { - if (data && data.id === id) { - const returnValue = callback(data); - - if (returnValue) { - // eslint-disable-next-line @typescript-eslint/no-empty-function - returnValue.catch(() => {}); + return ( + id: string, + callback: (data: {id: string, data: unknown}) => void | Promise, + options?: { + selectors?: string[], + } + ): unknown => { + if (type === 'datasetSubscribe') { + // Send message to admin that this window wants to subscribe to a dataset + void send('datasetSubscribeRegistration', { + id, + selectors: options?.selectors, + }); + } + + const wrapper = (data: {id: string, data: unknown, selectors?: string[]}): void => { + if (data?.id !== id) { + return; + } + + if (data.selectors && data.selectors.length > 0) { + // Compare if the selectors match independent of the order + if (options?.selectors?.sort().join(',') !== data.selectors.sort().join(',')) { + return; } } + + const returnValue = callback(data); + + if (returnValue) { + // eslint-disable-next-line @typescript-eslint/no-empty-function + returnValue.catch(() => {}); + } }; return createSubscriber(type, wrapper as (data: unknown) => void | Promise); @@ -32,7 +55,7 @@ export const update = createSender('datasetUpdate'); /** * Internal methods used by the administration */ -export const register = createSender('datasetRegistration'); +export const register = processDataRegistration; export const updateSubscriber = createFilteredSubscriber('datasetUpdate'); export const handleGet = createHandler('datasetGet'); @@ -54,6 +77,20 @@ export type datasetSubscribe = { id: string, data: unknown, + + selectors?: string[], +} + +/** + * Will be used for giving the admin the information that + * a window wants to subscribe to a dataset + */ +export type datasetSubscribeRegistration = { + responseType: unknown, + + id: string, + + selectors?: string[], } export type datasetUpdate = { @@ -70,6 +107,8 @@ export type datasetGet = { id: string, data?: unknown, + + selectors?: string[], } const Classes: { diff --git a/src/messages.types.ts b/src/messages.types.ts index 5ee8d38d..c84c2b9b 100644 --- a/src/messages.types.ts +++ b/src/messages.types.ts @@ -14,7 +14,7 @@ import type { uiModalOpen, uiModalClose } from './ui/modal/index'; import type { actionButtonAdd } from './ui/actionButton'; import type { actionExecute } from './app/action'; import type Criteria from './data/Criteria'; -import type { datasetRegistration, datasetUpdate, datasetGet, datasetSubscribe } from './data'; +import type { datasetRegistration, datasetUpdate, datasetGet, datasetSubscribe, datasetSubscribeRegistration } from './data'; import type EntityCollection from './data/_internals/EntityCollection'; import type { Entity } from './data/_internals/Entity'; import type { @@ -72,6 +72,7 @@ export interface ShopwareMessageTypes { /* eslint-enable @typescript-eslint/no-explicit-any */ datasetRegistration: datasetRegistration, datasetSubscribe: datasetSubscribe, + datasetSubscribeRegistration: datasetSubscribeRegistration, datasetUpdate: datasetUpdate, datasetGet: datasetGet, __function__: __function__, @@ -153,4 +154,6 @@ export type __function__ = { export type __registerWindow__ = { responseType: void, + + sdkVersion: string, } diff --git a/src/privileges/privilege-resolver.ts b/src/privileges/privilege-resolver.ts index 9feb4434..b2bc9f52 100644 --- a/src/privileges/privilege-resolver.ts +++ b/src/privileges/privilege-resolver.ts @@ -73,6 +73,16 @@ function getMissingPrivileges(requiredPrivileges: privileges, privileges: privil } export function findExtensionByBaseUrl(baseUrl: string): extension | undefined { + if (typeof baseUrl !== 'string') { + return undefined; + } + + const comparedBaseUrl = new URL(baseUrl); + return Object.values(adminExtensions) - .find((ext) => ext.baseUrl === baseUrl); + .find((ext) => { + const extensionBaseUrl = new URL(ext.baseUrl); + + return extensionBaseUrl.hostname === comparedBaseUrl.hostname; + }); } diff --git a/tsconfig.json b/tsconfig.json index 42476402..297acf3b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,7 +13,8 @@ "noUnusedParameters": true, "noImplicitReturns": true, "outDir": "./lib", - "declaration": true + "declaration": true, + "allowJs": true }, "include": ["src"] } diff --git a/vite.config.ts b/vite.config.ts index 3a2a305b..e12b703b 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -9,7 +9,7 @@ module.exports = defineConfig(({ command, mode }) => { return { root: resolve(__dirname, './e2e/testpage'), plugins: [ - tsconfigPaths() + tsconfigPaths(), ], build: { outDir: resolve(__dirname, 'testpageDist'),