-
Notifications
You must be signed in to change notification settings - Fork 25
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2625 from IDEMSInternational/feat/set-data-action…
…-operators Feat: add_data action
- Loading branch information
Showing
16 changed files
with
599 additions
and
271 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
138 changes: 138 additions & 0 deletions
138
src/app/shared/services/dynamic-data/actions/add_data.action.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
import { TestBed } from "@angular/core/testing"; | ||
|
||
import { HttpClientTestingModule } from "@angular/common/http/testing"; | ||
import { MockAppDataService } from "../../data/app-data.service.mock.spec"; | ||
import { AppDataService } from "../../data/app-data.service"; | ||
|
||
import addDataAction, { IActionAddDataParams } from "./add_data.action"; | ||
import { DynamicDataService } from "../dynamic-data.service"; | ||
import { firstValueFrom } from "rxjs"; | ||
import { FlowTypes } from "packages/data-models"; | ||
import { DeploymentService } from "../../deployment/deployment.service"; | ||
import { MockDeploymentService } from "../../deployment/deployment.service.spec"; | ||
import { TemplateActionRegistry } from "../../../components/template/services/instance/template-action.registry"; | ||
import { DynamicDataActionFactory, IActionRemoveDataParams } from "./index"; | ||
|
||
type ITestRow = { id: string; number: number; string: string; _meta_field?: any }; | ||
|
||
const TEST_DATA_ROWS = (): FlowTypes.Data_listRow<ITestRow>[] => [ | ||
{ id: "id_0", number: 0, string: "hello", _meta_field: "original" }, | ||
{ id: "id_1", number: 1, string: "hello", _meta_field: "original" }, | ||
]; | ||
|
||
const TEST_DATA_LIST = (): FlowTypes.Data_list => ({ | ||
flow_name: "test_flow", | ||
flow_type: "data_list", | ||
// Make deep clone of data to avoid data overwrite issues | ||
rows: TEST_DATA_ROWS(), | ||
// Metadata would be extracted from parser based on data or defined schema | ||
_metadata: { | ||
boolean: { type: "boolean" }, | ||
number: { type: "number" }, | ||
_meta_field: { type: "object" }, | ||
}, | ||
}); | ||
|
||
/******************************************************************************** | ||
* Test Utilities | ||
*******************************************************************************/ | ||
|
||
/** | ||
* Trigger the set_data action with included params and return first update | ||
* to corresponding data_list | ||
* * */ | ||
async function triggerTestAddDataAction( | ||
service: DynamicDataService, | ||
params: Partial<IActionAddDataParams> | ||
) { | ||
await addDataAction(service, { ...params, _list_id: "test_flow" }); | ||
const obs = await service.query$<any>("data_list", "test_flow"); | ||
const data = await firstValueFrom(obs); | ||
return data; | ||
} | ||
|
||
async function triggerRemoveDataAction( | ||
service: DynamicDataService, | ||
params: Partial<IActionRemoveDataParams> | ||
) { | ||
const { remove_data } = new DynamicDataActionFactory(service); | ||
await remove_data({ | ||
action_id: "remove_data", | ||
args: [], | ||
trigger: "click", | ||
params: { ...params, _list_id: "test_flow" }, | ||
}); | ||
const obs = await service.query$<any>("data_list", "test_flow"); | ||
const data = await firstValueFrom(obs); | ||
return data; | ||
} | ||
|
||
/******************************************************************************** | ||
* Tests | ||
* yarn ng test --include src/app/shared/services/dynamic-data/actions/add_data.action.spec.ts | ||
*******************************************************************************/ | ||
describe("set_data Action", () => { | ||
let service: DynamicDataService; | ||
|
||
beforeEach(async () => { | ||
TestBed.configureTestingModule({ | ||
imports: [HttpClientTestingModule], | ||
providers: [ | ||
DynamicDataService, | ||
{ | ||
provide: AppDataService, | ||
useValue: new MockAppDataService({ | ||
data_list: { | ||
test_flow: TEST_DATA_LIST(), | ||
}, | ||
}), | ||
}, | ||
{ | ||
provide: DeploymentService, | ||
useValue: new MockDeploymentService({ name: "test" }), | ||
}, | ||
{ | ||
provide: TemplateActionRegistry, | ||
useValue: { register: () => null }, | ||
}, | ||
], | ||
}); | ||
|
||
// HACK - polyfill not loaded for rxdb dev plugin so manually fill global before running tests | ||
window.global = window; | ||
service = TestBed.inject(DynamicDataService); | ||
await service.ready(); | ||
// ensure data cleared between flows | ||
await service.resetFlow("data_list", "test_flow"); | ||
}); | ||
|
||
/************************************************************* | ||
* Main Tests | ||
************************************************************/ | ||
it("add_data", async () => { | ||
const data = await triggerTestAddDataAction(service, { number: 2 }); | ||
expect(data.length).toEqual(3); | ||
const addedData = data[2]; | ||
expect(addedData.number).toEqual(2); | ||
// populated metadata | ||
expect(addedData._user_created).toEqual(true); | ||
expect(addedData.row_index).toEqual(2); | ||
expect(addedData.id).toBeTruthy(); | ||
}); | ||
|
||
it("add_data type coercion", async () => { | ||
const data = await triggerTestAddDataAction(service, { number: "2" }); | ||
const addedData = data[2]; | ||
expect(addedData.number).toEqual(2); | ||
}); | ||
|
||
it("remove_data", async () => { | ||
const data = await triggerTestAddDataAction(service, { number: 2 }); | ||
const addedData = data[2]; | ||
expect(addedData.number).toEqual(2); | ||
const updatedData = await triggerRemoveDataAction(service, { _id: addedData.id }); | ||
expect(updatedData.length).toEqual(2); | ||
}); | ||
|
||
// TODO - @list evaluation (when supported) | ||
}); |
31 changes: 31 additions & 0 deletions
31
src/app/shared/services/dynamic-data/actions/add_data.action.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import { generateUUID } from "src/app/shared/utils"; | ||
import { coerceDataUpdateTypes, evaluateDynamicDataUpdate } from "../dynamic-data.utils"; | ||
import { DynamicDataService } from "../dynamic-data.service"; | ||
|
||
export type IActionAddDataParams = { | ||
_list_id: string; | ||
} & Record<string, any>; | ||
|
||
export default async (service: DynamicDataService, params: IActionAddDataParams) => { | ||
const { _list_id, ...data } = await parseParams(params); | ||
|
||
const schema = await service.getSchema("data_list", _list_id); | ||
|
||
// assign a row_index to push user_generated docs to bottom of list | ||
data.row_index = await service.getCount("data_list", _list_id); | ||
// generate a unique id for the entry | ||
data.id = generateUUID(); | ||
// add metadata to track user created | ||
data._user_created = true; | ||
// HACK - use the same dynamic data evaluator as set_data action | ||
// This requires passing an item list, so just create an ad-hoc list with a single item | ||
// TODO - add support for evaluating @list reference to parent list | ||
const [evaluated] = evaluateDynamicDataUpdate([{ id: data.id }], data); | ||
|
||
const [coerced] = coerceDataUpdateTypes(schema?.jsonSchema?.properties, [evaluated]); | ||
return service.insert("data_list", _list_id, coerced); | ||
}; | ||
|
||
async function parseParams(params) { | ||
return params; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
import { isObjectLiteral } from "packages/shared/src/utils/object-utils"; | ||
import { IActionHandler } from "../../../components/template/services/instance/template-action.registry"; | ||
import type { DynamicDataService } from "../dynamic-data.service"; | ||
|
||
import addDataAction, { IActionAddDataParams } from "./add_data.action"; | ||
import setDataAction, { IActionSetDataParams } from "./set_data.action"; | ||
|
||
interface IActionBaseParams { | ||
_list_id?: string; | ||
_index?: string | number; | ||
} | ||
|
||
export type IActionRemoveDataParams = { | ||
_list_id: string; | ||
_id: string; | ||
}; | ||
|
||
type IActionResetDataParams = { | ||
_list_id: string; | ||
}; | ||
|
||
// Use action factory to make it easier to apply common logic to action params and pass service | ||
export class DynamicDataActionFactory { | ||
constructor(private service: DynamicDataService) {} | ||
|
||
public set_data: IActionHandler = async ({ params }: { params?: IActionSetDataParams }) => { | ||
const parsedParams = this.parseActionParams(params); | ||
return setDataAction(this.service, parsedParams); | ||
}; | ||
|
||
public reset_data: IActionHandler = async ({ params }: { params?: IActionResetDataParams }) => { | ||
const { _list_id } = this.parseActionParams(params); | ||
return this.service.resetFlow("data_list", _list_id); | ||
}; | ||
|
||
public remove_data: IActionHandler = async ({ params }: { params?: IActionRemoveDataParams }) => { | ||
const { _id, _list_id } = params; | ||
return this.service.remove("data_list", _list_id, [_id]); | ||
}; | ||
|
||
public add_data: IActionHandler = async ({ params }: { params?: IActionAddDataParams }) => { | ||
const parsedParams = this.parseActionParams(params); | ||
return addDataAction(this.service, parsedParams); | ||
}; | ||
|
||
/** Parse action parameters to ensure list id provided and */ | ||
private parseActionParams<T extends IActionBaseParams>(params: T) { | ||
if (isObjectLiteral(params)) { | ||
const parsed = hackParseTemplatedParams(params); | ||
if (!parsed._list_id) { | ||
console.error(params); | ||
throw new Error("[Data Actions] could not parse list id"); | ||
} | ||
return parsed; | ||
} | ||
|
||
// throw error if args not parsed correctly | ||
console.error(params); | ||
throw new Error("[set_data] could not parse params"); | ||
} | ||
} | ||
|
||
/** | ||
* Any params provided by templating system will already be partially parsed to replace dynamic references | ||
* such as @local or @field. This also converts @item references to `this.item` | ||
* Revert the item changes and also convert string parameter values to number where required | ||
* | ||
* TODO - this method should be removed and `items` made to update their own `this` context | ||
**/ | ||
function hackParseTemplatedParams<T extends IActionBaseParams>(params: T) { | ||
const parsed = {} as T; | ||
// HACK - un-parse @item references that the templating system converts to `this.item` | ||
for (const [key, value] of Object.entries(params)) { | ||
if (typeof value === "string") { | ||
parsed[key] = value.replace(/this\.item/g, "@item"); | ||
} else { | ||
parsed[key] = value; | ||
} | ||
} | ||
// convert _index param which may be passed as string from template if defined inline | ||
// NOTE - RXDB will automatically cast all other string values to correct type due to inferred schema | ||
if (typeof params._index === "string") { | ||
parsed._index = Number(params._index); | ||
} | ||
return parsed; | ||
} |
Oops, something went wrong.