Skip to content

Commit

Permalink
[Feature] [EDT-1714] setDataRow (#539)
Browse files Browse the repository at this point in the history
* [Feature] Add data source methods

* [Feature] Add DataSourceController

* [Feature] Make waitToBeReady public (again)

* [EDT-1714] mapping tools separated

* [EDT-1714] feat: mapping tools separated

* [EDT-1714] feat: mapping tools separated

* [EDT-1714] setDataRow method added to DataSourceController.ts

* [EDT-1714] subscription onDataRowChanged added

* [Feature] Expose data source

* [EDT-1714] conflicts

* [EDT-1714] export DataConnectorTypes

* [EDT-1714] subscription onDataRowChanged dropped

* [EDT-1714] better comments for DataItemMappingTools

---------

Co-authored-by: Laurent Raymond <1528484+Dvergar@users.noreply.github.com>
  • Loading branch information
evan-mcgeek and Dvergar authored Nov 28, 2024
1 parent 8de043c commit 9079986
Show file tree
Hide file tree
Showing 7 changed files with 152 additions and 57 deletions.
54 changes: 7 additions & 47 deletions packages/sdk/src/controllers/DataConnectorController.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ConnectorConfigOptions, EditorAPI, EditorResponse, MetaData } from '../types/CommonTypes';
import { getEditorResponseData } from '../utils/EditorResponseData';
import { DataConnectorCapabilities, DataItem, DataModel, DataPage, PageConfig } from '../types/DataConnectorTypes';
import { DataItemMappingTools, EngineDataItem } from '../utils/DataItemMappingTools';

/**
* The DataConnectorController is responsible for all communication regarding Data connectors.
Expand All @@ -20,12 +21,14 @@ export class DataConnectorController {
* @ignore
*/
#editorAPI: EditorAPI;
#dataItemMappingTools: DataItemMappingTools;

/**
* @ignore
*/
constructor(editorAPI: EditorAPI) {
constructor(editorAPI: EditorAPI, dataItemMappingTools: DataItemMappingTools) {
this.#editorAPI = editorAPI;
this.#dataItemMappingTools = dataItemMappingTools;
}

/**
Expand All @@ -49,7 +52,9 @@ export class DataConnectorController {
const update: EditorResponse<DataPage<DataItem>> = { ...resp, parsedData: null };
if (resp.parsedData) {
update.parsedData = {
data: resp.parsedData.data.map((e: EngineDataItem) => this.mapEngineToDataItem(e)),
data: resp.parsedData.data.map((e: EngineDataItem) =>
this.#dataItemMappingTools.mapEngineToDataItem(e),
),
continuationToken: resp.parsedData.continuationToken,
};
}
Expand Down Expand Up @@ -96,49 +101,4 @@ export class DataConnectorController {
.dataConnectorGetCapabilities(connectorId)
.then((result) => getEditorResponseData<DataConnectorCapabilities>(result));
};

/**
* Check if the value is a DatePropertyWrapper with the type guard
* @param value a dynamic value which
* @returns boolean value.
*/
private isDatePropertyWrapper(
value: string | number | boolean | DatePropertyWrapper | null,
): value is DatePropertyWrapper {
return typeof value === 'object' && value?.type === 'date';
}

/**
* Transforms an InternalDataItem into a DataItem.
*
* Converts DatePropertyWrapper values to JavaScript Date objects
* while keeping other values unchanged.
*
* @param dataItem the EngineDataItem to transform.
* @returns the resulting DataItem with parsed date properties.
*/
private mapEngineToDataItem(dataItem: EngineDataItem): DataItem {
const parsedItem: DataItem = {};

Object.entries(dataItem).forEach(([key, value]) => {
parsedItem[key] = this.isDatePropertyWrapper(value) ? new Date(value.value) : value;
});

return parsedItem;
}
}

/**
* Internal type to wrap the date object value on the engine side.
*/
type DatePropertyWrapper = {
value: number;
type: 'date';
};

/**
* Engine data item type.
*/
type EngineDataItem = {
[key: string]: string | number | boolean | DatePropertyWrapper | null;
};
18 changes: 17 additions & 1 deletion packages/sdk/src/controllers/DataSourceController.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { EditorAPI } from '../types/CommonTypes';
import { getEditorResponseData } from '../utils/EditorResponseData';
import { ConnectorInstance } from '../next';
import { DataItem } from '../types/DataConnectorTypes';
import { DataItemMappingTools } from '../utils/DataItemMappingTools';

/**
* The DataSourceController is responsible for all communication regarding data source.
Expand All @@ -16,12 +18,14 @@ export class DataSourceController {
* @ignore
*/
#editorAPI: EditorAPI;
#dataItemMappingTools: DataItemMappingTools;

/**
* @ignore
*/
constructor(editorAPI: EditorAPI) {
constructor(editorAPI: EditorAPI, dataItemMappingTools: DataItemMappingTools) {
this.#editorAPI = editorAPI;
this.#dataItemMappingTools = dataItemMappingTools;
}

/**
Expand Down Expand Up @@ -51,4 +55,16 @@ export class DataSourceController {
const res = await this.#editorAPI;
return res.removeDataSource().then((result) => getEditorResponseData<null>(result));
};

/**
* Maps the data row values to variables by names (data row keys).
* Variables must exist.
* @param dataRow DataItem to set
* @returns
*/
setDataRow = async (dataRow: DataItem) => {
const res = await this.#editorAPI;
const engineDataItem = this.#dataItemMappingTools.mapDataItemToEngine(dataRow);
return res.setDataRow(JSON.stringify(engineDataItem)).then((result) => getEditorResponseData<null>(result));
};
}
5 changes: 3 additions & 2 deletions packages/sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export type {
LayoutWithFrameProperties,
LayoutListItemType,
Layout,
LayoutPreset
LayoutPreset,
} from './types/LayoutTypes';
export type {
FrameLayoutType,
Expand All @@ -59,7 +59,7 @@ export type {
StartFrameAnchor,
EndFrameAnchor,
StartAndEndFrameAnchor,
CenterFrameAnchor
CenterFrameAnchor,
} from './types/FrameTypes';
export { PageAnchorTarget, FrameAnchorTarget } from './types/FrameTypes';

Expand Down Expand Up @@ -141,6 +141,7 @@ export { ColorType } from './types/ColorStyleTypes';
export * from './types/MediaConnectorTypes';
export * from './types/FontConnectorTypes';
export * from './types/ConnectorTypes';
export * from './types/DataConnectorTypes';

export { WellKnownConfigurationKeys } from './types/ConfigurationTypes';

Expand Down
10 changes: 6 additions & 4 deletions packages/sdk/src/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import { NextSubscribers } from './next';
import { LocalConfigurationDecorator } from './utils/LocalConfigurationDecorator';
import { ConfigurationController } from './controllers/ConfigurationController';
import { DataConnectorController } from './controllers/DataConnectorController';
import { DataItemMappingTools } from './utils/DataItemMappingTools';
import { DataSourceController } from './controllers/DataSourceController';

let connection: Connection;
Expand Down Expand Up @@ -89,6 +90,7 @@ export class SDK {
private subscriber: SubscriberController;
private enabledNextSubscribers: NextSubscribers | undefined;
private localConfig = new Map<string, string>();
private dataItemMappingTools = new DataItemMappingTools();

/**
* The SDK should be configured clientside and it exposes all controllers to work with in other applications
Expand All @@ -110,8 +112,8 @@ export class SDK {
this.connector = new ConnectorController(this.editorAPI, this.localConfig);
this.mediaConnector = new MediaConnectorController(this.editorAPI);
this.fontConnector = new FontConnectorController(this.editorAPI);
this.dataConnector = new DataConnectorController(this.editorAPI);
this.dataSource = new DataSourceController(this.editorAPI);
this.dataConnector = new DataConnectorController(this.editorAPI, this.dataItemMappingTools);
this.dataSource = new DataSourceController(this.editorAPI, this.dataItemMappingTools);
this.animation = new AnimationController(this.editorAPI);
this.document = new DocumentController(this.editorAPI);

Expand Down Expand Up @@ -211,8 +213,8 @@ export class SDK {
this.characterStyle = new CharacterStyleController(this.editorAPI);
this.mediaConnector = new MediaConnectorController(this.editorAPI);
this.fontConnector = new FontConnectorController(this.editorAPI);
this.dataConnector = new DataConnectorController(this.editorAPI);
this.dataSource = new DataSourceController(this.editorAPI);
this.dataConnector = new DataConnectorController(this.editorAPI, this.dataItemMappingTools);
this.dataSource = new DataSourceController(this.editorAPI, this.dataItemMappingTools);
this.connector = new ConnectorController(this.editorAPI, this.localConfig);
this.variable = new VariableController(this.editorAPI);
this.font = new FontController(this.editorAPI);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { DataItem, DataPage, PageConfig } from '../../types/DataConnectorTypes';
import { EditorAPI, EditorResponse } from '../../types/CommonTypes';
import { castToEditorResponse, getEditorResponseData } from '../../utils/EditorResponseData';
import { DataConnectorController } from '../../controllers/DataConnectorController';
import { DataItemMappingTools } from '../../utils/DataItemMappingTools';

let mockedDataConnectorController: DataConnectorController;

Expand All @@ -12,8 +13,10 @@ const mockedEditorApi: EditorAPI = {
dataConnectorGetConfigurationOptions: async () => getEditorResponseData(castToEditorResponse(null)),
};

const mockedDataItemMappingTools = new DataItemMappingTools();

beforeEach(() => {
mockedDataConnectorController = new DataConnectorController(mockedEditorApi);
mockedDataConnectorController = new DataConnectorController(mockedEditorApi, mockedDataItemMappingTools);
jest.spyOn(mockedEditorApi, 'dataConnectorGetPage');
jest.spyOn(mockedEditorApi, 'dataConnectorGetModel');
jest.spyOn(mockedEditorApi, 'dataConnectorGetCapabilities');
Expand Down
34 changes: 32 additions & 2 deletions packages/sdk/src/tests/controllers/DataSourceController.test.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
import { EditorAPI } from '../../types/CommonTypes';
import { getEditorResponseData, castToEditorResponse } from '../../utils/EditorResponseData';
import { castToEditorResponse, getEditorResponseData } from '../../utils/EditorResponseData';
import { DataSourceController } from '../../controllers/DataSourceController';
import { DataItemMappingTools, EngineDataItem } from '../../utils/DataItemMappingTools';

let mockedDataSourceController: DataSourceController;

const mockEditorApi: EditorAPI = {
setDataSource: async () => getEditorResponseData(castToEditorResponse(null)),
getDataSource: async () => getEditorResponseData(castToEditorResponse(null)),
removeDataSource: async () => getEditorResponseData(castToEditorResponse(null)),
setDataRow: async () => getEditorResponseData(castToEditorResponse(null)),
};

const mockedDataItemMappingTools = new DataItemMappingTools();

beforeEach(() => {
mockedDataSourceController = new DataSourceController(mockEditorApi);
mockedDataSourceController = new DataSourceController(mockEditorApi, mockedDataItemMappingTools);
jest.spyOn(mockEditorApi, 'setDataSource');
jest.spyOn(mockEditorApi, 'getDataSource');
jest.spyOn(mockEditorApi, 'removeDataSource');
jest.spyOn(mockEditorApi, 'setDataRow');
});

afterAll(() => {
Expand All @@ -39,4 +44,29 @@ describe('DataSourceController', () => {
await mockedDataSourceController.removeDataSource();
expect(mockEditorApi.removeDataSource).toHaveBeenCalledTimes(1);
});

it('Should be possible to set a data row', async () => {
const dataRow = { '1': 2, '3': 4 };
await mockedDataSourceController.setDataRow(dataRow);
expect(mockEditorApi.setDataRow).toHaveBeenCalledTimes(1);
expect(mockEditorApi.setDataRow).toHaveBeenCalledWith(JSON.stringify(dataRow));
});

it('Date objects are being converted to DatePropertyWrapper objects', async () => {
await mockedDataSourceController.setDataRow({
createDate: new Date(1111),
anotherDate: new Date(2222),
stringValue: 'hola',
numberValue: 5,
});

expect(mockEditorApi.setDataRow).toHaveBeenCalledWith(
JSON.stringify({
createDate: { value: new Date(1111).getTime(), type: 'date' } as EngineDataItem,
anotherDate: { value: new Date(2222).getTime(), type: 'date' } as EngineDataItem,
stringValue: 'hola',
numberValue: 5,
}),
);
});
});
83 changes: 83 additions & 0 deletions packages/sdk/src/utils/DataItemMappingTools.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { DataItem } from '../types/DataConnectorTypes';

/**
* Utility tools for mapping between `EngineDataItem` and `DataItem`.
*/
export class DataItemMappingTools {
/**
* Checks if the given value is a `DatePropertyWrapper`.
*
* @param value The value to check.
* @returns `true` if the value is a `DatePropertyWrapper`, otherwise `false`.
*/
private isDatePropertyWrapper(
value: string | number | boolean | DatePropertyWrapper | null,
): value is DatePropertyWrapper {
return typeof value === 'object' && value?.type === 'date';
}

/**
* Checks if the given value is a `Date` object instance.
*
* @param value The value to check.
* @returns `true` if the value is a `Date` object, otherwise `false`.
*/
private isDateObject(value: string | number | boolean | Date | null): value is Date {
return value instanceof Date;
}

/**
* Transforms an `EngineDataItem` into a `DataItem`.
*
* Converts `DatePropertyWrapper` values to JavaScript `Date` objects
* while keeping other values unchanged.
*
* @param dataItem the EngineDataItem to transform.
* @returns the resulting `DataItem` with parsed date properties.
*/
mapEngineToDataItem(dataItem: EngineDataItem): DataItem {
const parsedItem: DataItem = {};

Object.entries(dataItem).forEach(([key, value]) => {
parsedItem[key] = this.isDatePropertyWrapper(value) ? new Date(value.value) : value;
});

return parsedItem;
}

/**
* Transforms a `DataItem` into an `EngineDataItem`.
*
* Converts JavaScript `Date` objects to `DatePropertyWrapper` objects
* while keeping other values unchanged.
*
* @param dataItem the `DataItem` to transform.
* @returns the resulting `EngineDataItem` with parsed date properties.
*/
mapDataItemToEngine(dataItem: DataItem): EngineDataItem {
const parsedItem: EngineDataItem = {};

Object.entries(dataItem).forEach(([key, value]) => {
parsedItem[key] = this.isDateObject(value)
? ({ value: value.getTime(), type: 'date' } as DatePropertyWrapper)
: value;
});

return parsedItem;
}
}

/**
* Internal type used to wrap date objects on the engine side.
*/
export type DatePropertyWrapper = {
value: number;
type: 'date';
};

/**
* Engine data item type.
*/
export type EngineDataItem = {
[key: string]: string | number | boolean | DatePropertyWrapper | null;
};

0 comments on commit 9079986

Please sign in to comment.