Angular 2, ngrx/store, RxJS and tree-like data - javascript

I've been trying to figure out a way to use the select operator in combination with rxjs's other operators to query a tree data structure (normalized in the store to a flat list) in such a way that it preserves referential integrity for ChangeDetectionStrategy.OnPush semantics but my best attempts cause the entire tree to be rerendered when any part of the tree changes. Does anyone have any ideas?
If you consider the following interface as representative of the data in the store:
export interface TreeNodeState {
id: string;
text: string;
children: string[] // the ids of the child nodes
}
export interface ApplicationState {
nodes: TreeNodeState[]
}
I need to create a selector that denormalizes the state above to return a graph of objects implementing the following interface:
export interface TreeNode {
id: string;
text: string;
children: TreeNode[]
}
That is, I need a function that takes an Observable<ApplicationState> and returns an Observable<TreeNode[]> such that each TreeNode instance maintains referential integrity unless one of its children has changed.
Ideally I'd like to have any one part of the graph only update its children if they've changed rather than return an entirely new graph when any node changes.
Does anyone know how such a selector could be constructed using ngrx/store and rxjs?
For more concrete examples of the kinds of things I've attempted check out the snippet below:
// This is the implementation I'm currently using.
// It works but causes the entire tree to be rerendered
// when any part of the tree changes.
export function getSearchResults(searchText: string = '') {
return (state$: Observable<ExplorerState>) =>
Observable.combineLatest(
state$.let(getFolder(undefined)),
state$.let(getFolderEntities()),
state$.let(getDialogEntities()),
(root, folders, dialogs) =>
searchFolder(
root,
id => folders ? folders.get(id) : null,
id => folders ? folders.filter(f => f.parentId === id).toArray() : null,
id => dialogs ? dialogs.filter(d => d.folderId === id).toArray() : null,
searchText
)
);
}
function searchFolder(
folder: FolderState,
getFolder: (id: string) => FolderState,
getSubFolders: (id: string) => FolderState[],
getSubDialogs: (id: string) => DialogSummary[],
searchText: string
): FolderTree {
console.log('searching folder', folder ? folder.toJS() : folder);
const {id, name } = folder;
const isMatch = (text: string) => !!text && text.toLowerCase().indexOf(searchText) > -1;
return {
id,
name,
subFolders: getSubFolders(folder.id)
.map(subFolder => searchFolder(
subFolder,
getFolder,
getSubFolders,
getSubDialogs,
searchText))
.filter(subFolder => subFolder && (!!subFolder.dialogs.length || isMatch(subFolder.name))),
dialogs: getSubDialogs(id)
.filter(dialog => dialog && (isMatch(folder.name) || isMatch(dialog.name)))
} as FolderTree;
}
// This is an alternate implementation using recursion that I'd hoped would do what I wanted
// but is flawed somehow and just never returns a value.
export function getSearchResults2(searchText: string = '', folderId = null)
: (state$: Observable<ExplorerState>) => Observable<FolderTree> {
console.debug('Searching folder tree', { searchText, folderId });
const isMatch = (text: string) =>
!!text && text.search(new RegExp(searchText, 'i')) >= 0;
return (state$: Observable<ExplorerState>) =>
Observable.combineLatest(
state$.let(getFolder(folderId)),
state$.let(getContainedFolders(folderId))
.flatMap(subFolders => subFolders.map(sf => sf.id))
.flatMap(id => state$.let(getSearchResults2(searchText, id)))
.toArray(),
state$.let(getContainedDialogs(folderId)),
(folder: FolderState, folders: FolderTree[], dialogs: DialogSummary[]) => {
console.debug('Search complete. constructing tree...', {
id: folder.id,
name: folder.name,
subFolders: folders,
dialogs
});
return Object.assign({}, {
id: folder.id,
name: folder.name,
subFolders: folders
.filter(subFolder =>
subFolder.dialogs.length > 0 || isMatch(subFolder.name))
.sort((a, b) => a.name.localeCompare(b.name)),
dialogs: dialogs
.map(dialog => dialog as DialogSummary)
.filter(dialog =>
isMatch(folder.name)
|| isMatch(dialog.name))
.sort((a, b) => a.name.localeCompare(b.name))
}) as FolderTree;
}
);
}
// This is a similar implementation to the one (uses recursion) above but it is also flawed.
export function getFolderTree(folderId: string)
: (state$: Observable<ExplorerState>) => Observable<FolderTree> {
return (state$: Observable<ExplorerState>) => state$
.let(getFolder(folderId))
.concatMap(folder =>
Observable.combineLatest(
state$.let(getContainedFolders(folderId))
.flatMap(subFolders => subFolders.map(sf => sf.id))
.concatMap(id => state$.let(getFolderTree(id)))
.toArray(),
state$.let(getContainedDialogs(folderId)),
(folders: FolderTree[], dialogs: DialogSummary[]) => Object.assign({}, {
id: folder.id,
name: folder.name,
subFolders: folders.sort((a, b) => a.name.localeCompare(b.name)),
dialogs: dialogs.map(dialog => dialog as DialogSummary)
.sort((a, b) => a.name.localeCompare(b.name))
}) as FolderTree
));
}

If willing to rethink the problem, you could use Rxjs operator scan:
If no previous ApplicationState exists, accept the first one. Translate it to TreeNodes recursively. As this is an actual object, rxjs isn't involved.
Whenever a new application state is received, ie when scan fires, implement a function that mutates the previous nodes using the state received, and returns the previous nodes in the scan operator. This will guarantee you referential integrity.
You might now be left with a new problem, as changes to mutated tree nodes might not be picked up. If so, either look into track by by making a signature for each node, or consider adding a changeDetectorRef to a node (provided by component rendering node), allowing you to mark a component for update. This will likely perform better, as you can go with change detection strategy OnPush.
Pseudocode:
state$.scan((state, nodes) => nodes ? mutateNodesBy(nodes, state) : stateToNodes(state))
The output is guaranteed to preserve referential integrity (where possible) as nodes are built once, then only mutated.

Related

Firestore: Array of references within a document into an object with array of objects (AngularFire)

I have a collection, where each single document have an array of references.
The structure in Firestore is like this:
collection1
doc1
doc2
doc3
name: string
...
refs: Array
collection2/x786123nd...
collection2/z1237683s...
...
I want to convert that into:
[
{<doc1 as object>},
{<doc2 as object},
{ name: "Document 3", refsResult:
[
{<object here mapped from reference>},
{<object here mapped from reference>},
]
}
]
Long story short, from firebase I need to get an output of a list of object where each object has a list of objects instead of references.
THE FOLLOWING SNIPPET IS NOT WORKING
I am trying to do it at service level in Angular using RxJS to transform the output, something like this:
return this.afs.collection('collection1')
.valueChanges()
.pipe(
switchMap(objects => from(objects).pipe(
switchMap(obj => from(obj.refs).pipe(
switchMap(ref => ref.get()),
map(r => ({ ...obj, refsResult: r.data() })),
tap(obj => console.log('OBJ', obj)),
)),
)),
tap(objects => console.log(objects))
);
But it seems that I only receive one object instead of a list of objects with 2. Also, it seems that the refsResult is also a single object instead of an array. I am sure I am using the switchMaps wrong, probably they are cancelling the others results or similar.
I would like to keep the solution within the chain of rxjs operators.
EDIT 1
I got it working in a hacky way, I am just sharing it here in order to give more context, and see if we can find a way to do it with RxJS.
return this.afs.collection('collection1')
.valueChanges()
.pipe(
tap(objects => {
objects.forEach(object => {
object.refsResult = [];
object.refs.forEach(ref => {
ref.get().then(d => object.refsResult.push(d.data()));
})
})
}),
tap(programs => console.log(programs))
);
The code above works but not very well, since first return the collection and then, mutates the objects to inject the items within the array. Hopefully it helps.
Help is very appreciated! :)
You can use combineLatest to create a single observable that emits an array of data.
In your case, we can use it twice in a nested way:
to handle each object in collection
to handle each ref in refs array for each object
The result is a single observable that emits the full collection of objects, each with their full collection of ref result data. You then only need a single
switchMap to handle subscribing to this single observable:
return this.afs.collection('collection1').valueChanges().pipe(
switchMap(objects => combineLatest(
objects.map(obj => combineLatest(
obj.refs.map(ref => from(ref.get()).pipe(map(r => r.data())))
).pipe(
map(refsResult => ({ ...obj, refsResult }))
)
))
))
);
For readability, I'd probably create a separate function:
function appendRefData(obj) {
return combineLatest(
obj.refs.map(ref => ref.get().pipe(map(r => r.data())))
).pipe(
map(refsResult => ({ ...obj, refsResult }))
);
}
return this.afs.collection('collection1').valueChanges().pipe(
switchMap(objects => combineLatest(objects.map(appendRefData)))
);

How to make recursive HTTP calls using RxJs operators?

I use the following method in odder to retrieve data by passing pageIndex (1) and pageSize (500) for each HTTP call.
this.demoService.geList(1, 500).subscribe(data => {
this.data = data.items;
});
The response has a property called isMore and I want to modify my method in odder to continue HTTP calls if isMore is true. I also need to merge the returned values and finally return the accumulated values.
For example, assuming that there are 5000 records and until 10th HTTP call, the service returns true for isMore value. After 10th HTTP call, it returns false and then this method sets this.data value with the merged 5000 records. For this problem, should I use mergeMap or expand or another RxJs operator? What is the proper way to solve this problem?
Update: I use the following approach, but it does not merge the returned values and not increase the pageIndex. For this reason it does not work (I tried to make some changes, but could not make it work).
let pageIndex = 0;
this.demoService.geList(pageIndex+1, 500).pipe(
expand((data) => {
if(data.isComplete) {
return of(EMPTY);
} else {
return this.demoService.geList(pageIndex+1, 500);
}
})
).subscribe((data) => {
//your logic here
});
Update II:
of({
isMore : true,
pageIndex: 0,
items: []
}).pipe(
expand(data => demoService.geList(data.pageIndex+1, 100)
.pipe(
map(newData => ({...newData, pageIndex: data.pageIndex+1}))
)),
// takeWhile(data => data.isMore), //when using this, it does not work if the total record is less than 100
takeWhile(data => (data.isMore || data.pageIndex === 1)), // when using this, it causing +1 extra HTTP call unnecessarily
map(data => data.items),
reduce((acc, items) => ([...acc, ...items]))
)
.subscribe(data => {
this.data = data;
});
Update III:
Finally I made it work by modifying Elisseo's approach as shown below. Howeveri **I need to make it void and set this.data parameter in this getData() method. How can I do this?
getData(pageIndex, pageSize) {
return this.demoService.geList(pageIndex, pageSize).pipe(
switchMap((data: any) => {
if (data.isMore) {
return this.getData(pageIndex+1, pageSize).pipe(
map((res: any) => ({ items: [...data.items, ...res.items] }))
);
}
return of(data);
})
);
}
I want to merge the following subscribe part to this approach but I cannot due to some errors e.g. "Property 'pipe' does not exist on type 'void'."
.subscribe((res: any) => {
this.data = res;
});
getData(pageIndex, pageSize) {
return this.demoService.getList(pageIndex, pageSize).pipe(
switchMap((data: any) => {
if (!data.isCompleted) {
return this.getData(pageIndex+1, pageSize).pipe(
map((res: any) => ({ data: [...data.data, ...res.data] }))
);
}
return of(data);
})
);
}
stackblitz
NOTE: I updated pasing as argument pageIndex+1 as #mbojko suggest -before I wrote pageIndex++
UPDATE 2
Using expand operator we need take account that we need feed the "recursive function" with an object with pageIndex -it's necesarry in our call- for this, when we make this.demoService.getList(data.pageIndex+1,10) we need "transform the result" adding a new property "pageIndex". for this we use "map"
getData() {
//see that initial we create "on fly" an object with properties: pageIndex,data and isCompleted
return of({
pageIndex:1,
data:[],
isCompleted:false
}).pipe(
expand((data: any) => {
return this.demoService.getList(data.pageIndex,10).pipe(
//here we use map to create "on fly" and object
map((x:any)=>({
pageIndex:data.pageIndex+1, //<--pageIndex the pageIndex +1
data:[...data.data,...x.data], //<--we concatenate the data using spread operator
isCompleted:x.isCompleted})) //<--isCompleted the value
)
}),
takeWhile((data: any) => !data.isCompleted,true), //<--a take while
//IMPORTANT, use "true" to take account the last call also
map(res=>res.data) //finally is we only want the "data"
//we use map to return only this property
)
}
Well we can do a function like this:
getData() {
of({pageIndex:1,data:[],isCompleted:false}).pipe(
expand((data: any) => {
return this.demoService.getList(data.pageIndex,10).pipe(
tap(x=>{console.log(x)}),
map((x:any)=>({
pageIndex:data.pageIndex+1,
data:[...data.data,...x.data],
isComplete:x.isComplete}))
)
}),
takeWhile((data: any) => !data.isComplete,true), //<--don't forget the ",true"
).subscribe(res=>{
this.data=res.data
})
}
See that in this case we don't return else simple subscribe to the function and equal a variable this.data to res.data -it's the reason we don't need the last map
Update 3 by Mrk Sef
Finally, if you don't want your stream to emit intermittent values and you just want the final concatenated data, you can remove the data concatenation from expand, and use reduce afterward instead.
getData() {
of({
pageIndex: 1,
data: [],
isCompleted: false
})
.pipe(
expand((prevResponse: any) => this.demoService.getList(prevResponse.pageIndex, 10).pipe(
map((nextResponse: any) => ({
...nextResponse,
pageIndex: prevResponse.pageIndex + 1
}))
)
),
takeWhile((response: any) => !response.isCompleted, true),
// Keep concatenting each new array (data.items) until the stream
// completes, then emit them all at once
reduce((acc: any, data: any) => {
return [...acc, ...data.data];
}, [])
)
.subscribe(items => {
this.data=items;
});
}
It doesn't matter if you're total record change as long as api response give you the isMore flag.
I'm skipping the part how to implement reducer action event i'm assuming you've already done that part. So i will just try to explain with pseudo codes.
You have a table or something like that with pagination data. on intial state you can just create an loadModule effect or using this fn:
getPaginationDataWithPageIndex(pageIndex = 1){
this.store.dispatch(new GetPaginationData({ pageIndex: pageIndex, dataSize: 500}));
}
in your GetPaginationData effect
... map(action => {
return apicall.pipe(map((response)=> {
if(response.isMore){
return new updateState({data:response.data, isMore: responseisMore})
} else {
return new updateState({isMore: response.isMore}),
}
}})
`
all you have to left is subscribing store in your .ts if isMore is false you will not display the next page button. and on your nextButton or prevButton's click method you should have to just dispatch the action with pageIndex
I do not think recursion is the correct approach here:
interval(0).pipe(
map(count => this.demoService.getList(count + 1, 500)),
takeWhile(reponse => response.isMore, true),
reduce((acc, curr) => //reduce any way you like),
).subscribe();
This should make calls to your endpoint until the endpoint returns isMore === false. The beautiful thing about interval is that we get the count variable for free.
But if you are set on using recrsion, here is the rxjs-way to do that using the expand-operator (see the docs). I find it slightly less readable, as it requires an if-else-construct which increases code complexity. Also the outer 'counter' variable just isn't optimal.
let index = 1;
this.demoService.geList(index, 500).pipe(
expand(response => response.isMore ? this.demoService.geList(++index, 500) : empty()),
reduce((acc, curr) => //reduce here)
).subscribe();

How to build a chain of methods run and find cycles (collisions)

What I have: there is some json config (descriptive template), methods stored in diffrent order, its look like:
[
{
"name" : "methodA", //methodA output arguments are methodB input arguments
"inArgs" : "[arg1, arg2]",
"returnArgs" : "[arg3, arg4]"
},
{
"name" : "methodB", //methodB output arguments are methodZ input arguments
"inArgs" : "[arg3, arg5]",
"returnArgs" : "[arg6, arg7]"
},
{
"name" : "methodС",
"inArgs" : "[arg1]",
"returnArgs" : "[arg10]"
},
a bunch of other methods whose input arguments are not part of methodA or methodB
.....
{
"name" : "methodZ",
"inArgs" : "[arg6, arg11]",
"returnArgs" : "[arg20]"
}
]
I need to put these methods in the right order(chain) to run, like:
methodC //the output of this method is not used as an input argument to other methods
methodA //chain i need right order
methodB
methodZ
second case
[
.....
{
"name" : "methodX", //methodX output arguments are methodY input arguments
"inArgs" : «arg1, arg2, arg3]»,
"returnArgs" : «[arg4, arg5, arg6]»
},
{
"name" : "methodY", //methodY output arguments are methodX input arguments
"inArgs" : «[arg4, arg5, arg7]»,
"returnArgs" : «[arg8, arg9, arg10]»
},
....
{
"name" : "methodZ", //methodZ output arguments are methodX input arguments( collision or cycle, so throw error )
"inArgs" : «[arg8, arg11, arg12]»,
"returnArgs" : «[arg3, arg13, arg14]»
},
]
Because the output arguments of one method can be the input arguments of another method (also through a chain of methods of indefinite nesting), it is necessary to catch such collisions, preferably at the stage of parsing the config.
Can someone advise the optimal solution to such a problem, so far only graphs come to mind.
Sorry for my English.
(Sorry, this is a very long answer. I hope it's useful.)
An answer I like
I started trying to solve this using the API you were looking for. I did manage to get something reasonably close. But it wasn't something I would personally use. I rewrote the API and refactored the implementation several times until I came up with something I would like to use. Below I will discuss more of my early steps (which may be more relevant to you) but here is how I would use my version:
const def = {
url: (server, path, query, fragment) => `${server}/${path || ''}${query || ''}${fragment ? `#${fragment}` : ''}`,
query: (parameters) => parameters ? '?' + Object.entries(parameters).map(([k, v]) => `${k}=${v}`).join('&') : '',
server: (schema, port, host) => `${schema}:/\/${host}${port && (String(port) != '80') ? `:${port}` : ''}`,
host: (domain, subdomain) => `${subdomain ? `${subdomain}.` : ''}${domain}`,
}
const vals = {
schema: 'https',
port: '80',
domain: 'example.com',
subdomain: 'test',
path: 'path/to/resource',
parameters: {foo: 42, bar: 'abc'},
fragment: 'baz',
}
runFunctions (def) (vals)
This would generate an output like the following:
{
schema: "https",
port: "80",
domain: "example.com",
subdomain: "test",
path: "path/to/resource",
parameters: {foo: 42, bar: "abc"},
fragment: "baz",
query: "?foo=42&bar=abc",
host: "test.example.com",
server: "https://test.example.com",
url: "https://test.example.com/path/to/resource?foo=42&bar=abc#baz"
}
API Design
The main advantage I see in this version is that the API feels quite clean. The configuration object just maps names to functions and the data object supplied to the resulting function simply maps names to the initial parameters needed by those functions. The result is an enhanced version of that data object. The initial call returns a reusable function. It's all very simple.
Implementation
Some of the history of how I wrote this is embedded in the design. It probably could use a good refactoring; several of the helper functions are probably not necessary. But for now it consists of:
four trivial helper functions:
isEmpty reports whether an array is empty
removeIndex acts like an immutable splice, returning a copy of an array without its nth index
props maps an array of property names to their values in a given object
error simply wraps a string in an error and throws it
one less trivial helper function:
parseArgs retrieves the parameter names from a function. It is based on https://stackoverflow.com/a/9924463. (Oddly, the first one I tried, https://stackoverflow.com/a/31194949, which worked fine in my testing REPL, failed here in the StackOverflow snippets.)
four main functions:
preprocess converts our description object into a configuration object that looks something like the structure described in the question (with name and inArgs properties, although without returnArgs ones.)
makeGraph converts converts a configuration object into an adjacency graph (an array of objects with a name string and a predecessors array of strings.)
sortGraph performs a topological sort on an adjacency graph. It's borrowed from one I wrote at https://stackoverflow.com/a/54408852/, but enhanced with the ability to throw an error if the graph is cyclic.
process accepts the configuration object and the sorted graph and generates a unary function. That function takes a context object and applies the functions in order to the properties of that object, adding a new value to the object keyed to the function name. This calls makeGraph and then sortGraph on the result.
and, finally, a small wrapper function:
runFunctions accepts a description object, calls preprocess on it to create the configuration object, passing that to process and returns the resulting function.
I'm sure there's a reasonable refactoring that removes the need for the intermediate configuration object and/or one that combines the creation and sorting of the graph. That's left as an exercise for the reader!
Complete example
// helpers
const isEmpty = arr =>
arr .length == 0
const removeIndex = (n, arr) =>
arr .slice (0, n) .concat (arr .slice (n + 1) )
const props = (names) => (obj) =>
names .map (name => obj [name] )
const error = (msg) => {
throw new Error (msg)
}
// retrieves parameter named from a function (https://stackoverflow.com/a/9924463)
const parseArgs = (func) => {
var fnStr = func.toString().replace( /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg, '');
var result = fnStr.slice(fnStr.indexOf('(')+1, fnStr.indexOf(')')).match(/([^\s,]+)/g);
if(result === null)
result = [];
return result;
}
// chooses an appropriate order for our digraph, throwing error on circular
const sortGraph = (
graph,
sorted = [],
idx = graph .findIndex (node => isEmpty (node.predecessors) ),
nodeName = (graph [idx] || {}) .name
) => isEmpty (graph)
? sorted
: idx < 0
? error ('function definitions contains cycle')
: sortGraph (
removeIndex (idx, graph) .map (({name, predecessors}) => ({
name,
predecessors: predecessors .filter (n => n !== nodeName)
}), graph),
sorted .concat (nodeName)
)
// turns a config into an adjacensy graph
const makeGraph = config =>
Object .entries (config) .map (([name, {inArgs}]) => ({
name,
predecessors: inArgs .filter (name => name in config)
}) )
// turns a config object into a function that will run its
// functions in an appropriate order
const process = (config, order = sortGraph (makeGraph (config) )) =>
(vals) =>
order .reduce
( (obj, name) => ({
...obj,
[name]: config [name] .fn .apply (obj, props (config [name] .inArgs) (obj) )
})
, vals
)
// converts simpler configuration into complete version
const preprocess = (def) =>
Object .entries (def) .reduce
( (obj, [name, fn]) => ( { ...obj, [name]: {fn, inArgs: parseArgs(fn)} })
, {}
)
// main function
const runFunctions = (def) =>
process (preprocess (def) )
// input definition
const def = {
url: (server, path, query, fragment) => `${server}/${path || ''}${query || ''}${fragment ? `#${fragment}` : ''}`,
query: (parameters) => parameters ? '?' + Object.entries(parameters).map(([k, v]) => `${k}=${v}`).join('&') : '',
server: (schema, port, host) => `${schema}:/\/${host}${port && (String(port) != '80') ? `:${port}` : ''}`,
host: (domain, subdomain) => `${subdomain ? `${subdomain}.` : ''}${domain}`,
}
// initial input object
const vals = {
schema: 'https',
port: '80',
domain: 'example.com',
subdomain: 'test',
path: 'path/to/resource',
parameters: {foo: 42, bar: 'abc'},
fragment: 'baz',
}
console .log (
runFunctions (def) (vals)
)
Differences from requested design
The API in the question was different: the configuration object looked more like:
[{
name: 'makeUrl',
inArgs: '[domain, subdomain]',
returnArgs: '[host]',
}, /* ... */]
and even after some cleanup, would look like this:
[{
name: 'makeHost',
inArgs: ['domain', 'subdomain'],
returnArgs: ['host'],
}, /* ... */]
This is more flexible than my solution, as it allows multiple returns from a single function, wrapped in an array. But without some uncomfortable gymnastics in the implementation, it would also require multiple returns from each function. Moreover it would require that however you supplied your functions to this, you would have to match the function separately with the name, you would have to ensure that the argument names and order exactly matched the inArgs parameter, and you would have to wrap the more common scalar returns in an array. That might look something like this:
const fns = {
makeHost: (domain, subdomain) => [`${subdomain ? `${subdomain}.` : ''}${domain}`],
/* ... */
}
My Initial Approach
Adding a second configuration parameter and keeping them in sync makes for a much less ergonomic API in my opinion. But it can be done, and it was how I first approached the problem.
This version needed several fewer helper functions. There is no need for preprocess or parseArgs. props was only added to simplify the refactored version above. I haven't checked whether it would help with this one.
Note that process is substantially more complicated here and makeGraph is somewhat more complicated. That's because handling the multiple return arguments adds a fair bit of work. Overall, this version is a few lines shorter than the version above. That's often the trade-off when you create a more comfortable API. But the individual functions are less complex.
Implementation
You can expand this snippet to see a complete example:
// helpers
const isEmpty = arr =>
arr .length == 0
const removeIndex = (n, arr) =>
arr .slice (0, n) .concat (arr .slice (n + 1))
const error = (msg) => {
throw new Error (msg)
}
// chooses an appropriate order for our digraph, throwing error on circular
const sortGraph = (
graph,
sorted = [],
idx = graph .findIndex (node => isEmpty (node.predecessors) ),
nodeName = (graph [idx] || {}) .name
) => isEmpty (graph)
? sorted
: idx < 0
? error ('contains cycle')
: sortGraph (
removeIndex (idx, graph) .map (({name, predecessors}) => ({
name,
predecessors: predecessors .filter (n => n !== nodeName)
}), graph),
sorted .concat (nodeName)
)
// turns a config into an adjacensy graph
const makeGraph = config =>
config .map (({name, inArgs}) => ({
name,
predecessors: inArgs .flatMap (
input => config
.filter ( ({returnArgs}) => returnArgs .includes (input) )
.map ( ({name}) => name )
)
}) )
// main function
const process = (config) => (fns, order = sortGraph (makeGraph (config) )) =>
(vals) =>
order .reduce
( (obj, name) => {
const {inArgs, returnArgs} = config .find
( node => node .name == name
)
const args = inArgs .map (key => obj [key])
const res = fns [name] .apply (obj, args)
return returnArgs .reduce
( (o, k, i) => ({...o, [k]: res [i]})
, obj
)
}
, vals
)
const config = [
{name: 'host', inArgs: ['domain', 'subdomain'], returnArgs: ['host']},
{name: 'server', inArgs: ['schema', 'port', 'host'], returnArgs: ['server']},
{name: 'query', inArgs: ['parameters'], returnArgs: ['query']},
{name: 'url', inArgs: ['server', 'path', 'query', 'fragment'], returnArgs: ['url']}
]
const fns = {
host: (domain, subdomain) => [`${subdomain ? `${subdomain}.` : ''}${domain}`],
server: (schema, port, host) =>
[`${schema}:/\/${host}${port && (String(port) != '80') ? `:${port}` : ''}`],
query: (parameters) => [parameters ? '?' + Object.entries(parameters).map(([k, v]) => `${k}=${v}`).join('&') : ''],
url: (server, path, query, fragment) => [`${server}/${path || ''}${query || ''}${fragment ? `#${fragment}` : ''}`]
}
const vals = {
schema: 'https',
port: '80',
domain: 'example.com',
subdomain: 'test',
path: 'my/path',
parameters: {foo: 42, bar: 'abc'},
fragment: 'baz',
}
console .log (
process (config) (fns) (vals)
)
Intermediate Work
I wouldn't even attempt to show all the stages my code went through between the initial and final versions, but there was an interesting waypoint in the API, in which I used a configuration object like this:
const config = {
host: {
inArgs: ['domain', 'subdomain'],
fn: (domain, subdomain) => `${subdomain ? `${subdomain}.` : ''}${domain}`,
},
/* ... */
}
There is something to be said for that version: it avoids the need to parse the function in order to to get the parameters. The variety of fragile answers to How to get function parameter names/values dynamically? demonstrate that this is a non-trivial problem. And it should be quite familiar to users of Angular's dependency injection.
But in the end, this is just too much cleaner:
const config = {
host: fn: (domain, subdomain) => `${subdomain ? `${subdomain}.` : ''}${domain}`,
/* ... */
}
And hence I prefer my final version.
Conclusions
This is a non-trivial problem.
The implementation is not particularly difficult in any of these versions. But breaking it down into the useful pieces is challenging. And determining a useful API when we're afforded the flexibility to choose whatever seems right can take a lot of thought, a lot of discussion, and a lot of playing around.
Different developers will make different choices, often for important reasons, but to me sacrificing the likely rare facility to have multiple returns from a single function was entirely worth it to achieve a substantially simpler configuration object. In fact, it's difficult to imagine a simpler configuration.
An easier, but not-that-bulletproof (you cannot detect cycles) solution would be to wrap every value into a Promise: When a function generates certain outputs, resolve the Promise, then use Promise.all on the inputs. That way, the promises will automatically determine the right order:
const context = { /* [var: string]: { resolve(v: T), value: Promise<T> */ };
function getVar(name) {
if(context[name]) return context[name].value;
const cont = context[name] = { };
return cont.value = new Promise(res => cont.resolve = res);
}
function setVar(name, value) {
getVar(name); // Make sure prop is initialized, don't await!
context[name].resolve(value);
}
async function run(fn, argNames, resultNames) {
const args = await Promise.all(argNames.map(getVar));
const results = fn(...args);
for(let i = 0; i < results.length; i++)
setVar(resultNames[i], results[i]);
}

How can i use ramda.js with this code?

I am beginning to use ramda, but have doubts about how to implement functions ramda.
This code is to mount a select object to do queries for sequelize.
See code:
const stringRequired = string => !!string.length
const numberRequired = number => Number.isInteger(number)
const validadeInput = (value, initial, validade) => (
validade(value) ? value : initial
)
const input = (obj, key, initial, validade) => (
typeof obj[key] !== 'undefined' ?
validadeInput(obj[key], initial, validade) :
initial
);
const addValue = (obj, key, value) => {
const prop = {}
prop[key] = value
return Object.assign(obj, prop)
}
const addFilter = (obj, key, value, validate) => (
validate(value) ? addValue(obj, key, value)
: obj
)
const selector = (query = {}) => {
const limit = input(query, 'limit', 10, numberRequired);
const name = input(query, 'name', '', stringRequired);
let select = {}
select = addFilter(select, 'name', name, stringRequired);
select = addFilter(select, 'limit', limit, numberRequired);
return select
}
console.log(selector());
// { limit: 10 }
console.log(selector({ name: 'David Costa' }));
// { limit: 10, name: 'David Costa' }
console.log(selector({ limit: 50 }));
// { limit: 50 }
Or see demo on link
http://jsbin.com/zerahay/edit?js,console,output
Why?
I think you need to consider why you want to convert this to Ramda
(Disclaimer: I'm a Ramda author) Ramda is a library, a toolkit. Use it when it helps clean up your code or when it makes it easier to understand the problem and its solution. Don't use it when it doesn't.
That said, I did refactor it, using Ramda:
A Refactoring
I simply tried to refactor it a bit. In the end, I replaced all your helper functions with a single one that takes a list of conditions such as ['limit', 10, numberRequired] to create a function equivalent to your selector.
I did use a few Ramda functions along the way, but the only one that offers substantive help is assoc, which creates a new object from an old one and a key and value. Using, for instance, compose(Boolean, length) is cleaner than const stringRequired = string => !!string.length, but it's not a large difference.
The important change, to my mind, is the makeSelector function, which makes creating your selector function much more declarative. It's a bit ugly, and I probably would write it differently if I were starting from scratch, but I did this in a series of steps, inlining your helper functions until I had a reasonably short function that had the same behavior.
// rules
const stringRequired = R.compose(Boolean, R.length)
const numberRequired = number => Number.isInteger(number)
// utils
const makeSelector = (conditions) => (query = {}) => R.reduce(
(select, [key, initial, validate]) => {
const value = key in select && validate(select[key]) ? select[key] : initial;
return validate(value) ? R.assoc(key, value, select) : select
},
query,
conditions
)
// main
const selector = makeSelector([
['limit', 10, numberRequired],
['name', '', stringRequired]
])
console.log(selector());
console.log(selector({ name: 'David Costa' }));
console.log(selector({ limit: 50 }));
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.25.0/ramda.js"></script>
I'm a little hesitant to simply respond with a translated block of code and my recommendation for learning going forward would be to try replace the functions you already have one by one with those from Ramda.
Hesitation aside, the following offers one example of what your code might look like when using various functions from Ramda.
// The existing check for `!!string.length` allows for potential issues with
// arrays being passed in and incorrectly validating, so this will ensure the
// value is indeed a string before checking its length
const stringRequired = R.both(R.is(String), R.complement(R.isEmpty))
// `addFilter` produces a function that takes an object and uses the provided
// function to validate the value associated with the provided key if it exists
// otherwise using the provided `initial` value. If valid, an object containing
// the key and value will be returned, otherwise an empty object is returned.
const addFilter = (validate, initial, key) =>
R.pipe(
R.propOr(initial, key),
R.ifElse(validate, R.objOf(key), R.always({}))
)
// `selector` takes an object and passes it to each function generated by
// calling `addFilter`, merging the resulting objects together.
const selector = (q = {}) =>
R.converge(R.merge, [
addFilter(stringRequired, '', 'name'),
addFilter(Number.isInteger, 10, 'limit')
])(q)
console.log(selector())
console.log(selector({ name: 'David Costa' }))
console.log(selector({ limit: 50 }))
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.25.0/ramda.min.js"></script>

#ngrx return one object from array with .single

I want to return an Observable of one object, selected from an array of objects from my store. I would like to use the .single operator, because this would throw an exception if there are less or more than 1 objects present. I can't get it to work though, because my store is returning an array instead of an observable. I know I could use .filter on the array, but I would like to understand why I can't get it to work. I even tried using a selector.
My App reducer has:
export interface AppState {
batStores: fromBatStores.State;
}
export const reducers: ActionReducerMap<AppState> = {
batStores: fromBatStores.batStoresReducer,
};
The reducer of my module:
export interface State {
batStores: BatStore[];
}
const initialState = {
batStores: [
new BatStore(0, 0, true, 'Cell 11', 11, 11),
new BatStore(1, 0, false, 'Cel 12', 12, 11),
new BatStore(2, 0, true, 'Cel 13', 13, 11),
new BatStore(2, 0, true, 'Cel 14', 14, 11),
]
};
export const selectBatStores = (state: fromApp.AppState) => state.batStores;
export const selectBatStoresBatStores = createSelector(selectBatStores, (state: State) => state.batStores);
Now I try something like this in my ngOnInit:
ngOnInit() {
const editId = 11;
const batStore$: Observable<BatStore> = this.store.select(fromBatStores.selectBatStoresBatStores)
.single(bs => bs.number === editId);
this.batStore$ = batStore$;
}
PhpStorm says the number property doesn't exist on BatStore[]. Which I don't understand, because at the example documentation single is also used as an operator on observable array.
Update
After reading the answers, still very confused. To test, I wrote my function like this:
getBatStore(id: number) {
const versionA$ = this.store.select(fromBatStores.selectBatStoresBatStores)
.switchMap((batStores: BatStore[]) => Observable.from(batStores))
.filter((bs: BatStore) => {
console.log('Analyzing ', bs);
console.log('Result ', bs.number === id);
return bs.number === id;
});
const versionB$ = this.store.select(fromBatStores.selectBatStoresBatStores)
.switchMap((batStores: BatStore[]) => Observable.from(batStores))
.single((bs: BatStore) => {
console.log('Analyzing ', bs);
console.log('Result ', bs.number === id);
return bs.number === id;
});
console.log('VersionA: ', versionA$);
console.log('VersionB: ', versionB$);
console.log('Same? ', versionA$ === versionB$);
return versionB$;
}
Both versions output the same thing to the console. VersionA works when using with async pipe in my view, VersionB doesn't. The code compiles and PhpStorm doesn't complain.
The select is getting a reference to the whole array and so the number property does not exist on it. Try a switchMap to switch to a new observable that expands the list so they can be selected one at a time like so:
const batStore$: Observable<BatStore> = this.store.select(fromBatStores.selectBatStoresBatStores)
.switchMap((batStores : BatStore[]) => Observable.from(batStores))
.single((bs: BatStore) => bs.number === editId);
I understand your confusion, PhpStorm is right to say "the number property doesn't exist on BatStore[]". The example documentation is also correct. The key here, in this example from RxJs, is you need to know how the Observable.from([array]) works. An Observable.from([array]) creates a "cold" observable that does one emission for every item in the array. So, the resulting Observable is of type number, not of type array, as you can see here:
const source: Observable<number> = Rx.Observable.from([1,2,3,4,5]);
The Observable that are you facing in your store, is an Observable of type Array. For example if you use the operator Observable.of, this will emit the array, not every item of the array:
const source: Observable<number[]> = Rx.Observable.of([1,2,3,4,5]);
Said that, I would do an Array.filter, for me it's better than any other transformation on the Observable stream.
Let me know if you have any doubt,
Hope this helps.
The select method will retorn an array object, for that reason you cant access the number property. You could try the following:
ngOnInit() {
const editId = 11;
this.batStore$ = this.store.select(fromBatStores.selectBatStoresBatStores)
.flatMap(list => list)// flat list to single elements, emit 1 value per element
.single(bs => bs.number === editId);
}
Another approach would be to store the elements in your state as key,value pairs and then either create a selector that takes the Id argument, or store that Id in the state and then compose 2 selectors (1 for collection of elements, 1 for Id) into one.

Categories

Resources