Updating records and their associations with sequelize - sqlite

I have the following models defined in sequelize:
Library, MediaParentDir, MediaSubDir and MediaSubDirEpisodes
The first three hold information about directories on the system and the last one holds information about files in a particular directory on the system.
The associations are as follows:
Library.MediaParentDirs = Library.hasMany(models.MediaParentDir, {onDelete: 'CASCADE'});
Library.MediaSubDirs = Library.hasMany(models.MediaSubDir, {onDelete: 'CASCADE'});
MediaParentDir.MediaSubDirs = MediaParentDir.hasMany(models.MediaSubDir, {onDelete: 'CASCADE'});
MediaSubDir.Episodes = MediaSubDir.hasMany(models.Episode, {onDelete: 'CASCADE'});
And this is how I populate the database on first run:
db.Library.find({
where: lib
}).then((existingLib) => {
let includes = [{
model: db.MediaParentDir,
include: [{
model: db.MediaSubDir,
include: [db.Episode]
}]
},
{
model: db.MediaSubDir,
include: [db.Episode]
}
];
let mediaParentDirs = removeIgnored(library.getMediaParentDirsFrom(lib))
.map((parentDir) => {
parentDir.MediaSubDirs = removeIgnored(library.getMediaSubDirsFrom(parentDir));
parentDir.MediaSubDirs.map((subDir) => {
subDir.Episodes = removeIgnored(library.getMediaSubDirEpisodesFrom(subDir));
return subDir;
});
return parentDir;
});
let mediaSubDirs = removeIgnored(library.getMediaSubDirsFrom(lib))
.map((subDir) => {
subDir.Episodes = removeIgnored(library.getMediaSubDirEpisodesFrom(subDir));
return subDir;
});
let updatedLib = db.Library.build({
name: lib.name,
path: lib.path,
type: lib.type,
// Add all media parent dirs and child sub dirs under media parent dirs
MediaParentDirs: mediaParentDirs,
// Add all media sub dirs directly under library
MediaSubDirs: mediaSubDirs,
}, {
include: includes
});
if (!existingLib)
return updatedLib.save();
// Record already exists. Update library data.
});
In the code above, I'm reading the library directory and gathering all the information about MediaParentDirs and other models mentioned previously. Finally, I build a Library instance with all the nested associations defined.
Now, if a library already exists, I need to update the data associated to it and its models. I already tried a few things:
Library.upsert() but this doesn't update the associations.
Library.update() same as above.
embed.update() from https://github.com/Wsiegenthaler/sequelize-embed but this requires me to supply object IDs explicitly
Is there any other way I could update the associated model instances?
Any help would be appreciated. Thanks.

Sequelize automatically adds setter methods to models that can be used to update associated data in the database.
Following is the code that I use to list the methods. Add this in index.js after associations are defined and restart node server.
for (let assoc of Object.keys(db[modelName].associations)) {
for (let accessor of Object.keys(db[modelName].associations[assoc].accessors)) {
console.log(db[modelName].name + '.' + db[modelName].associations[assoc].accessors[accessor]+'()');
}

Related

How to structurally compare the previous and new value of nested objects that are being used in `watch`, in Options API?

I have a question which is a mix of both composition API and options API
What I want to do: I want to watch an object. That object is deeply nested with all kinds of data types.
Whenever any of the nested properties inside change, I want the watch to be triggered.
(This can be done using the deep: true option).
AND I want to be able to see the previous value and current value of the object.
(this doesn't seem to be possible because Vue stores the references of the objects, so, now the value and prevValue point to the same thing.)
In Vue3 docs, for the watch API, it says this
However, watching a reactive object or array will always return a reference to the
current value of that object for both the current and previous value of the state.
To fully watch deeply nested objects and arrays, a deep copy of values may be required.
This can be achieved with a utility such as lodash.cloneDeep
And this following example is given
import _ from 'lodash'
const state = reactive({
id: 1,
attributes: {
name: ''
}
})
watch(
() => _.cloneDeep(state),
(state, prevState) => {
console.log(state.attributes.name, prevState.attributes.name)
}
)
state.attributes.name = 'Alex' // Logs: "Alex" ""
Link to docs here - https://v3.vuejs.org/guide/reactivity-computed-watchers.html#watching-reactive-objects
However, this is composition API (if I'm not wrong).
How do I use this way of using cloneDeep in a watch defined in options API?
As an example, this is my code
watch: {
items: {
handler(value, prevValue) {
// check if value and prevValue are STRUCTURALLY EQUAL
let isEqual = this.checkIfStructurallyEqual(value, prevValue)
if (isEqual) return
else this.doSomething()
},
deep: true,
},
}
I'm using Vue 3 with Options API.
How would I go about doing this in Options API?
Any help would be appreciated! If there's another way of doing this then please do let me know!
I also asked this question on the Vue forums and it was answered.
We can use the same syntax as provided in the docs in Options API using this.$watch()
data() {
id: 1,
attributes: {
name: ''
}
}
this.$watch(
() => _.cloneDeep(this.attributes),
(state, prevState) => {
console.log(state.name, prevState.name)
}
)
this.attributes.name = 'Alex' // Logs: "Alex" ""

How do I handle values that are dependent on the result of a helper?

So I have an Angular controller with a meteor helper method, as below.
function localeCtrl($scope, $reactive, $stateParams{
$reactive(this).attach($scope);
var self = this;
self.helpers({
locale: function(){ return Locales.findOne($stateParams.id)},
staff: function(){
// Load data from second collection based on current Locale.
// But how?
},
address: function(){
// Take self.location.address and massage it to provide
// google maps link. How?
}
tags: function(){
// Collect all unique instances of a given tag by
// iterating over the available locales.
// E. G. If 10 locales have the 'restaurant' tag, and 5
// more have the 'library' tag, I want an array of
// ['restaurant', 'library'] -- easy enough to do
// by iterating over the locales, but how do I do that
// reactively?
}
});
}
Unfortunately, I need to set additional properties based on the data fetched by locale(). I can't set them up when I initialize the controller because the value in locale() changes as data is fetched from the server. But I need access to the data in locale to, for example, create the google maps address, or fetch associated records. (They aren't imbedded in the locale document for reasons that I'm sure made sense at the time).
Edit:
Additionally, I'm using ground DB to store a local copy of the data for offline access, which makes life even more complicated.
Probably you best bet is to publish your collection using publishComposite which is implemented using the reywood:publish-composite package.
Add the package:
meteor add reywood:publish-composite
Now where you publish the Locales collection you would do something like this:
Meteor.publishComposite('locales', function() {
return {
find() {
//Put whatever you need in the query for locales
const query = {
_userId: this.userId
};
return Locales.find(query);
},
children: [{
find(locale) {
return Staff.find({ localeId: locale._id });
}
}]
};
});
Then in your controller before the helper you add this:
this.subscribe('locales');
Now you should be able to simply call the code like this:
this.helpers({
locale(){
return Locales.findOne(this.$stateParams.id);
}
});
And access it in the template like this:
locale.staff
Give that a try and let me know!

Sails js 2:n relation

I recently started working with SailsJS, and I found the process of defining attributes and associations very elegant. However, there is one thing I cannot find out:
I have a model Couple(person1, person2). I'd like to create an association in Person, that lists every couple they are a member of. Something along the lines of:
attributes{
//other attributes,
couples:{
collection: 'couple',
via: 'person1, person2'
}
}
My current method is:
attributes{
//other attributes,
couples1:{
collection: 'couple',
via: 'person1'
},
couples2:{
collection: 'couple',
via: 'person2'
},
getCouples: function() {
return this.couples1 + this.couples2;
}
}
But this doesn't seem very pretty, is there a way to do it more elegantly?
If i get your question correctly, you want a person and its couple, i'm assuming you have a monogamous relationship in your couples (meaning one person belongs to the other and vice-versa) you can make an association with the same model, like this.
var Person = {
attributes: {
name: {type:'string'},
couple: {model: 'Person', via: 'couple'}
}
};
module.exports = Person
Then you can create and associate them like this. (Assuming sails console)
Person.create({name: 'John Doe', couple: {name: 'Jane Doe'} }).exec(console.log)
Person.update(2, {couple:1}).exec(console.log)
And retrieving them is as easy as
Person.find({name:'John Doe'}).populate('couple').exec(console.log)
You can try to associate them with .add and .save but i'm not sure if it works when the association points to the same model, so check that out.
Another option is to have it like this:
var Person = {
attributes: {
name: {type:'string'},
couple: {model: 'Couple', via: 'persons'}
}
};
module.exports = Person;
Couple model holds a collection of persons, so you can add persons to the
couple model, and associate them later with the person.couple attribute.
var Couple = {
attributes: {
persons: {collection: 'Person', via: 'couple'}
}
};
module.exports = Couple;
Or you could use what you have, which is not ideal in this case, but it works.

Meteor: Publish a subset of another publication

I have a custom publication on my server (which in some way join 2 collections).
This resulting set of this publication is exactly what I need but for performances issues I would like to avoid sending it entirely to the client.
If I did not care about performances, I would only subscribe to the
publication and do something like
theCollection.find({"my":"filter"})
I am therefore trying to find a way to publish a subset of the custom publication so that the filter would be applied on the custom publication on the server side.
Is there a way to chain or filter publications (server side) ?
For the question we can assume the custom publication to look like this and cannot be modified:
Meteor.publish('customPublication', function() {
var sub = this;
var aCursor = Resources.find({type: 'someFilter'});
Mongo.Collection._publishCursor(aCursor, sub, 'customPublication');
sub.ready();
});
if i understand the question right, you are looking for https://atmospherejs.com/reywood/publish-composite
It let's you "publish a set of related documents from various collections using a reactive join. This makes it easy to publish a whole tree of documents at once. The published collections are reactive and will update when additions/changes/deletions are made."
Ok I came to the following workaround. Instead of working on the publication, I simply added a new collection I update according to the other collections. In order to do so I am using the meteor hooks package
function transformDocument(doc)
{
doc.aField = "aValue"; // do what you want here
return doc;
}
ACollection.after.insert(function(userId, doc)
{
var transformedDocument = transformDocument(doc);
AnotherCollection.insert(transformedDocument);
});
ACollection.after.update(function(userId, doc, fieldNames, modifier, options)
{
var transformedDocument = transformDocument(doc);
delete transformedDocument._id;
AnotherCollection.update(doc._id,{$set:transformedDocument});
});
ACollection.after.remove(function(userId, doc)
{
AnotherCollection.remove(doc._id);
});
Then I have the new collection I can publish subsets the regular way
Benefits:
You can filter whatever you want into this db, no need to worry if the field is virtual or real
Only one operation every time a db changes. This avoid having several publication merging the same data
Cave eats:
This requires one more Collection = more space
The 2 db might not be always synchronised, there is few reasons for this:
The client manually changed the data of "AnotherCollection"
You had documents in "ACollection" before you added "AnotherCollection".
The transform function or source collection schema changed at some point
To fix this:
AnotherCollection.allow({
insert: function () {
return Meteor.isServer;
},
update: function () {
return Meteor.isServer;
},
remove: function () {
return Meteor.isServer;
}
});
And to synchronise at meteor startup (i.e. build the collection from scratch). Do this only once for maintenance or after adding this new collection.
Meteor.startup(function()
{
AnotherCollection.remove({});
var documents = ACollection.find({}).fetch();
_.each(documents, function(doc)
{
var transformedDocument = transformDocument(doc);
AnotherCollection.insert(transformedDocument);
});
});

How to 'transform' data returned via a Meteor.publish?

Meteor Collections have a transform ability that allows behavior to be attached to the objects returned from mongo.
We want to have autopublish turned off so the client does not have access to the database collections, but we still want the transform functionality.
We are sending data to the client with a more explicit Meteor.publish/Meteor.subscribe or the RPC mechanism ( Meteor.call()/Meteor.methods() )
How can we have the Meteor client automatically apply a transform like it will when retrieving data directly with the Meteor.Collection methods?
While you can't directly use transforms, there is a way to transform the result of a database query before publishing it. This is what the "publish the current size of a collection" example describes here.
It took me a while to figure out a really simple application of that, so maybe my code will help you, too:
Meteor.publish("publicationsWithHTML", function (data) {
var self = this;
Publications
.find()
.forEach(function(entry) {
addSomeHTML(entry); // this function changes the content of entry
self.added("publications", entry._id, entry);
});
self.ready();
});
On the client you subscribe to this:
Meteor.subscribe("publicationsWithHTML");
But your model still need to create a collection (on both sides) that is called 'publications':
Publications = new Meteor.Collection('publications');
Mind you, this is not a very good example, as it doesn't maintain the reactivity. But I found the count example a bit confusing at first, so maybe you'll find it helpful.
(Meteor 0.7.0.1) - meteor does allow behavior to be attached to the objects returned via the pub/sub.
This is from a pull request I submitted to the meteor project.
Todos = new Meteor.Collection('todos', {
// transform allows behavior to be attached to the objects returned via the pub/sub communication.
transform : function(todo) {
todo.update = function(change) {
Meteor.call('Todos_update', this._id, change);
},
todo.remove = function() {
Meteor.call('Todos_remove', this._id);
}
return todo;
}
});
todosHandle = Meteor.subscribe('todos');
Any objects returned via the 'todos' topic will have the update() and the remove() function - which is exactly what I want: I now attach behavior to the returned data.
Try:
let transformTodo = (fields) => {
fields._pubType = 'todos';
return fields;
};
Meteor.publish('todos', function() {
let subHandle = Todos
.find()
.observeChanges({
added: (id, fields) => {
fields = transformTodo(fields);
this.added('todos', id, fields);
},
changed: (id, fields) => {
fields = transformTodo(fields);
this.changed('todos', id, fields);
},
removed: (id) => {
this.removed('todos', id);
}
});
this.ready();
this.onStop(() => {
subHandle.stop();
});
});
Currently, you can't apply transforms on the server to published collections. See this question for more details. That leaves you with either transforming the data on the client, or using a meteor method. In a method, you can have the server do whatever you want to the data.
In one of my projects, we perform our most expensive query (it joins several collections, denormalizes the documents, and trims unnecessary fields) via a method call. It isn't reactive, but it greatly simplifies our code because all of the transformation happens on the server.
To extend #Christian Fritz answer, with Reactive Solution using peerlibrary:reactive-publish
Meteor.publish("todos", function() {
const self = this;
return this.autorun(function(computation) {
// Loop over each document in collection
todo.find().forEach(function(entry) {
// Add function to transform / modify each document here
self.added("todos", entry._id, entry);
});
});
});

Resources