Knex.JS Auto Update Trigger - javascript

I am using Knex.JS migration tools. However, when creating a table, I'd like to have a column named updated_at that is automatically updated when a record is updated in the database.
For example, here is a table:
knex.schema.createTable('table_name', function(table) {
table.increments();
table.string('name');
table.timestamp("created_at").defaultTo(knex.fn.now());
table.timestamp("updated_at").defaultTo(knex.fn.now());
table.timestamp("deleted_at");
})
The created_at and updated_at column defaults to the time the record is created, which is fine. But, when that record is updated, I'd like the updated_at column to show the new time that it was updated at automatically.
I'd prefer not to write in raw postgres.
Thanks!

With Postgres, you'll need a trigger. Here's a method I've used successfully.
Add a function
If you have multiple migration files in a set order, you might need to artificially change the datestamp in the filename to get this to run first (or just add it to your first migration file). If you can't roll back, you might need to do this step manually via psql. However, for new projects:
const ON_UPDATE_TIMESTAMP_FUNCTION = `
CREATE OR REPLACE FUNCTION on_update_timestamp()
RETURNS trigger AS $$
BEGIN
NEW.updated_at = now();
RETURN NEW;
END;
$$ language 'plpgsql';
`
const DROP_ON_UPDATE_TIMESTAMP_FUNCTION = `DROP FUNCTION on_update_timestamp`
exports.up = knex => knex.raw(ON_UPDATE_TIMESTAMP_FUNCTION)
exports.down = knex => knex.raw(DROP_ON_UPDATE_TIMESTAMP_FUNCTION)
Now the function should be available to all subsequent migrations.
Define a knex.raw trigger helper
I find it more expressive not to repeat large chunks of SQL in migration files if I can avoid it. I've used knexfile.js here but if you don't like to complicate that, you could define it wherever.
module.exports = {
development: {
// ...
},
production: {
// ...
},
onUpdateTrigger: table => `
CREATE TRIGGER ${table}_updated_at
BEFORE UPDATE ON ${table}
FOR EACH ROW
EXECUTE PROCEDURE on_update_timestamp();
`
}
Use the helper
Finally, we can fairly conveniently define auto-updating triggers:
const { onUpdateTrigger } = require('../knexfile')
exports.up = knex =>
knex.schema.createTable('posts', t => {
t.increments()
t.string('title')
t.string('body')
t.timestamps(true, true)
})
.then(() => knex.raw(onUpdateTrigger('posts')))
exports.down = knex => knex.schema.dropTable('posts')
Note that dropping the table is enough to get rid of the trigger: we don't need an explicit DROP TRIGGER.
This all might seem like a lot of work, but it's pretty "set-and-forget" once you've done it and handy if you want to avoid using an ORM.

You can create a knex migration using timestamps:
exports.up = (knex, Promise) => {
return Promise.all([
knex.schema.createTable('table_name', (table) => {
table.increments();
table.string('name');
table.timestamps(false, true);
table.timestamp('deleted_at').defaultTo(knex.fn.now());
})
]);
};
exports.down = (knex, Promise) => {
return Promise.all([
knex.schema.dropTableIfExists('table_name')
]);
};
With timestamps a database schema will be created which adds a created_at and updated_at column, each containing an initial timestamp.
To keep the updated_at column current, you'll need knex.raw:
table.timestamp('updated_at').defaultTo(knex.raw('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'));
To skip the knex.raw solution, I suggest using a high level ORM like Objection.js. With Objection.js you could implement your own BaseModel which then updates the updated_at column:
Something.js
const BaseModel = require('./BaseModel');
class Something extends BaseModel {
constructor() {
super();
}
static get tableName() {
return 'table_name';
}
}
module.exports = Something;
BaseModel
const knexfile = require('../../knexfile');
const knex = require('knex')(knexfile.development);
const Model = require('objection').Model;
class BaseModel extends Model {
$beforeUpdate() {
this.updated_at = knex.fn.now();
}
}
module.exports = BaseModel;
Source: http://vincit.github.io/objection.js/#timestamps

This is my way of doing that in Mysql 5.6+
The reason I didn't use table.timestamps is because I use DATETIME instead of timestamp.
table.dateTime('created_on')
.notNullable()
.defaultTo(knex.raw('CURRENT_TIMESTAMP'))
table.dateTime('updated_on')
.notNullable()
.defaultTo(knex.raw('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'))

This is not a feature of Knex. Knex only creates the columns, but does not keep them up to date for you.
If you use, the Bookshelf ORM, however, you can specify that a table has timestamps, and it will set & update the columns as expected:
Bookshelf docs
Github issue

exports.up = (knex) => {
return knex.raw(create or replace function table_name_update() RETURNS trigger AS $$ begin new.updated_at = now(); RETURN NEW; end; $$ language 'plpgsql'; create or replace trigger tg_table_name_update on table_name before update for each row execute table_name_update();)
};
exports.down = (knex) => {
return knex.raw(drop table if exists table_name; drop function if exists table_name_update;)
};

You can directly use this function
table.timestamps()
This will create the 'created_at' and 'updated_at' columns by default and update them accordingly
https://knexjs.org/#Schema-timestamps

Related

Sequelize restrict update if many-to-many relation established

In general I have a goal to restrict any update of an entity if it's binded to anything.
To be specific I have two models: Order and Good. They have many-to-many relation.
const Good = sequelize.define('good' , { name:Sequelize.STRING });
const Order = sequelize.define('order' , { });
//M:N relation
Good.belongsToMany(Order, { through: 'GoodsInOrders' });
Order.belongsToMany(Good, { through: 'GoodsInOrders' });
I have tried to set onUpdate: 'NO ACTION' and onUpdate: 'RESTRICT' inside belongsToMany association defining but it has no effect.
Here is the code to reproduce my manipulations with goods and order
//creating few goods
const good1 = await Good.create({ name:'Coca-Cola' });
const good2 = await Good.create({ name:'Hamburger' });
const good3 = await Good.create({ name:'Fanta' });
//creating an order
const order = await Order.create();
//adding good1 and good2 to the order
await order.addGoods([good1,good2]);
//It's ok to update good3 since no orders contains It
await good3.update( { name:'Pepsi' });
//But I need to fire an exeption if I try to update any goods belonged to order
//There should be an error because we previously added good1 to order
await good1.update( { name:'Sandwich' });
I have no clue how to restrict it in a simple way.
Surely we always can add beforeUpdate hook on Good model but I would like to avoid this kind of complications.
I will be glad to any ideas.
As I said I was looking for simpler alternative to hooks but many researches after all led me to nowhere.
My solution was to declare the beforeUpdate hook inside Good model so inital definition
const Good = sequelize.define('good' , { name : Sequelize.STRING } );
was transformed into this
const Good = sequelize.define('good', {
name: Sequelize.STRING
}, {
hooks: {
beforeUpdate: async (instance, options) => {
if ((await instance.getOrders()).length)
return Promise.reject('error');
return Promise.resolve();
}
}
});
So here we add the hook that fires every time before update particular good.
It makes inner request of list of orders that contains current good.
(await instance.getOrders()).length
And depends on the result it either fire an exception if the list isn't empty or just return resolved promise that means that everything ok and current good can be updated.

How to create migration of the entire DB using 1 knex script?

In my DB I have about 50 tables. Every table has created_at and updated_at. Of course, I could create 50 migrations, but there would be similar. Is there any opportunity to create knex script which could be make 50 migrations to entire DB?
Here is example of code:
exports.up = function(knex, Promise) {
return knex.schema.alterTable("balance", table => {
table.timestamp("created_at").defaultTo(knex.fn.now()).alter()
table
.timestamp("updated_at")
.defaultTo(knex.raw("CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP")).alter()
})
};
exports.down = function(knex, Promise) {
return knex.schema.alterTable("balance", table => {
table.dateTime("created_at").defaultTo(null).alter()
table.dateTime("updated_at").defaultTo(null).alter()
})
};
Balance is the name of table, so I have to create ~50 migrations changing only DB's name. Is it possible to make everything easier using just 1 knex script?
Thanks for answers!
Just like that:
const showList = await knex.raw(
`SELECT table_name FROM information_schema.tables WHERE table_schema = 'your_db';`)
for (const item of showList[0]) {
await knex.schema.alterTable(`${item.table_name}`, table => {
table.timestamp("created_at").defaultTo(knex.fn.now()).alter()
table
.timestamp("updated_at")
.defaultTo(knex.raw("CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP")).alter()
})
}

Set default notNullable reference column in Knex migration only for existing rows

I have a postgresql database, and I am trying the following migration to add a non-nullable reference column to an existing table, using Knex:
exports.up = function (knex) {
return knex.schema.table('Donation', (table) => {
table.integer('causeId').notNullable();
table.foreign('causeId').references('Cause.id');
});
};
When I run this migration, I get the following error due to existing rows:
error: alter table "Donation" add column "causeId" integer not null - column "causeId" contains null values
I want to seed the column with a default value only for existing rows. For subsequent insertions, I would rather the insert fail if a value is not provided, so .notNullable().defaultTo(...) won't work.
I was thinking of maybe using defaultTo() only for the initial migration and then removing the constraint after, but I'm drawing a blank on how to accomplish this.
Any help would be appreciated!
My solution ended up being to use defaultTo(...) on creation of this column and then alter it afterwards using alterTable(...).
Not sure of a better way, but this works.
exports.up = function (knex) {
function relateCauseToDonation() {
return knex.schema.table('Donation', (table) => {
table.integer('causeId').notNullable().defaultTo(1);
table.foreign('causeId').references('Cause.id');
});
}
function removeDefaultToFromCauseId() {
return knex.schema.alterTable('Donation', (table) => {
table.integer('causeId').notNullable().alter();
});
}
return relateCauseToDonation().then(removeDefaultToFromCauseId);
};

Customize cli-generated feathersjs services

I'm writing an api to try featherjs with its mongoose adapter. I want my GET /books/ endpoint to only return books with the private attribute set to false. Should I use a before hook? If that's the case, how do I prevent users from running custom queries in my endpoint? Should I manually empty the params object?
You need to create a before hook in books.hooks.js
const books_qry = require('../../hooks/books_qry');
module.exports = {
before: {
all: [],
find: [books_qry()],
...
Create /src/hooks/books_qry.js
module.exports = function () {
return function (context) {
//You have 2 choices to change the context.params.query
//overwrite any request for a custom query
context.params.query = { private: false };
//or add a query param to the request for a custom query
context.params.query.private = false
//check the updated context.params.query
console.log(context.params.query);
return context;
}
}
Select one of the choices that you need. Since I never used mongoose at the moment, check the documentation in order to create a valid query (btw above example works for mongodb adapter)

Sequelize: can you use hooks to add a comment to a query?

Heroku recently posted a list of some good tips for postgres. I was most intreged by the Track the Source of Your Queries section. I was curious if this was something that's possible to use with Sequelize. I know that sequelize has hooks, but wasn't sure if hooks could be used to make actual query string adjustments.
I'm curious if it's possible to use a hook or another Sequelize method to append a comment to Sequelize query (without using .raw) to keep track of where the query was called from.
(Appending and prepending to queries would also be helpful for implementing row-level security, specifically set role / reset role)
Edit: Would it be possible to use sequelize.fn() for this?
If you want to just insert a "tag" into the SQL query you could use Sequelize.literal() to pass a literal string to the query generator. Adding this to options.attributes.include will add it, however it will also need an alias so you would have to pass some kind of value as well.
Model.findById(id, {
attributes: {
include: [
[Sequelize.literal('/* your comment */ 1'), 'an_alias'],
],
},
});
This would produce SQL along the lines of
SELECT `model`.`id`, /* your comment */ 1 as `an_alias`
FROM `model` as `model`
WHERE `model`.`id` = ???
I played around with automating this a bit and it probably goes beyond the scope of this answer, but you could modify the Sequelize.Model.prototype before you create a connection using new Sequelize() to tweak the handling of the methods. You would need to do this for all the methods you want to "tag".
// alias findById() so we can call it once we fiddle with the input
Sequelize.Model.prototype.findById_untagged = Sequelize.Model.prototype.findById;
// override the findbyId() method so we can intercept the options.
Sequelize.Model.prototype.findById = function findById(id, options) {
// get the caller somehow (I was having trouble accessing the call stack properly)
const caller = ???;
// you need to make sure it's defined and you aren't overriding settings, etc
options.attributes.include.push([Sequelize.literal('/* your comment */ 1'), 'an_alias']);
// pass it off to the aliased method to continue as normal
return this.findById_untagged(id, options);
}
// create the connection
const connection = new Sequelize(...);
Note: it may not be possible to do this automagically as Sequelize has use strict so the arguments.caller and arguments.callee properties are not accessible.
2nd Note: if you don't care about modifying the Sequelize.Model prototypes you can also abstract your calls to the Sequelize methods and tweak the options there.
function Wrapper(model) {
return {
findById(id, options) {
// do your stuff
return model.findById(id, options);
},
};
}
Wrapper(Model).findById(id, options);
3rd Note: You can also submit a pull request to add this functionality to Sequelize under a new option value, like options.comment, which is added at the end of the query.
This overrides the sequelize.query() method that's internally used by Sequelize for all queries to add a comment showing the location of the query in the code. It also adds the stack trace to errors thrown.
const excludeLineTexts = ['node_modules', 'internal/process', ' anonymous ', 'runMicrotasks', 'Promise.'];
// overwrite the query() method that Sequelize uses internally for all queries so the error shows where in the code the query is from
sequelize.query = function () {
let stack;
const getStack = () => {
if (!stack) {
const o = {};
Error.captureStackTrace(o, sequelize.query);
stack = o.stack;
}
return stack;
};
const lines = getStack().split(/\n/g).slice(1);
const line = lines.find((l) => !excludeLineTexts.some((t) => l.includes(t)));
if (line) {
const methodAndPath = line.replace(/(\s+at (async )?|[^a-z0-9.:/\\\-_ ]|:\d+\)?$)/gi, '');
if (methodAndPath) {
const comment = `/* ${methodAndPath} */`;
if (arguments[0]?.query) {
arguments[0].query = `${comment} ${arguments[0].query}`;
} else {
arguments[0] = `${comment} ${arguments[0]}`;
}
}
}
return Sequelize.prototype.query.apply(this, arguments).catch((err) => {
err.fullStack = getStack();
throw err;
});
};

Categories