From 8b0efdff3b1be59478bd3d093a1c93f740178e4d Mon Sep 17 00:00:00 2001 From: Constantin Dumitrescu Date: Sun, 15 Nov 2020 02:33:17 +0200 Subject: [PATCH] feat(engine.producer): add methods to get and update operations add get.value, get.contains, get.length, update.pop, update.push re #53 --- docs/docs/api/get.md | 21 +++--- docs/docs/api/update.md | 10 ++- package.json | 2 +- .../engine.producer/specs/producer.spec.ts | 73 ++++++++++++++++++- .../src/graph/getInvokablePath.ts | 3 +- .../engine.producer/src/graph/getOperation.ts | 32 +++++++- .../src/graph/updateOperation.ts | 47 +++++++++++- packages/engine.react/specs/get.spec.tsx | 2 +- packages/engine.react/specs/path.spec.tsx | 2 +- packages/engine.types/src/producer.ts | 9 +++ 10 files changed, 175 insertions(+), 26 deletions(-) diff --git a/docs/docs/api/get.md b/docs/docs/api/get.md index da0521eb..128d85d5 100644 --- a/docs/docs/api/get.md +++ b/docs/docs/api/get.md @@ -13,7 +13,7 @@ import { get } from "@c11/engine.macro" `get` provides the ability to get values from the global state at a later time, after the `view` or `producer` was triggered. It works the same way as [observe](/docs/api/observe), except: -1. `get` don't provide a value, but instead a function which can be called at +1. `get` don't provide a value, but instead an api for that path which can be used at any time in future to get the latest value form state 2. `get` don't cause `view`s and `producer`s to get triggered @@ -25,20 +25,21 @@ after the `view` or `producer` was triggered. It works the same way as ## API -### `get.: (newParams?: object) => any` +`get.` returns an object with following properties: -A call to `get.` (where `` is a path to any data in state) returns a -getter function. +1. `.value(params?: object)` returns the date stored at that `` + `params` is an optional object argument, the keys of which set the + [param](/docs/api/param)s. +2. `.includes(value: any, params?: object)` if the value at the given + `` is an array or a string, it returns a boolean if the provided + `value` exists at that `` +3. `.length(params?: object)` if the value at the given `` is an `array`,a `string`, or a `function` it returns the length property -Calling this function returns the data stored in that path, or `undefined` (for -non-existent path). If the stored data is serializable (e.g a primitive +For the `value` getter method, if the stored data is serializable (e.g a primitive Javascript type, a plain object), a copy of the data is returned. However, if the data is not serializable (e.g a class instance, function etc), a reference to it is returned. -The getter function also receives an optional argument of type `Object`. The -keys of this object set the [param](/docs/api/param)s. - ## Example For example, if the state looks like: @@ -57,7 +58,7 @@ e.g ``` const doSomeWork: producer = ({ getBar = get.foo.bar }) => { - const var = getBar(); // provides lates value of bar, and does not trigger if bar ever changes + const var = getBar.value(); // provides lates value of bar, and does not trigger if bar ever changes } ``` diff --git a/docs/docs/api/update.md b/docs/docs/api/update.md index dad52099..b5dfd675 100644 --- a/docs/docs/api/update.md +++ b/docs/docs/api/update.md @@ -17,12 +17,14 @@ from state, `update` allows changing values in state. `update.` returns an object with following properties: -1. `.set(val: any, newParams?: object)` to replace the value of `` in - state, or create it if it doesn't exist yet. `newParams` is an optional +1. `.set(value: any, params?: object)` to replace the value of `` in + state, or create it if it doesn't exist yet. `params` is an optional object argument, the keys of which set the [param](/docs/api/param)s. -2. `.merge(val: any)` accepts an object, and merge it with existing object value +2. `.merge(value: any, params?: object)` accepts an object, and merge it with existing object value of `` in state -3. `.remove()` removes the `` from state +3. `.remove(params?: object)` removes the `` from state +4. `.push(value: any, params?: object)` if the value at the given `` is an array, then the value will be appened to the array +5. `.pop(params?: object)` if the value at the given `` is an array, then the last element will be removed ## Example diff --git a/package.json b/package.json index 31b37104..0fbfbaf7 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "test": "jest --clear-cache && lerna run test --parallel", "describe": "npm-scripts-info", "commit": "git-cz", - "release": "yarn clean && lerna run build && lerna run test", + "release": "yarn clean && lerna run build && lerna run test --parallel", "version:lerna": "lerna version --conventional-commits", "publish": "lerna publish from-package", "publish:local": "lerna run publish:local", diff --git a/packages/engine.producer/specs/producer.spec.ts b/packages/engine.producer/specs/producer.spec.ts index 6a3ac0a8..74861bb8 100644 --- a/packages/engine.producer/specs/producer.spec.ts +++ b/packages/engine.producer/specs/producer.spec.ts @@ -237,7 +237,7 @@ test("should support get operations", () => { }, }; const struct: producer = ({ refProp = get.items[param.id.bar].value }) => { - const value = refProp({ + const value = refProp.value({ id: { bar: "foo", }, @@ -416,7 +416,7 @@ test("should support path values to be used", () => { val3 = update[prop.path1][prop.path2.bam.value], }) => { observeVal = val1; - getVal = val2({ propName: "value" }); + getVal = val2.value({ propName: "value" }); updateVal = val3; }; const propName = "bar"; @@ -520,6 +520,75 @@ test("should redo paths and keep reference if external props change", () => { expect(fn.mock.calls[2][0]).toBe(333); }); +test("should support the full api for the update operation", () => { + const struct: producer = ({ + b = update.b, + c = update.c, + d = update.d, + e = update.e, + f = update.f, + g = update.g, + }) => { + b.set("123"); + c.merge({ foo: "123" }); + d.remove(); + e.push(3); + f.push(2); + g.pop(); + }; + const result = run(struct, { + b: "abc2", + c: { + bar: "123", + }, + d: "foo", + e: [1, 2], + f: 1, + g: [1, 2, 3], + }); + jest.runAllTimers(); + expect(result.db.get("/b")).toBe("123"); + expect(result.db.get("/c")).toEqual({ + foo: "123", + bar: "123", + }); + expect(result.db.get("/d")).toBe(undefined); + expect(result.db.get("/e")).toEqual([1, 2, 3]); + expect(result.db.get("/f")).toBe(1); + expect(result.db.get("/g")).toEqual([1, 2]); +}); + +test("should support the full api for the get operation", () => { + const struct: producer = ({ + a = get.a, + b = get.b[param.prop], + c = get.c, + d = get.d, + e = get.e, + f = get.f + }) => { + expect(a.value()).toBe('abc1') + expect(b.value({prop: 'bar'})).toBe('123') + expect(c.includes('foo')).toBe(true) + expect(c.includes('baz')).toBe(false) + expect(d.length()).toBe(4) + expect(e.includes('bam')).toBe(true) + expect(e.includes('qux')).toBe(false) + expect(f.length()).toBe(3) + }; + run(struct, { + a: "abc1", + b: { + bar: "123", + }, + c: ["foo"], + d: [1, 2, 3, 4], + e: 'foo bam baz', + f: (a, b, c) => {} + }); + jest.runAllTimers(); +}); + /* test("should allow args composition", () => { const state = { diff --git a/packages/engine.producer/src/graph/getInvokablePath.ts b/packages/engine.producer/src/graph/getInvokablePath.ts index d11cd2f5..9412a86f 100644 --- a/packages/engine.producer/src/graph/getInvokablePath.ts +++ b/packages/engine.producer/src/graph/getInvokablePath.ts @@ -2,6 +2,7 @@ import { GraphStructure, UpdateOperation, GetOperation, + OperationParams } from "@c11/engine.types"; import { PathSymbol } from "../path"; import { resolveValue } from "./resolveValue"; @@ -10,7 +11,7 @@ import { wildcard } from "../wildcard"; export const getInvokablePath = ( structure: GraphStructure, op: GetOperation | UpdateOperation, - params: any + params: OperationParams ) => { const path = op.path.reduce((acc, x: any) => { const value = resolveValue(structure, x, params); diff --git a/packages/engine.producer/src/graph/getOperation.ts b/packages/engine.producer/src/graph/getOperation.ts index 26f0ec91..2047237b 100644 --- a/packages/engine.producer/src/graph/getOperation.ts +++ b/packages/engine.producer/src/graph/getOperation.ts @@ -2,8 +2,12 @@ import { DatastoreInstance, GraphStructure, GetOperation, + OperationParams, } from "@c11/engine.types"; +import isString from "lodash/isString"; +import isArray from "lodash/isArray"; import { getInvokablePath } from "./getInvokablePath"; +import isFunction from "lodash/isFunction"; // TODO: add a isValid method to be able to check // if the ref path is properly generated @@ -15,11 +19,35 @@ export const getOperation = ( structure: GraphStructure, op: GetOperation ) => { - const get = (params: any) => { + const value = (params: OperationParams): unknown => { const path = getInvokablePath(structure, op, params); if (path) { return db.get(path); } + return + }; + const includes = (value: any, params: OperationParams): void | boolean => { + const path = getInvokablePath(structure, op, params); + if (path) { + const val = db.get(path); + if (isArray(val) || isString(val)) { + return val.includes(value) + } + } + } + const length = (params: OperationParams): void | number => { + const path = getInvokablePath(structure, op, params); + if (path) { + const val = db.get(path); + if (!(isString(val) || isArray(val) || isFunction(val))) { + return + } + return val.length + } + } + return { + value, + includes, + length }; - return get; }; diff --git a/packages/engine.producer/src/graph/updateOperation.ts b/packages/engine.producer/src/graph/updateOperation.ts index 3aedb9fc..93b5bdf4 100644 --- a/packages/engine.producer/src/graph/updateOperation.ts +++ b/packages/engine.producer/src/graph/updateOperation.ts @@ -2,7 +2,9 @@ import { DatastoreInstance, GraphStructure, UpdateOperation, + OperationParams } from "@c11/engine.types"; +import isArray from "lodash/isArray"; import { getInvokablePath } from "./getInvokablePath"; export const updateOperation = ( @@ -10,7 +12,7 @@ export const updateOperation = ( structure: GraphStructure, op: UpdateOperation ) => { - const set = (value: any, params: any) => { + const set = (value: any, params: OperationParams) => { const path = getInvokablePath(structure, op, params); if (path) { const patch = { @@ -21,7 +23,7 @@ export const updateOperation = ( db.patch([patch]); } }; - const merge = (value: any, params: any) => { + const merge = (value: any, params: OperationParams) => { const path = getInvokablePath(structure, op, params); if (path) { const patch = { @@ -42,7 +44,7 @@ export const updateOperation = ( db.patch([patch]); } }; - const remove = (params: any) => { + const remove = (params: OperationParams) => { const path = getInvokablePath(structure, op, params); if (path) { const patch = { @@ -52,9 +54,46 @@ export const updateOperation = ( db.patch([patch]); } }; + const push = (value: any, params: OperationParams) => { + const path = getInvokablePath(structure, op, params); + if (path) { + const val = db.get(path) + if (!isArray(val)) { + // console.error('path is not an array') + return + } + val.push(value) + const patch = { + op: "add", + path: path, + value: val + }; + db.patch([patch]); + } + }; + const pop = (params: OperationParams) => { + const path = getInvokablePath(structure, op, params); + if (path) { + const val = db.get(path) + if (!isArray(val)) { + // console.error('path is not an array') + return + } + val.pop() + const patch = { + op: "add", + path: path, + value: val + }; + db.patch([patch]); + } + }; + return { set, merge, remove, - }; + push, + pop + } }; diff --git a/packages/engine.react/specs/get.spec.tsx b/packages/engine.react/specs/get.spec.tsx index f79b7665..d13ce180 100644 --- a/packages/engine.react/specs/get.spec.tsx +++ b/packages/engine.react/specs/get.spec.tsx @@ -25,7 +25,7 @@ test("Expect to call using only get", async (done) => { document.body.appendChild(rootEl); const Component: view = ({ foo = get.foo }) => { expect(foo).toBeDefined(); - return
{foo()}
; + return
{foo.value()}
; }; engine({ state: defaultState, diff --git a/packages/engine.react/specs/path.spec.tsx b/packages/engine.react/specs/path.spec.tsx index b40a98ed..27bd7cf9 100644 --- a/packages/engine.react/specs/path.spec.tsx +++ b/packages/engine.react/specs/path.spec.tsx @@ -70,7 +70,7 @@ test("should support path operations with multiple components", async (done) => path2.set(e.target.value)} /> ); diff --git a/packages/engine.types/src/producer.ts b/packages/engine.types/src/producer.ts index 60352505..4faa320c 100644 --- a/packages/engine.types/src/producer.ts +++ b/packages/engine.types/src/producer.ts @@ -142,3 +142,12 @@ export interface ProducerContext { debug?: boolean; addView?: (view: ViewInstance) => void; } + +export type OperationParams = { + [k: string]: OperationParams | string | number | void | null; +} + + + + +