Proper way of updating js-data relation objects - javascript

I have a linked list of objects of the same type:
GET /cards.json
[{
"id": 1,
"lesserCardId": null,
"greaterCardId": 2,
}, {
"id": 2,
"lesserCardId": 1,
"greaterCardId": 3
}, {
"id": 3,
"lesserCardId": 2,
"greaterCardId": null
}]
Resource definition:
DS.defineResource({
name: 'card',
relations: {
belongsTo: {
card: [{
localField: 'lesserCard',
localKey: 'lesserCardId'
}, {
localField: 'greaterCard',
localKey: 'greaterCardId'
}]
}
}
});
js-data correctly reads json and creates object hierarchy. What I want next is to automatically update linked card properties like this:
var card1 = DS.get('card', 1);
var card4 = DS.inject('card', {'id': 4});
card1.greaterCard = card4;
// corresponding liked object properties should be updated automatically
expect(card4.lesserCard).toBe(card1);
expect(card2.lesserCard).toBeUndefined();
I have achieved this without js-data making custom setter like this:
Object.defineProperty(Card.prototype, 'greaterCard', {
set: function (card) {
// custom logic for updating linked list structure
if (card === this.$greaterCard) {
return;
}
this.$greaterCard.lesserCard = this.$lesserCard;
this.$greaterCard = card;
if (card) {
card.lesserCard = this;
}
// updating depending properties
this.$updateCardLevel();
this.$updateCardLevelMax();
},
get: function () {
return this.$greaterCard;
}
});
But js-data introduces it's own property descriptors for relation objects. So this approach cannot be applied. I haven't found a way to hook into js-data relation property setters. I could make a separate service with method like cardService.setGreaterCard(card, greaterCard); which would update list structure, but this wouldn't be so convenient. Is there better way for updating linked objects on property change?
js-data version: 2.9.0

In JSData 2.x you would do this:
var Card = DS.defineResource({
name: 'card',
relations: {
belongsTo: {
card: [
{
localField: 'lesserCard',
localKey: 'lesserCardId',
link: false
},
{
localField: 'greaterCard',
localKey: 'greaterCardId',
link: false
}
]
}
}
});
Object.defineProperties(Card[Card.class].prototype, {
lesserCard: {
set: function (lesserCard) {
this.lesserCardId = lesserCard.id;
lesserCard.greaterCardId = this.id;
},
get: function () {
return Card.get(this.lesserCardId);
}
},
greaterCard: {
set: function (greaterCard) {
this.greaterCardId = greaterCard.id;
greaterCard.lesserCardId = this.id;
},
get: function () {
return Card.get(this.greaterCardId);
}
}
});
In JSData 3.x you would just do this:
store.defineMapper('card', {
relations: {
belongsTo: {
card: [
{
localField: 'lesserCard',
foreignKey: 'lesserCardId',
set: function (Relation, card, lesserCard, originalSet) {
lesserCard.greaterCardId = card.id;
originalSet(lesserCard);
}
},
{
localField: 'greaterCard',
foreignKey: 'greaterCardId',
set: function (Relation, card, greaterCard, originalSet) {
greaterCard.lesserCardId = card.id;
originalSet(lesserCard);
}
}
]
}
}
});

Related

Updated nested object by matching ID

I have an array with nested objects that I need to update from another array of objects, if they match.
Here is the data structure I want to update:
const invoices = {
BatchItemRequest: [
{
bId: "bid10",
Invoice: {
Line: [
{
SalesItemLineDetail: {
ItemAccountRef: { AccountCode: "10110" },
},
},
{
SalesItemLineDetail: {
ItemAccountRef: { AccountCode: "11110" },
},
Amount: 2499,
},
],
},
},
{
bId: "bid10",
Invoice: {
Line: [
{
SalesItemLineDetail: {
ItemAccountRef: { AccountCode: "10110" },
},
},
{
SalesItemLineDetail: {
ItemAccountRef: { AccountCode: "10111" },
},
Amount: 2499,
},
],
},
},
],
};
Here is the array of objects I want to update it from:
const accounts = [
{ AccountCode: "10110", Id: "84" },
{ AccountCode: "11110", Id: "5" },
{ AccountCode: "10111", Id: "81" },
];
I want to update invoices, using accounts, by inserting Id if AccountCode matches, to get the following structure:
const invoices = {
BatchItemRequest: [
{
bId: "bid10",
Invoice: {
Line: [
{
SalesItemLineDetail: {
ItemAccountRef: { AccountCode: "10110", Id: "84" },
},
},
{
SalesItemLineDetail: {
ItemAccountRef: { AccountCode: "11110", Id: "5" },
},
Amount: 2499,
},
],
},
},
{
bId: "bid10",
Invoice: {
Line: [
{
SalesItemLineDetail: {
ItemAccountRef: { AccountCode: "10110", Id: "84" },
},
},
{
SalesItemLineDetail: {
ItemAccountRef: { AccountCode: "10111", Id: "81" },
},
Amount: 2499,
},
],
},
},
],
};
I have tried various methods, such as the following:
const mapped = invoices.BatchItemRequest.map((item1) => {
return Object.assign(
item1,
accounts.find((item2) => {
return item2 && item1.Invoice.Line.ItemAccountRef.AccountCode === item2.AccountCode;
})
);
});
Problem with this approach (it doesn't work as I think I need to do another nested map), but it also creates a new array, only including the nested elements of invoices.
Does anyone know a good approach to this?
This isn't the cleanest of code but it gets the job done:
function matchInvoiceWithAccount(invoices, accounts) {
const mappedInvoices = invoices.BatchItemRequest.map((request) => {
// Shouldn't modify input parameter, could use Object.assign to create a copy and modify the copy instead for purity
request.Invoice.Line = request.Invoice.Line.map((line) => {
const accountCode = line.SalesItemLineDetail.ItemAccountRef.AccountCode;
// If accounts was a map of AccountCode to Id you would't need to search for it which would be more effective
const account = accounts.find((account) => account.AccountCode === accountCode);
if (account) {
line.SalesItemLineDetail.ItemAccountRef.Id = account.Id;
}
return line;
});
return request;
});
return {
BatchItemRequest: mappedInvoices,
};
}
What you could and probably should do to improve this is to not modify the input parameters of the function, but that requires that you in a better way copy the original, either using Object.assign or spread operator.
At first, it will be good to create Map from your accounts array. We will go one time for array with O(n) and then will read ids by code with O(1). And nested fors is O(m*n), that will be much more slower at big arrays.
const idsByAccountCodes = new Map();
accounts.forEach((data) => {
idsByAccountCodes.set(data.AccountCode, data.Id);
})
or shorter:
const idsByAccountCode = new Map(accounts.map((data) => [data.AccountCode, data.Id]))
then if you want to mutate original values you can go through all nesting levels and add values
for ( const {Invoice:{ Line: line }} of invoices.BatchItemRequest){
for ( const {SalesItemLineDetail: {ItemAccountRef: item}} of line){
item.Id = idsByAccountCodes.get(item.AccountCode) || 'some default value'
// also if you don't have ids for all codes you need to define logic for that case
}
}
If you don't need to mutate original big object "invoices" and all of nested objects, then you can create recursive clone of if with something like lodash.cloneDeep

Update a nested object while maintaining the rest the same

I'm starting with redux, and I want to do the following modification to my
state:
From this:
state = {
loaded: true,
fetching false,
byId: {
"employeeID1": {
id: "employeeID1",
name: "Steve"
},
"employeeID2": {
id: "employeeID2",
name: "Susan"
}
}
}
To this:
{
loaded: true,
fetching false,
byId: {
"employeeID1": {
id: "employeeID1",
name: "Steve",
data: data // <---- add a new property
},
"employeeID2": {
id: "employeeID2",
name: "Susan"
}
}
}
This const modifEmployee = {...state.byId["employeeID1"], data: data} will give me the modified employee with the data.
But, how can I add the modified employee in byId while mantaining the others unchanged?
You could do something like this using spread syntax:
{
...state,
byId: {
...state.byId,
employeeID1: { ...state.byId.employeeID1, data }
}
}
If "employeeID1" value is a fetched from a variable employeeId, then you could use computed property names:
{
...state,
byId: {
...state.byId,
[employeeId]: { ...state.byId[employeeId], data }
}
}

How to create new component data based on existing in Vue.js

I have following Vue.js component:
Vue.component('channels-list', {
data() {
return {
channels: [],
}
},
methods: {
getChannels() {
this.$http.get('/channels')
.then(response => {
this.channels = response.data;
});
}
},
ready() {
this.getChannels();
}
});
Channels is just array of objects, like:
[{
"id": 1,
"title": "ANB",
"image_url": "/img/1.png",
"popular": true
}, {
"id": 2,
"title": "Roya TV",
"image_url": "/img/2.png",
"popular": false
}]
Now I want to create a new component property, for example called popularChannels, which will be used in view to display only popular channels.
I tried to do this like in other MVVM-frameworks:
data() {
return {
channels: [],
popularChannels: function() {
return this.channels.filter(function(item) {
return item.popular
});
}
}
},
but this doesn't works.
Can you please tell how to do it in Vue.js? Thank you.
If I understand you correctly what you want is a computed property.
If so, you can do it as simple as this:
data() {
return {
channels: [],
}
},
computed: {
popularChannels: function() {
return this.channels.filter(function(item) {
return item.popular
});
}
}

Pass function argument as a name of field in $set operator while using findAndModify

I've got the following function:
module.exports.saveClient = function(name) {
db.myDatabase.findAndModify({
query: { _id: 'clients' },
update: { $set: { clients: { name: {
'name': name,
'projects':{}
}
}}},
new: true
}
};
When I call function like this:
myModule.saveClient(clientName);
in $set operator it read it as:
update: { $set: { 'clients': { 'name': {
'name': clientName,
'projects':{}
}
}}}
So name of fields are converted to strings and I want the first 'name' to be 'clientName' (value passed as the argument to my function).
If you want to put the object property in a variable, then you need to use the square bracket notation to construct the field object as follows:
module.exports.saveClient = function(name) {
var clients = {};
clients[name] = {
'name': name,
'projects':{}
};
db.myDatabase.findAndModify({
query: { _id: 'clients' },
update: { $set: { clients: clients }},
new: true
}
};
Or using computed property names (ES6):
module.exports.saveClient = function(name) {
let clients = {
[name]: {
'name': name,
'projects':{}
}
}
db.myDatabase.findAndModify({
query: { _id: 'clients' },
update: { $set: { clients: clients } },
new: true
}
}

How to access data on a `through` table with Bookshelf

I am using [BookshelfJS][bookshelfjs] for my ORM and am wondering how to access data on a though table.
I have 3 Models, Recipe, Ingredient and RecipeIngredient which joins the two.
var Recipe = BaseModel.extend({
tableName: 'recipe',
defaults: { name: null },
ingredients: function () {
return this
.belongsToMany('Ingredient')
.through('RecipeIngredient')
.withPivot(['measurement']);
}
}));
var Ingredient = BaseModel.extend({
tableName: 'ingredients',
defaults: { name: null },
recipes: function () {
return this
.belongsToMany('Recipe')
.through('RecipeIngredient');
}
}));
var RecipeIngredient = BaseModel.extend({
tableName: 'recipe_ingredients',
defaults: { measurement: null },
recipe: function () {
return this.belongsToMany('Recipe');
},
ingredient: function () {
return this.belongsToMany('Ingredient');
}
}));
I then attempt to retrieve a Recipe along with all the Ingredients however cannot work out how to access measurement on the RecipeIngredient.
Recipe
.forge({
id: 1
})
.fetch({
withRelated: ['ingredients']
})
.then(function (model) {
console.log(model.toJSON());
})
.catch(function (err) {
console.error(err);
});
Return:
{
"id": 1,
"name": "Delicious Recipe",
"ingredients": [
{
"id": 1,
"name": "Tasty foodstuff",
"_pivot_id": 1,
"_pivot_recipe_id": 1,
"_pivot_ingredient_id": 1
}
]
}
With no measurement value.
I had thought that the .withPivot(['measurement']) method would have grabbed the value but it does not return any additional data.
Have I missed something or misunderstood how this works?
I'm not exactly sure why you want to use through. If it's just a basic many-to-many mapping, you can achieve this by doing the following:
var Recipe = BaseModel.extend({
tableName: 'recipe',
defaults: { name: null },
ingredients: function () {
return this
.belongsToMany('Ingredient').withPivot(['measurement']);
}
}));
var Ingredient = BaseModel.extend({
tableName: 'ingredients',
defaults: { name: null },
recipes: function () {
return this
.belongsToMany('Recipe').withPivot(['measurement']);;
}
}));
You don't need an additional model for junction table. Just be sure to define a junction table in your database as ingredients_recipe (alphabetically joining the name of tables!). Or , you can provide your own custom name to belongsToMany function for what the junction table should be named. Be sure to have ingredients_id and recipe_id in ingredients_recipe
This is pretty much it. Then you can do
Recipe
.forge({
id: 1
})
.fetch({
withRelated: ['ingredients']
})
.then(function (model) {
console.log(model.toJSON());
})
.catch(function (err) {
console.error(err);
});

Categories

Resources