Skip to content

Commit ca3348b

Browse files
committed
support for crud hook functions, fixes #51
1 parent 79bd9a2 commit ca3348b

5 files changed

Lines changed: 209 additions & 22 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Express-Cassandra is an advanced Cassandra ORM for NodeJS. No more hassling with
1515
* support for complex queries, streaming and token based pagination
1616
* support for user defined types/functions/aggregates
1717
* support for batching ORM operations for atomic updates
18+
* support for before and after hook functions for save/update/delete
1819
* builtin experimental support for automatic migrations
1920

2021
This module uses datastax [cassandra-driver](https://github.com/datastax/nodejs-driver) for node and some of the base orm features are wrapper over a highly modified version of [apollo-cassandra](https://github.com/3logic/apollo-cassandra) module. The modifications made to the orm library was necessary to support missing features in the orm, keep it updated with the latest cassandra releases and to make it compatible with the advanced requirements of this module.

docs/management.md

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,4 +122,47 @@ models.instance.Person.findOne({name: 'John'}, function(err, john){
122122
//...
123123
});
124124
});
125-
```
125+
```
126+
127+
## Hook Functions
128+
129+
When you perform a save/update/delete operation, a hook function helps you to tap into it in order to change data or perform other operations. Following are the available hook functions you can define in your schema:
130+
131+
```js
132+
module.exports = {
133+
fields: {
134+
...
135+
},
136+
key: [...],
137+
before_save: function (instance, options, next) {
138+
next();
139+
},
140+
after_save: function (instance, options, next) {
141+
next();
142+
},
143+
before_update: function (queryObject, updateValues, options, next) {
144+
next();
145+
},
146+
after_update: function (queryObject, updateValues, options, next) {
147+
next();
148+
},
149+
before_delete: function (queryObject, options, next) {
150+
next();
151+
},
152+
after_delete: function (queryObject, options, next) {
153+
next();
154+
},
155+
}
156+
```
157+
158+
* `before_save` if defined, will be automatically called each time before a save operation is performed. The `instance` will contain the model instance, so you could modify instance values or perform other things based on it. The `options` will contain the query options being passed to cassandra. You could also modify the options or do things based on it. The `next` is a callback function and after you're done, you must call it like `next()` to let the data saved in cassandra. Otherwise you may also send an error message like `next(err)` to halt the save operation. In this case the data will not be saved and the caller will receive the error message via callback.
159+
160+
* `after_save` if defined, will be automatically called each time after a save operation is successfully performed. The `instance` will contain the model instance, so you could get the instance values that were actually used and perform other things based on it. Note that if you performed any database functions then the output of those functions will not be available in the instance object. For example if you used the `$db_function: 'uuid()'` to generate your id field, then the actual saved value will not be available in the instance object. If you need to know what id was generated, then you need to use the utility function `models.uuid()` in javascript and send that value in the id field instead of using $db_function. The `options` will contain the final query options passed to cassandra. The `next` is a callback function and after you're done, you must call it like `next()` to let the caller recieve it's callback. Otherwise you may also send an error message like `next(err)` and in this case the caller will receive the error message via callback.
161+
162+
* `before_update` if defined, will be automatically called each time before an update operation is performed. The `queryObject` and the `updateValues` will contain the query and updated values part of the update operation as is, so you could modify them if required or perform other things based on them. The `options` will contain the query options being passed to cassandra. You could also modify the options or do things based on it. The `next` is a callback function and after you're done, you must call it like `next()` to let the data updated in cassandra. Otherwise you may also send an error message like `next(err)` to halt the update operation. In this case the data will not be updated and the caller will receive the error message via callback.
163+
164+
* `after_update` if defined, will be automatically called each time after an update operation is successfully performed. The `queryObject` and the `updateValues` will contain the query and updated values part of the update operation as is, so you could get the query and values that were actually used and perform other things based on them. Note that if you performed any database functions then the output of those functions will not be available in the updateValues object. For example if you used the `$db_function: 'now()'` to update your `updatedAt` field, then the actual updated value will not be available in the `updateValues` object. If you need to know the updated value of the `updatedAt` field, then you need to generate the current timestamp in javascript and send that value in the updatedAt field instead of using $db_function. The `options` will contain the final query options passed to cassandra. The `next` is a callback function and after you're done, you must call it like `next()` to let the caller recieve it's callback. Otherwise you may also send an error message like `next(err)` and in this case the caller will receive the error message via callback.
165+
166+
* `before_delete` if defined, will be automatically called each time before a delete operation is performed. The `queryObject` will contain the query part of the delete operation as is, so you could modify them if required or perform other things based on them. The `options` will contain the query options being passed to cassandra. You could also modify the options or do things based on it. The `next` is a callback function and after you're done, you must call it like `next()` to let the data deleted in cassandra. Otherwise you may also send an error message like `next(err)` to halt the delete operation. In this case the data will not be deleted and the caller will receive the error message via callback.
167+
168+
* `after_delete` if defined, will be automatically called each time after a delete operation is successfully performed. The `queryObject` will contain the query part of the delete operation as is, so you could get the query that was actually used and perform other things based on it. The `options` will contain the final query options passed to cassandra. The `next` is a callback function and after you're done, you must call it like `next()` to let the caller recieve it's callback. Otherwise you may also send an error message like `next(err)` and in this case the caller will receive the error message via callback.

src/orm/apollo_error.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,18 +79,36 @@ const AERROR_TYPES = {
7979
'model.save.dberror': {
8080
msg: 'Error during save query on DB -> %s',
8181
},
82+
'model.save.before.error': {
83+
msg: 'Error in before_save lifecycle function -> %s',
84+
},
85+
'model.save.after.error': {
86+
msg: 'Error in after_save lifecycle function -> %s',
87+
},
8288
'model.update.invalidvalue': {
8389
msg: 'Invalid Value: "%s" for Field: %s',
8490
},
8591
'model.update.dberror': {
8692
msg: 'Error during update query on DB -> %s',
8793
},
94+
'model.update.before.error': {
95+
msg: 'Error in before_update lifecycle function -> %s',
96+
},
97+
'model.update.after.error': {
98+
msg: 'Error in after_update lifecycle function -> %s',
99+
},
88100
'model.delete.invalidvalue': {
89101
msg: 'Invalid Value: "%s" for Field: %s (Type: %s)',
90102
},
91103
'model.delete.dberror': {
92104
msg: 'Error during delete query on DB -> %s',
93105
},
106+
'model.delete.before.error': {
107+
msg: 'Error in before_delete lifecycle function -> %s',
108+
},
109+
'model.delete.after.error': {
110+
msg: 'Error in after_delete lifecycle function -> %s',
111+
},
94112
};
95113

96114
const ERR_NAME_PREFIX = 'apollo';

src/orm/base_model.js

Lines changed: 128 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1775,16 +1775,52 @@ BaseModel.update = function f(queryObject, updateValues, options, callback) {
17751775
if (options.retry) queryOptions.retry = options.retry;
17761776
if (options.serialConsistency) queryOptions.serialConsistency = options.serialConsistency;
17771777

1778-
this._execute_table_query(query, queryParams, queryOptions, (err, results) => {
1779-
if (typeof callback === 'function') {
1780-
if (err) {
1781-
callback(buildError('model.update.dberror', err));
1778+
// set dummy hook function if not present in schema
1779+
const schema = this._properties.schema;
1780+
if (typeof schema.before_update !== 'function') {
1781+
schema.before_update = function f1(queryObj, updateVal, optionsObj, next) {
1782+
next();
1783+
};
1784+
}
1785+
1786+
if (typeof schema.after_update !== 'function') {
1787+
schema.after_update = function f1(queryObj, updateVal, optionsObj, next) {
1788+
next();
1789+
};
1790+
}
1791+
1792+
schema.before_update(queryObject, updateValues, options, (error) => {
1793+
if (error) {
1794+
if (typeof callback === 'function') {
1795+
callback(buildError('model.update.before.error', error));
17821796
return;
17831797
}
1784-
callback(null, results);
1785-
} else if (err) {
1786-
throw buildError('model.update.dberror', err);
1798+
throw buildError('model.update.before.error', error);
17871799
}
1800+
1801+
this._execute_table_query(query, queryParams, queryOptions, (err, results) => {
1802+
if (typeof callback === 'function') {
1803+
if (err) {
1804+
callback(buildError('model.update.dberror', err));
1805+
return;
1806+
}
1807+
schema.after_update(queryObject, updateValues, options, (error1) => {
1808+
if (error1) {
1809+
callback(buildError('model.update.after.error', error1));
1810+
return;
1811+
}
1812+
callback(null, results);
1813+
});
1814+
} else if (err) {
1815+
throw buildError('model.update.dberror', err);
1816+
} else {
1817+
schema.after_update(queryObject, updateValues, options, (error1) => {
1818+
if (error1) {
1819+
throw buildError('model.update.after.error', error1);
1820+
}
1821+
});
1822+
}
1823+
});
17881824
});
17891825

17901826
return {};
@@ -1833,16 +1869,52 @@ BaseModel.delete = function f(queryObject, options, callback) {
18331869
if (options.retry) queryOptions.retry = options.retry;
18341870
if (options.serialConsistency) queryOptions.serialConsistency = options.serialConsistency;
18351871

1836-
this._execute_table_query(query, queryParams, queryOptions, (err, results) => {
1837-
if (typeof callback === 'function') {
1838-
if (err) {
1839-
callback(buildError('model.delete.dberror', err));
1872+
// set dummy hook function if not present in schema
1873+
const schema = this._properties.schema;
1874+
if (typeof schema.before_delete !== 'function') {
1875+
schema.before_delete = function f1(queryObj, optionsObj, next) {
1876+
next();
1877+
};
1878+
}
1879+
1880+
if (typeof schema.after_delete !== 'function') {
1881+
schema.after_delete = function f1(queryObj, optionsObj, next) {
1882+
next();
1883+
};
1884+
}
1885+
1886+
schema.before_delete(queryObject, options, (error) => {
1887+
if (error) {
1888+
if (typeof callback === 'function') {
1889+
callback(buildError('model.delete.before.error', error));
18401890
return;
18411891
}
1842-
callback(null, results);
1843-
} else if (err) {
1844-
throw buildError('model.delete.dberror', err);
1892+
throw buildError('model.delete.before.error', error);
18451893
}
1894+
1895+
this._execute_table_query(query, queryParams, queryOptions, (err, results) => {
1896+
if (typeof callback === 'function') {
1897+
if (err) {
1898+
callback(buildError('model.delete.dberror', err));
1899+
return;
1900+
}
1901+
schema.after_delete(queryObject, options, (error1) => {
1902+
if (error1) {
1903+
callback(buildError('model.delete.after.error', error1));
1904+
return;
1905+
}
1906+
callback(null, results);
1907+
});
1908+
} else if (err) {
1909+
throw buildError('model.delete.dberror', err);
1910+
} else {
1911+
schema.after_delete(queryObject, options, (error1) => {
1912+
if (error1) {
1913+
throw buildError('model.delete.after.error', error1);
1914+
}
1915+
});
1916+
}
1917+
});
18461918
});
18471919

18481920
return {};
@@ -2027,16 +2099,51 @@ BaseModel.prototype.save = function fn(options, callback) {
20272099
if (options.retry) queryOptions.retry = options.retry;
20282100
if (options.serialConsistency) queryOptions.serialConsistency = options.serialConsistency;
20292101

2030-
this.constructor._execute_table_query(query, queryParams, queryOptions, (err, result) => {
2031-
if (typeof callback === 'function') {
2032-
if (err) {
2033-
callback(buildError('model.save.dberror', err));
2102+
// set dummy hook function if not present in schema
2103+
if (typeof schema.before_save !== 'function') {
2104+
schema.before_save = function f(instance, option, next) {
2105+
next();
2106+
};
2107+
}
2108+
2109+
if (typeof schema.after_save !== 'function') {
2110+
schema.after_save = function f(instance, option, next) {
2111+
next();
2112+
};
2113+
}
2114+
2115+
schema.before_save(this, options, (error) => {
2116+
if (error) {
2117+
if (typeof callback === 'function') {
2118+
callback(buildError('model.save.before.error', error));
20342119
return;
20352120
}
2036-
callback(null, result);
2037-
} else if (err) {
2038-
throw buildError('model.save.dberror', err);
2121+
throw buildError('model.save.before.error', error);
20392122
}
2123+
2124+
this.constructor._execute_table_query(query, queryParams, queryOptions, (err, result) => {
2125+
if (typeof callback === 'function') {
2126+
if (err) {
2127+
callback(buildError('model.save.dberror', err));
2128+
return;
2129+
}
2130+
schema.after_save(this, options, (error1) => {
2131+
if (error1) {
2132+
callback(buildError('model.save.after.error', error1));
2133+
return;
2134+
}
2135+
callback(null, result);
2136+
});
2137+
} else if (err) {
2138+
throw buildError('model.save.dberror', err);
2139+
} else {
2140+
schema.after_save(this, options, (error1) => {
2141+
if (error1) {
2142+
throw buildError('model.save.after.error', error1);
2143+
}
2144+
});
2145+
}
2146+
});
20402147
});
20412148

20422149
return {};

test/models/PersonModel.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,4 +119,22 @@ module.exports = {
119119
key: [['userID', 'age'], 'active'],
120120
},
121121
},
122+
before_save: (instance, options, next) => {
123+
next();
124+
},
125+
after_save: (instance, options, next) => {
126+
next();
127+
},
128+
before_update: (queryObject, updateValues, options, next) => {
129+
next();
130+
},
131+
after_update: (queryObject, updateValues, options, next) => {
132+
next();
133+
},
134+
before_delete: (queryObject, options, next) => {
135+
next();
136+
},
137+
after_delete: (queryObject, options, next) => {
138+
next();
139+
},
122140
};

0 commit comments

Comments
 (0)