Add multiple records to model's collection Sailsjs - javascript

I have the following models in my Sailsjs application with a many-to-many relationship:
event.js:
attributes: {
title : { type: 'string', required: true },
description : { type: 'string', required: true },
location : { type: 'string', required: true },
maxMembers : { type: 'integer', required: true },
currentMembers : { collection: 'user', via: 'eventsAttending', dominant: true },
creator : { model: 'user', required: true },
invitations : { collection: 'invitation', via: 'eventID' },
tags : { collection: 'tag', via: 'taggedEvents', dominant: true },
lat : { type: 'float' },
lon : { type: 'float' },
},
tags.js:
attributes: {
tagName : { type: 'string', unique: true, required: true },
taggedEvents : { collection: 'event', via: 'tags' },
},
Based on the documentation, this relationship looks correct. I have the following method in tag.js that accepts an array of tag strings, and an event id, and is supposed to add or remove the tags that were passed in:
modifyTags: function (tags, eventId) {
var tagRecords = [];
_.forEach(tags, function(tag) {
Tag.findOrCreate({tagName: tag}, {tagName: tag}, function (error, result) {
tagRecords.push({id: result.id})
})
})
Event.findOneById(eventId).populate('tags').exec(function(error, event){
console.log(event)
var currentTags = event.tags;
console.log(currentTags)
delete currentTags.add;
delete currentTags.remove;
if (currentTags.length > 0) {
currentTags = _.pluck(currentTags, 'id');
}
var modifiedTags = _.pluck(tagRecords, 'id');
var tagsToAdd = _.difference(modifiedTags, currentTags);
var tagsToRemove = _.difference(currentTags, modifiedTags);
console.log('current', currentTags)
console.log('remove', tagsToRemove)
console.log('add', tagsToAdd)
if (tagsToAdd.length > 0) {
_.forEach(tagsToAdd, function (tag) {
event.tags.add(tag);
})
event.save(console.log)
}
if (tagsToRemove.length > 0) {
_.forEach(tagsToRemove, function (tagId) {
event.tags.remove(tagId)
})
event.save()
}
})
}
This is how the method is called from the event model:
afterCreate: function(record, next) {
Tag.modifyTags(tags, record.id)
next();
}
When I post to event/create, I get this result: http://pastebin.com/PMiqBbfR.
It looks as if the method call itself is looped over, rather than just the tagsToAdd or tagsToRemove array. Whats more confusing is that at the end, in the last log of the event, it looks like the event has the correct tags. When I then post to event/1, however, the tags array is empty. I've also tried saving immediately after each .add(), but still get similar results.
Ideally, I'd like to loop over both the tagsToAdd and tagsToRemove arrays, modify their ids in the model's collection, and then call .save() once on the model.
I have spent a ton of time trying to debug this, so any help would be greatly appreciated!

There are a few problems with your implementation, but the main issue is that you're treating certain methods--namely .save() and .findOrCreate as synchronous methods, when they are (like all Waterline methods) asynchronous, requiring a callback. So you're effectively running a bunch of code in parallel and not waiting for it to finish before returning.
Also, since it seems like what you're trying to do is replace the current event tags with this new list, the method you came up with is a bit over-engineered--you don't need to use event.tags.add and event.tags.remove. You can just use plain old update.
So you could probably rewrite the modifyTags method as:
modifyTags: function (tags, eventId, mainCb) {
// Asynchronously transform the `tags` array into an array of Tag records
async.map(tags, function(tag, cb) {
// For each tag, find or create a new record.
// Since the async.map `cb` argument expects a function with
// the standard (error, result) node signature, this will add
// the new (or existing) Tag instance to the resulting array.
// If an error occurs, async.map will exit early and call the
// "done()" function below
Tag.findOrCreate({tagName: tag}, {tagName: tag}, cb);
}, function done (err, tagRecords) {
if (err) {return mainCb(err);}
// Update the event with the new tags
Event.update({id: eventId}, {tags: tagRecords}).exec(mainCb);
});
}
See the full docs for async.map here.
If you wanted to stick with your implementation using .add and .remove, you would still want to use async.map, and do the rest of your logic in the done method. You don't need two .save calls; just do run all the .add and .remove code first, then do a single .save(mainCb) to finish it off.
And I don't know what you're trying to accomplish by deleting the .add and .remove methods from currentTags (which is a direct reference to event.tags), but it won't work and will just cause confusion later!

Related

sequelize saving multiple objects that assosiates

I wasnt quite sure what to call this question but here is my setup:
var AcademyModule = sequelize.define('academy_module', {
academy_id: {
type: DataTypes.INTEGER,
primaryKey: true
},
module_id: {
type: DataTypes.INTEGER,
primaryKey: true
},
module_module_type_id: DataTypes.INTEGER,
sort_number: DataTypes.INTEGER,
requirements_id: DataTypes.INTEGER
}, {freezeTableName: true,}
With the following assosiation:
Requirements = sequelize.define('requirements', {
id: DataTypes.INTEGER,
value: DataTypes.STRING,
requirement_type_id: DataTypes.INTEGER
}, {freezeTableName: true});
AcademyModule.belongsTo(Requirements, {foreignKey: 'requirements_id'});
Now as you can see from my table setup i would have to save a row in requirements table and then use the inserted id to insert into the academy_module table.
for this i created the following:
add: function (requirements,academyModule,onSuccess, onError) {
var academyModule = academyModule;
if(requirements == null) {
AcademyModule.build(this.dataValues)
.save().ok(onSuccess).error(onError);
} else {
if(requirements.requirement_type_id == '1') {
requirements.value = requirements.module.id;
}
Requirements.create(requirements).then(function(createdReq) {
var associationPromises = [];
associationPromises.push(AcademyModule.create(this.dataValues));
return sequelize.Promise.all(associationPromises);
}).success(onSuccess).error(onError);
}
}
However in the then function i am unable to reach the academyModule object that contains the values that needs to be inserted.
This is a repeating problem for me and i really want to know how it is possible to connect so they do it automatically without doing small hacks
i have scouted the documentations but i havnt been able to find a single example of the above (which i find rather odd seeing as this is a fairly normal situation)
Jan's method
I tried to solve it using Jan's elegant method
However i am getting an error saying:
academy_module is not associated to requirements!
My code looks like this as for now:
var academyModule = academyModule;
if(requirements == null)
{
AcademyModule.build(this.dataValues)
.save().ok(onSuccess).error(onError);
}
else
{
requirements.academy_module = academyModule;
Requirements.create(requirements, {
include: [AcademyModule]
});
}
Funny thing here is t hat i have the assosiation:
AcademyModule.belongsTo(Requirements, {foreignKey: 'requirements_id'});
Requirements.hasOne(AcademyModule, {foreignKey: 'requirements_id'});
First of all, I cannot answer why you are having problems accessing academyModule - it should be available via simple javascript scoping.
There are some unexplained bits of your code... You are pushing a single promise to associationPromises - Why not just return that promise?
You are accessing this.dataValues to create an AcademyModule instance - this will normally be undefined inside a promise handler.
Your issue could be solved more elegantly with nested creation, which is in 2.0.5: https://github.com/sequelize/sequelize/pull/3386
First of all you need to create the reverse association from Requirements --> AcademyModule
Requirements.hasOne(AcademyModule, {foreignKey: 'requirements_id'});
This is needed because the Requirement is the 'main model', which is used to create the AcademyModule
requirements.academy_module = academy_module;
Requirements.create(requirements, {
include: [AcademyModule]
});
By setting academy_module on the requirements object, and adding include as the 2nd parameter (similar to how you use includes for find) both are created and associated in one go.
You'll need to modify your models slightly. With your current model definition the table will contain an id column, but sequelzie does not know that it is the primary key. You either need to remove id completely from the Requirements model (in which case sequelize will add it automatically and mark it as primary key), or add primaryKey to it:
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true // optional - only if you actually want AI :)
}

PersistenceJS: Updating an existing database using migrate()

I'm currently trying to make changes to an existing DB using the migrations plugin for PersistenceJS. I can add/edit/delete items in the DB just fine — but…
How to add a column to an existing(!) table?
How to change the type of an existing(!) column, e.g. from 'text' to 'integer'?
These changes should retain currently existing data.
Sadly, the documentation is a little scarce, maybe you could help?
Here's the current, working setup:
persistence.store.websql.config(persistence, 'tododatabase', 'todos are fun', 5*1024*1024);
var Todo = persistence.define('Todo', {
task: 'TEXT',
priority: 'INT',
done: 'BOOL'
});
persistence.schemaSync();
function addTodo( item ){
var todo = new Todo();
todo.task = item.task;
todo.priority = item.priority;
todo.done = item.done;
persistence.add(todo);
persistence.flush();
};
function deleteTodo( item, callback ){
// item.id was created automatically by calling "new Todo()"
Todo.all().filter('id','=', item.id ).destroyAll( function(){
persistence.flush( callback );
});
};
The migration code that kinda works:
persistence.defineMigration(1, {
up: function() {
this.createTable('Todo', function(t){
t.text('task');
t.integer('priority');
t.boolean('done');
});
},
down: function() {
this.dropTable('Todo');
}
});
persistence.defineMigration(2, {
up: function() {
this.addColumn('Todo', 'due', 'DATE');
},
down: function() {
this.removeColumn('Todo', 'due');
}
});
function migrate( callback ){
console.log('migrating...');
persistence.migrations.init( function(){
console.log('migration init');
// this should migrate up to the latest version, in our case: 2
persistence.migrate( function(){
console.log('migration complete!');
} );
});
}
Results…
calling migrate() will only log up to "migration init", the complete handler is never called, the "due" column is not created
not calling schemaSync() before calling migrate() as Zef Hemel himself proposed in this post yields the same result as 1.
changing the first line to persistence.store.websql.config(persistence, 'newdatabase', 'testing migration', 5*1024*1024);, not calling schemaSync() and only calling migrate() will successfully log "migration complete!" — but it does so in a new, completely empty database "newdatabase", which will of course not retain any exsiting data.
Summary
There is a database that was created using persistence.store.websql.config(...), persistence.define('Todo',...) and persistence.schemaSync().
I now want to keep all the data that already exist in that database, but want to
change the type of column priority from 'integer' to 'text'
add a column due with type 'date' to all existing Todos
If you could push me in the right direction, I'd greatly appreciate it!
Thanks!
I finally got it working. There are a number of issues with my initial requirements that I'd like to point out for future reference. Take a look at the first migration definition:
persistence.defineMigration(1, {
up: function() {
this.createTable('Todo', function(t){
...
Not surprisingly, createTable will do exactly that: it will execute the SQL statement 'CREATE TABLE Todo ...', which will silently fail and halt the migration if there is a table with the name Todo already. This is why it worked with a new database, but not with the existing one. Bear in mind: I already had a live database with a table "Todo" that needed updating. If you're starting fresh (i.e. you've not used schemaSync), createTable works just fine. Since the Migrations plugin does not provide a createTableIfNotExists method, I needed to utilize executeSql as follows:
persistence.defineMigration(1, {
up: function() {
this.executeSql('CREATE TABLE IF NOT EXISTS Todo (id VARCHAR(32) PRIMARY KEY, task TEXT, priority INT, done BOOL)');
...
Now that the migration from schema version 0 to 1 succeeded, the migration to version 2 was successful as well.
With the migration to version 3 the type of the priority column needed to change from int to text. This would normally be done using the ALTER COLUMN SQL command, wich is not supported by Web SQL / SQLite. See Omitted Features for SQLite.
Altering a column with SQLite requires a 4-step workaround:
persistence.defineMigration(3, {
up: function() {
// rename current table
this.executeSql('ALTER TABLE Todo RENAME TO OldTodo');
// create new table with required columns and column types
this.executeSql('CREATE TABLE Todo (id VARCHAR(32) PRIMARY KEY, task TEXT, priority TEXT, done BOOL)');
// copy contents from old table to new table
this.executeSql('INSERT INTO Todo(id, task, priority, done) SELECT id, task, priority, done FROM OldTodo');
// delete old table
this.executeSql('DROP TABLE OldTodo');
},
...
Of course, after changing the column type, the entity definition for 'Todo' should also be changed:
var Todo = persistence.define('Todo', {
task: 'TEXT',
priority: 'TEXT', // was 'INT'
due: 'DATE',
done: 'BOOL'
});
And finally, the complete source:
persistence.store.websql.config(persistence, 'tododatabase', 'todos are fun', 5*1024*1024);
// persistence.debug = true;
//v0 + v1
// var Todo = persistence.define('Todo', {
// task: 'TEXT',
// priority: 'INT',
// done: 'BOOL'
// });
//v2
// var Todo = persistence.define('Todo', {
// task: 'TEXT',
// priority: 'INT',
// due: 'DATE',
// done: 'BOOL'
// });
//v3
var Todo = persistence.define('Todo', {
task: 'TEXT',
priority: 'TEXT',
due: 'DATE',
done: 'BOOL'
});
persistence.defineMigration(1, {
up: function() {
this.executeSql('CREATE TABLE IF NOT EXISTS Todo (id VARCHAR(32) PRIMARY KEY, task TEXT, priority INT, done BOOL)');
},
down: function() {
this.dropTable('Todo');
}
});
persistence.defineMigration(2, {
up: function() {
this.addColumn('Todo', 'due', 'DATE');
},
down: function() {
this.removeColumn('Todo', 'due');
}
});
persistence.defineMigration(3, {
up: function() {
// rename current table
this.executeSql('ALTER TABLE Todo RENAME TO OldTodo');
// create new table with required columns
this.executeSql('CREATE TABLE Todo (id VARCHAR(32) PRIMARY KEY, task TEXT, priority TEXT, due DATE, done BOOL)');
// copy contents from old table to new table
this.executeSql('INSERT INTO Todo(id, task, priority, due, done) SELECT id, task, priority, due, done FROM OldTodo');
// delete current table
this.executeSql('DROP TABLE OldTodo');
},
down: function() {
this.executeSql('ALTER TABLE Todo RENAME TO OldTodo');
this.executeSql('CREATE TABLE Todo (id VARCHAR(32) PRIMARY KEY, task TEXT, priority INT, due DATE, done BOOL)');
this.executeSql('INSERT INTO Todo(id, task, priority, due, done) SELECT id, task, priority, due, done FROM OldTodo');
this.executeSql('DROP TABLE OldTodo');
}
});
function migrate( callback ){
console.log('migrating...');
persistence.migrations.init( function(){
console.log('migration init');
persistence.migrate( function(){
console.debug('migration complete!');
callback();
} );
});
};
migrate( onMigrationComplete );
function onMigrationComplete(){
// database is ready. do amazing things...
};
That's a great explanation, thank you! But I think I know an easier way to achieve this.
I got in the same trouble like you: I'v got a set of schemas described with persistence.define and created with persistence.schemaSync.
So this is my particular case:
// This is my mixin for all schemas
var Versioned = persistence.defineMixin('Versioned', {
serverId: "TEXT",
intVersion: "INT",
dtSynced: "DATE",
dtCreatedAt: "DATE",
dtUpdatedAt: "DATE",
delete: "BOOL",
update: "BOOL",
add: "BOOL",
isReadOnly: "BOOL"
});
// This is one of the schemas I need to update with a new field.
var Person = persistence.define('Person', {
fullName: "TEXT",
rate: "INT"
});
//... More schema definitions
// Setup mixin
Person.is(Versioned);
// Sync schemas
persistence.schemaSync();
Ok. Nothing special about it. Now after a few months my app's being in production I want to add a new field isEmployed to the Person schema.
According to the docs I should rewrite all of my schema definitions to the migrations and to stop using persistence.schemaSync(). But I don't want to rewrite all of my definitions. Instead of it I define a new migration right behind the PersistenceJS init code:
// Init ORM
persistence.store.websql.config(
persistence,
'Sarafan',
'0.0.2',
'Sarafan.app database',
100 * 1024 * 1024,
0
);
// Define Migrations
persistence.defineMigration(1, {
up: function () {
this.addColumn('Person', 'isEmployed', 'BOOL');
}
});
// ... describing isVersioned mixin
// Updated schema definition with a new field 'isEmployed'
var Person = persistence.define('Person', {
fullName: "TEXT",
rate: "INT",
isEmployed: "BOOL"
});
//... More schema definitions
// Setup mixin
Person.is(Versioned);
// Apply the migration right away from the schemaSync call.
persistence.schemaSync(function (tx) {
persistence.migrations.init(function () {
persistence.migrate(function(){
// Optional callback to be executed after initialization
});
});
});
So that's it! I tested this approach only to add new fields to the schema.
Let me know if it does or doesn't work for you.

Backbone not making a put request with save() after save

I am experiencing a really interesting problem with backbone, I have a function like this in one of my views:
addpeople :function(){
var authArray = _.clone(this.model.get("authorizedUsers"));
var values = $(".add-input").val().split(",");
values.forEach(function(value) {
authArray.push(value);
});
this.model.set("authorizedUsers" , authArray);
this.model.save();
},
this function gets called when a button is clicked. This version of the function triggers a change event because I am cloning my array, but for some reason this.model.save()never gets called, aka the server never receives a PUT request. When I refresh the page I go back to the old state of the model..
However if I dont clone the array and change the function to, this:
addpeople :function(){
var authArray = this.model.get("authorizedUsers");
var values = $(".add-input").val().split(",");
values.forEach(function(value) {
authArray.push(value);
});
this.model.set("authorizedUsers" , authArray);
this.model.save();
},
This time the PUT request is sent successfully, but the page is not re-rendered because a change event is not triggered. When I refresh the page I can see the updated model..
I know that I can manually trigger a change event in the second example but I am more curious about why my this.model.save() is not called in the first example..
To help you understand the problem more my model looks something like:
var PostModel = Backbone.Model.extend({
urlRoot : '/tweet',
idAttribute: '_id',
defaults:{
name: '',
comments: [],
tags: [],
authorizedUsers: [],
postedBy : '',
dateCreated: ''
},
});
and my node.js update function looks like:
exports.updateTweet = function(req,res){
console.log("Getting called ! ")
var update = req.body;
var id = req.url.split("/")[2];
Post.update({ _id: id }, { $set: { authorizedUsers: req.body.authorizedUsers }}, function (err, post) {
if (err) return handleError(err);
});
res.end();
};
The reason why change didn't trigger for your second example is because it is the same object and Backbone ignore it. Hence no change event triggered.
As for why the first one failed; do you have validator for your model? May be something that validating for empty string perhaps? val() can return an empty string and split() on empty string will return [""]
Also, your defaults should be a function otherwise all your model would have the same instance of comments, tags and authorizedUsers
From Backbone doc.
Remember that in JavaScript, objects are passed by reference, so if you include an object as a default value, it will be shared among all instances. Instead, define defaults as a function.
Arrays are object too.
var PostModel = Backbone.Model.extend({
urlRoot : '/tweet',
idAttribute: '_id',
defaults: function(){
return {
name: '',
comments: [],
tags: [],
authorizedUsers: [],
postedBy : '',
dateCreated: ''
}
}
});
Lastly, array.forEach() is not available on IE8 and older.

Sencha touch store - phantom data

I created a model like
Ext.define('MyApp.model.ContainerDetailsModel', {
extend: 'Ext.data.Model',
alias: 'model.ContainerDetailsModel',
config: {
fields: [
{
name: 'id',
allowNull: false,
type: 'string'
},
{
name: 'container_types_id',
type: 'string'
}
]
}
});
and a store like this
Ext.define('MyApp.store.ContainerDetailsStore', {
extend: 'Ext.data.Store',
requires: [
'MyApp.model.ContainerDetailsModel'
],
config: {
model: 'MyApp.model.ContainerDetailsModel',
storeId: 'ContainerDetailsStore',
proxy: {
type: 'ajax',
enablePagingParams: false,
url: 'hereIsServiceUrl',
reader: {
type: 'json'
}
}
}
});
Now somewhere in application I tried to get one record like:
var detailsStore = Ext.getStore("ContainerDetailsStore");
detailsStore.load();
var detailsRecord = detailsStore.last();
But it gaves me undefined. The json returned by service is ok, it use it in different place as source for list. I already tried to change allowNull to true, but there is no null id in source. I tried set types to 'int' with the same result.
So I have tried
console.log(detailsStore);
Result is like this (just important values):
Class {
...
loaded: true,
data: Class {
...
all: Array[1] {
length: 1,
0: Class {
container_types_id: "1",
id: "726",
....
}
...
}
...
},
...
}
In the same place
console.log(detailsStore.data);
returns (as it should):
Class {
...
all: Array[1] {
length: 1,
0: Class {
container_types_id: "1",
id: "726",
....
}
...
}
but (next line)
console.log(detailsStore.data.all);
returns
[]
And it's empty array. When i try any methods from the store it says the store is empty.
I wrote console.log() lines one after another - so for sure it doesn't change between them (I try it also in different order or combinations).
My browser is Google Chrome 23.0.1271.97 m
I use Sencha from https://extjs.cachefly.net/touch/sencha-touch-2.0.1.1/sencha-touch-all-debug.js
How can I take a record from that store?
store.load() Loads data into the Store via the configured proxy. This uses the Proxy to make an asynchronous call to whatever storage backend the Proxy uses, automatically adding the retrieved instances into the Store and calling an optional callback if required. The method, however, returns before the datais fetched. Hence the callback function, to execute logic which manipulates the new data in the store.
Try,
detailsStore.load({
callback: function(records, operation, success) {
var detailsRecord = detailsStore.last();
},
scope: this
});

Backbone model.save() is sending PUT instead of POST

I call save using this:
console.log(this.model.isNew());
console.log(this.model);
this.model.save({}, {
success: function (model, response, options) {
console.log(response);
},
error: function (model, xhr, options) {
console.log(xhr.result.Errors);
}
});
The isNew() returns false. But the output of this.model has an ID of 0. (this.model.id is 0 as well)
My url is url: ROOTAREA + "/Expenses/Entry/",
Updating works fine, and uses PUT as expected.
Edit : here's part of my model:
defaults: function () {
return {
DocumentDate: "",
JobNo_: "",
PhaseCode: "",
WorkTypeCode: "",
Description: "",
Quantity: 0,
UnitCost: 0,
ExpenseCurrencyCode: "",
ReimbursementCurrencyCode: "",
UnitofMeasureCode: "DIEM",
LineNo_: 0
};
},
idAttribute: "LineNo_",
ID should not even exist for a new entry.
The issue is in the part you didn't show - in the part where you instantiate, create and populate the model.
Here is a quote from the Backbone documentation:
If the model does not yet have an id, it is considered to be new.
It is clear from your code that you are assigning an id attribute.
Your backend should be doing that.
And since you are doing it on a client, backbone presumes it it not new, and uses PUT
The above answers are correct in that if the model you are .save'ing has an id attribute backbone will do a PUT rather than a POST.
This behavior can be overridden simply by adding type: 'POST' to your save block:
var fooModel = new Backbone.Model({ id: 1});
fooModel.save(null, {
type: 'POST'
});
You can specify the ID in defaults, just make sure it's set to null (isNew will be set to true).
In your case it must be
LineNo_: null

Categories

Resources