I'm building a tool that allows user grab data from a generic source (with generic structure) and remap the properties to match my API.
- User will input a source URL (a REST API or a JSON);
- User will input a JSON-based DTO instruction
DTO will be a JSON like this:
{
"data.collection.name": "section.title",
"just.an.example": "data.foo",
}
and when the source provides a data with this structure:
{
"data": {
"foo": "bar",
"collection": {
"totalCount": 123,
"title": "Best sellers",
}
}
}
the functionality will remap the value to:
{
"section": {
"title": "Best sellers"
},
"just": {
"an": {
"example": "bar"
}
}
}
I could achieve this using lodash get
and set
methods, with the following algorithm:
const data = {
foo: "bar",
collection: {
title: "BestSellers",
},
just: {
an: {
example: "bar",
},
},
nodes: {
edges: [
{
title: "Orange juice",
slug: "orange-juice",
},
{
title: "Apple juice",
slug: "apple-juice",
},
],
},
};
let transformed = {};
const transformer = {
"collection.title": "section.title",
"just.an.example": "foo",
};
Object.keys(transformer).forEach((key) => {
const value = _.get(data, key);
const path = transformer[key];
_.set(transformed, path, value);
});
console.log(transformed);
Now I need to achieve the same results, but with array properties (like nodes.edges
).
User DTO JSON looking like this (not a requirement, but ideal):
{
"data.collection.name": "section.title",
"nodes.edges.title": "products.title",
"nodes.edges.slug": "products.seo.slug",
}
will generate this output:
{
"section": {
"title": "BestSellers"
},
"products": [
{
"title": "Orange Juice",
"seo": {
"slug": "orange-juice"
}
},
{
"title": "Apple Juice",
"seo": {
"slug": "apple-juice"
}
}
]
}
CodePudding user response:
There is no ambiguity about where the source arrays are, but there is ambiguity as to where exactly the destination array should be. Therefore []
is placed in the destination mapping to indicate the array position.
const data = { foo: "bar", collection: { title: "BestSellers", }, just: { an: { example: "bar", }, }, nodes: { edges: [ { title: "Orange juice", slug: "orange-juice", }, { title: "Without a slug" }, { slug: "without-a-title" }, { title: "Apple juice", slug: "apple-juice", }, ], }, };
const transformer = {
"collection.title": "section.title",
"nodes.edges.title": "products.items[].title",
"nodes.edges.slug": "products.items[].seo.slug",
};
function getAtPath(path, data) {
let s = path.split('.');
let v = data[s[0]];
if(s.length===1) return v;
let q = s.slice(1).join('.');
return Array.isArray(v) ? v.map(i=>getAtPath(q, i)) : getAtPath(q, v);
}
function setAtPath(path, value, dest) {
let s = path.split('.'), d = dest;
for(let [i,e] of s.entries()) {
if(i === s.length-1) d[e] = value;
else {
if(e.endsWith('[]')) {
e = e.substring(0, e.length-2);
value.forEach((j,k) => setAtPath(`${k}.${s.slice(i 1).join('.')}`, j, d[e]??=[]));
break;
}
else d = d[e]??={};
}
}
}
function transform(transformer, data) {
let r = {};
Object.entries(transformer).forEach(([x,y])=>setAtPath(y,getAtPath(x, data),r));
return r;
}
console.log(transform(transformer, data));
CodePudding user response:
I think the easier part of it, would be to change how you define your specific DTO, if that is still possible at this time.
If you would include in the definition that an item is expected to be an array, you can write specific handling for it.
In your example, I've changed the definition of the transformer to be:
const transformer = {
"collection.title": "section.title",
"nodes.edges.length": "products.count",
"nodes.edges[].title": "products.items[].title",
"nodes.edges[].slug": "products.items[].seo.slug",
};
And then I've changed the handling from lodash to a custom made one specific for the job (I don't know if lodash has a handling for this already, I haven't looked that part up)
Basically, if during the get, it detects that a property name has an [] near it's end, it will return a list of strings (I have not tested what happens if there are arrays in arrays, and how that would be handled), and then during the setting, it will just create an entry inside an array based on need and fill that one up.
const data = {
foo: "bar",
collection: {
title: "BestSellers",
},
just: {
an: {
example: "bar",
},
},
nodes: {
edges: [
{
title: "Orange juice",
slug: "orange-juice",
},
{
title: "Without a slug"
},
{
slug: "without-a-title"
},
{
title: "Apple juice",
slug: "apple-juice",
},
],
},
};
let transformed = {};
const transformer = {
"collection.title": "section.title",
"nodes.edges.length": "products.count",
"nodes.edges[].title": "products.items[].title",
"nodes.edges[].slug": "products.items[].seo.slug",
};
function getPropertyAndRemaindingPath( path ) {
if (!path) {
return ['', ''];
}
const [property, ...rest] = path.split('.');
return [property, rest.join('.')];
}
function getFromObject( source, property, remainder ) {
return get( source[property], remainder );
}
function getFromArray( source, property, remainder ) {
const arrayProperty = source[property.substring(0, property.length-2)];
return arrayProperty.reduce( (agg, item) => {
agg.push( get( item, remainder ) );
return agg;
}, []);
}
function getFrom( source, property, remainder ) {
return property.endsWith('[]') ?
getFromArray( source, property, remainder ) :
getFromObject( source, property, remainder );
}
function get( source, path ) {
if (!path || !source) {
return source;
}
const [property, rest] = getPropertyAndRemaindingPath( path );
return getFrom( source, property, rest );
}
function setToArray( target, property, value, remainder ) {
const targetProperty = property.substring(0, property.length-2);
if (!target[targetProperty]) {
target[targetProperty] = [];
}
const arr = target[targetProperty];
for (let idx = 0; idx < value.length; idx ) {
if (!arr[idx]) {
arr[idx] = {};
}
set( arr[idx], remainder, value[idx] );
}
}
function setToObject( target, property, value, remainder ) {
if (!target[property]) {
target[property] = {};
}
set( target[property], remainder, value );
}
function setTo( target, property, value, remainder ) {
if (property.endsWith('[]')) {
setToArray( target, property, value, remainder );
return;
}
setToObject( target, property, value, remainder );
}
function set( target, path, value ) {
if (!target) {
throw new Error('target does not exist');
}
const [property, pathRemaining] = getPropertyAndRemaindingPath( path );
if (!pathRemaining) {
target[property] = value;
return;
}
setTo( target, property, value, pathRemaining );
}
const _ = {
get,
set
};
Object.keys(transformer).forEach((key) => {
const value = _.get(data, key);
const path = transformer[key];
_.set(transformed, path, value);
});
console.log(transformed);