@sera4/essentia 1.1.11 → 1.1.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/cache/index.js +188 -0
- package/health/health-logger.js +40 -0
- package/health/index.js +64 -21
- package/index.js +1 -0
- package/package.json +5 -4
- package/package.tar.gz +0 -0
- package/paper-trail/README.md +67 -0
- package/paper-trail/helpers.js +99 -0
- package/paper-trail/index.js +637 -0
package/README.md
CHANGED
package/cache/index.js
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"use strict"
|
|
2
|
+
|
|
3
|
+
import { promisify } from "util";
|
|
4
|
+
|
|
5
|
+
const isEmpty = obj => [Object, Array].includes((obj || {}).constructor) && !Object.entries((obj || {})).length;
|
|
6
|
+
|
|
7
|
+
class CacheConfigError extends Error {
|
|
8
|
+
constructor(m) {
|
|
9
|
+
super(m);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/* Redis cache helper
|
|
14
|
+
* Example use: Place the following code in your project to setup redis use
|
|
15
|
+
*
|
|
16
|
+
* import { cache } from "@sera4/essentia";
|
|
17
|
+
* const cacheHelper = cache({ password: .., database: .., host: .., port: .. }, "service:"));
|
|
18
|
+
*
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const convertRedisKeyValueToJson = (v) => {
|
|
22
|
+
return (v && v.length > 0 && v[0] !== null) ? JSON.parse(v) : null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const convertRedisHashToJson = (v) => {
|
|
26
|
+
if (v) {
|
|
27
|
+
var result = {};
|
|
28
|
+
for(var k in v) {
|
|
29
|
+
if (v.hasOwnProperty(k)) {
|
|
30
|
+
result[k] = JSON.parse(v[k]);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return result;
|
|
34
|
+
} else {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function convertJsonToRedisValue(j) {
|
|
40
|
+
var result = {};
|
|
41
|
+
for (var key in j) {
|
|
42
|
+
if (j.hasOwnProperty(key)) {
|
|
43
|
+
result[key] = JSON.stringify(j[key]);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
class Cache {
|
|
50
|
+
constructor(config, servicePrefix) {
|
|
51
|
+
if ((config == undefined) || isEmpty(config)) {
|
|
52
|
+
throw new CacheConfigError("Invalid cache configuration");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (servicePrefix == null) {
|
|
56
|
+
throw new CacheConfigError("Invalid service prefix");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (config.fake) {
|
|
60
|
+
// used in testing
|
|
61
|
+
this.redisClient = require("fakeredis").createClient("fake backend")
|
|
62
|
+
} else {
|
|
63
|
+
this.redisClient = require("redis").createClient(config);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
this.redisClient.on("error", (err) => {
|
|
67
|
+
logger.error("Cache/Redis error: ", err);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
this.servicePrefix = servicePrefix;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
ping() {
|
|
74
|
+
return this.redisClient.ping();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
properKey(key, path) {
|
|
78
|
+
let p = this.servicePrefix + key
|
|
79
|
+
if (path)
|
|
80
|
+
p += path
|
|
81
|
+
return p
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
writeHash(path, key, value) {
|
|
85
|
+
return this.redisClient.hsetAsync(properKey(key), key, value);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
writeHashBlocking(path, key, value) {
|
|
89
|
+
return this.redisClient.hset(properKey(key), key, value);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// write multiple hash keys and stringify every value
|
|
93
|
+
writeMultiHash(path, j) {
|
|
94
|
+
return this.redisClient.hmsetAsync(properKey(key), convertJsonToRedisValue(j));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
readHashKey(obj, key, autoConvert=true) {
|
|
98
|
+
var k = autoConvert;
|
|
99
|
+
|
|
100
|
+
return new Promise((resolve, reject) => {
|
|
101
|
+
this.redisClient.hget(properKey(key, obj), key, (e, r) => {
|
|
102
|
+
if (e) {
|
|
103
|
+
logger.error("Redis hmget error:", e);
|
|
104
|
+
reject(e);
|
|
105
|
+
} else {
|
|
106
|
+
resolve(k ? convertRedisKeyValueToJson(r) : r);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
readHash(path) {
|
|
113
|
+
return new Promise((resolve, reject) => {
|
|
114
|
+
this.redisClient.hgetall(properKey(key, path), (e, r) => {
|
|
115
|
+
if (e) {
|
|
116
|
+
console.log("Redis hget error:", e);
|
|
117
|
+
reject(e);
|
|
118
|
+
} else {
|
|
119
|
+
resolve(convertRedisHashToJson(r));
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
deleteHashKey(hash, key) {
|
|
126
|
+
return new Promise((resolve, reject) => {
|
|
127
|
+
this.redisClient.hdel(properKey(key, hash), key, (err) => {
|
|
128
|
+
if (err) {
|
|
129
|
+
logger.error("Redis hdel error:", err);
|
|
130
|
+
reject(err);
|
|
131
|
+
} else {
|
|
132
|
+
resolve();
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
get(key) {
|
|
139
|
+
return this.redisClient.getAsync(properKey(key));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
getAsync(key) {
|
|
143
|
+
return promisify(this.redisClient.get(key)).bind(this.redisClient)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
/* a single key write with optional expiry */
|
|
148
|
+
// returns bluebird promise
|
|
149
|
+
writeKey(key, value, expiry) {
|
|
150
|
+
key = this.servicePrefix + key;
|
|
151
|
+
if (expiry) {
|
|
152
|
+
return this.redisClient.setAsync(key, value, "EX", expiry);
|
|
153
|
+
} else {
|
|
154
|
+
return this.redisClient.setAsync(key, value);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
deleteKey(key) {
|
|
159
|
+
return redisClient.delAsync(properKey(key));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
expire(path, expiry) {
|
|
163
|
+
return this.redisClient.expire(path, expiry);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
exists(path) {
|
|
167
|
+
return this.redisClient.exists(path);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
deleteAllKeys() {
|
|
171
|
+
return new Promise((resolve, reject) => {
|
|
172
|
+
if(process.env.NODE_ENV === "production") {
|
|
173
|
+
throw new Error("Clearing system cache is not permitted in production");
|
|
174
|
+
}
|
|
175
|
+
this.redisClient.flushdb((err, res) => {
|
|
176
|
+
if (err) {
|
|
177
|
+
reject(err);
|
|
178
|
+
} else {
|
|
179
|
+
resolve(res);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export default function(config, servicePrefix) {
|
|
187
|
+
return new Cache(config, servicePrefix)
|
|
188
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import logger from "../logger/s4-logger.js";
|
|
2
|
+
let enabled = false;
|
|
3
|
+
|
|
4
|
+
const helpers = {
|
|
5
|
+
debug(...args) {
|
|
6
|
+
if (enabled) {
|
|
7
|
+
logger.debug(":: HealthCheck ::", args);
|
|
8
|
+
}
|
|
9
|
+
},
|
|
10
|
+
|
|
11
|
+
log(...args) {
|
|
12
|
+
if (enabled) {
|
|
13
|
+
logger.debug(":: HealthCheck ::", args);
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
warn(...args) {
|
|
18
|
+
if (enabled) {
|
|
19
|
+
logger.warn(":: HealthCheck ::", args);
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
info(...args) {
|
|
24
|
+
if (enabled) {
|
|
25
|
+
logger.info(":: HealthCheck ::", args);
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
error(...args) {
|
|
30
|
+
if (enabled) {
|
|
31
|
+
logger.error(":: HealthCheck ::", args);
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
setEnabled(e) {
|
|
36
|
+
enabled = e;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export default helpers;
|
package/health/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict"
|
|
2
2
|
|
|
3
3
|
import pathHelper from "path";
|
|
4
|
+
import logger from "./health-logger.js";
|
|
4
5
|
|
|
5
6
|
class HealthCheckError extends Error {
|
|
6
7
|
constructor(m) {
|
|
@@ -15,12 +16,16 @@ class HealthCheckError extends Error {
|
|
|
15
16
|
* const router = require('express').Router();
|
|
16
17
|
*
|
|
17
18
|
* ....
|
|
19
|
+
*
|
|
20
|
+
* const checkRedis = async () => { await cache.ping(); }
|
|
21
|
+
* hc.setupCheck({ name: "redis", checkResult: false, critical: true, healthFunction: checkRedis });
|
|
18
22
|
* app.use(hc.defaultPath(serverConfigs.api_prefix), hc.routes(router));
|
|
19
23
|
*
|
|
20
24
|
*/
|
|
21
|
-
|
|
22
25
|
class HealthCheck {
|
|
23
|
-
constructor(appendData = {}) {
|
|
26
|
+
constructor(appendData = {}, enableLogging = true) {
|
|
27
|
+
logger.setEnabled(enableLogging);
|
|
28
|
+
|
|
24
29
|
this.defaultResponse = {
|
|
25
30
|
...
|
|
26
31
|
appendData,
|
|
@@ -29,11 +34,13 @@ class HealthCheck {
|
|
|
29
34
|
};
|
|
30
35
|
|
|
31
36
|
this.defaultPathRoute = `health_check\(.*\)?`
|
|
37
|
+
this.suppressLogs = false;
|
|
32
38
|
this.lastCallId = 0; // default start point/integer
|
|
33
39
|
this.healthChecks = [];
|
|
34
40
|
}
|
|
35
41
|
|
|
36
|
-
/*
|
|
42
|
+
/* Deprecated and replaced by setupCheck
|
|
43
|
+
* call this with an async function, must support wait capability
|
|
37
44
|
*
|
|
38
45
|
* const dbCheck = async () => {
|
|
39
46
|
* return { name: "db", "result": "success", critical: false };
|
|
@@ -60,15 +67,40 @@ class HealthCheck {
|
|
|
60
67
|
return router
|
|
61
68
|
}
|
|
62
69
|
|
|
63
|
-
|
|
70
|
+
// v2 version of health check, replaced addCheck
|
|
71
|
+
// example:
|
|
72
|
+
// const checkRedisFcn = async () => { await cache.ping().then(() => true) }
|
|
73
|
+
// setupCheck({ name: "redis", checkResult: false, critical: false, healthFunction: checkRedisFcn });
|
|
74
|
+
setupCheck(config) {
|
|
75
|
+
this.healthChecks.push(config);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/* performs health checks and returns 200 if true and 500 if not */
|
|
64
79
|
async checkHealth(req, res) {
|
|
65
80
|
const checkFormat = pathHelper.extname(req.baseUrl);
|
|
66
81
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
82
|
+
const result = await Promise.all(this.healthChecks.map(async hc => {
|
|
83
|
+
if (typeof(hc) === "function") {
|
|
84
|
+
try {
|
|
85
|
+
return await hc();
|
|
86
|
+
return { "result": "success", critical: false };
|
|
87
|
+
} catch(e) {
|
|
88
|
+
return { name: "unknown", result: 'fail', status: e.status }
|
|
89
|
+
}
|
|
90
|
+
} else {
|
|
91
|
+
let res = { name: hc.name, critical: hc.critical }
|
|
92
|
+
try {
|
|
93
|
+
await hc.healthFunction();
|
|
94
|
+
return { ...res, "result": "success" };
|
|
95
|
+
} catch(e) {
|
|
96
|
+
const appendStatus = e.status ? ` (${e.status})` : ""
|
|
97
|
+
logger.warn(`${hc.name} service failed${appendStatus}` )
|
|
98
|
+
return { ...res, "result": "fail", status: e.status }
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}));
|
|
71
102
|
|
|
103
|
+
try {
|
|
72
104
|
let callId = ++this.lastCallId;
|
|
73
105
|
|
|
74
106
|
// start the counter over
|
|
@@ -78,10 +110,18 @@ class HealthCheck {
|
|
|
78
110
|
|
|
79
111
|
let status = this.defaultResponse.status;
|
|
80
112
|
|
|
113
|
+
let nonCriticalFail = false;
|
|
114
|
+
let criticalFail = false;
|
|
115
|
+
|
|
81
116
|
result.some(e => {
|
|
82
117
|
if (e.result.match(/fail/)) {
|
|
83
|
-
|
|
84
|
-
|
|
118
|
+
if (e.critical == false) {
|
|
119
|
+
nonCriticalFail = true;
|
|
120
|
+
} else {
|
|
121
|
+
criticalFail = true;
|
|
122
|
+
status = 500;
|
|
123
|
+
return true; // break
|
|
124
|
+
}
|
|
85
125
|
}
|
|
86
126
|
})
|
|
87
127
|
|
|
@@ -89,33 +129,36 @@ class HealthCheck {
|
|
|
89
129
|
res.setHeader('check', callId)
|
|
90
130
|
|
|
91
131
|
if (checkFormat === ".json") {
|
|
92
|
-
const payload = { "check": callId }
|
|
132
|
+
const payload = { "check": callId, healthy: "true", message: "success" }
|
|
93
133
|
|
|
94
|
-
if (
|
|
134
|
+
if (criticalFail) {
|
|
95
135
|
res.status(status).json({
|
|
96
136
|
... payload,
|
|
97
|
-
"healthy":
|
|
98
|
-
"message":"
|
|
137
|
+
"healthy": false,
|
|
138
|
+
"message": "fail",
|
|
139
|
+
"services": result
|
|
99
140
|
});
|
|
100
|
-
} else {
|
|
141
|
+
} else if (nonCriticalFail) {
|
|
101
142
|
res.status(status).json({
|
|
102
143
|
... payload,
|
|
103
|
-
"healthy": false,
|
|
104
144
|
"message": "fail",
|
|
105
|
-
"services":
|
|
145
|
+
"services": result
|
|
106
146
|
});
|
|
147
|
+
} else if (status === 200) {
|
|
148
|
+
res.status(status).json(payload);
|
|
107
149
|
}
|
|
108
150
|
} else {
|
|
109
151
|
let status_message;
|
|
110
|
-
if (
|
|
111
|
-
status_message = "success"
|
|
112
|
-
} else {
|
|
152
|
+
if (criticalFail) {
|
|
113
153
|
status_message = "fail"
|
|
114
|
-
}
|
|
154
|
+
} else if ((status === 200) || nonCriticalFail) {
|
|
155
|
+
status_message = "success"
|
|
156
|
+
}
|
|
115
157
|
|
|
116
158
|
res.status(status).send(status_message);
|
|
117
159
|
}
|
|
118
160
|
} catch(error) {
|
|
161
|
+
console.debug("error: ", error)
|
|
119
162
|
res.status(500).send();
|
|
120
163
|
}
|
|
121
164
|
}
|
package/index.js
CHANGED
|
@@ -8,5 +8,6 @@ export { default as halDecorator } from "./hal/index.js";
|
|
|
8
8
|
export { default as formatter } from "./formatter/index.js";
|
|
9
9
|
export { HealthCheck as healthCheck } from "./health/index.js";
|
|
10
10
|
export { default as queue } from "./queue/index.js";
|
|
11
|
+
export { default as paperTrail } from "./paper-trail/index.js";
|
|
11
12
|
export * from "./utils/index.js";
|
|
12
13
|
export * from "./safe_proxy/index.js";
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sera4/essentia",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.14",
|
|
4
4
|
"description": "A library of utilities for Teleporte Web Services",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"test": "NODE_ENV=test ./node_modules/.bin/mocha --forbid-only --reporter mocha-junit-reporter --reporter-options mochaFile=./test-results/result.xml --exit",
|
|
9
|
-
"test:dev": "NODE_ENV=test ./node_modules/.bin/mocha --
|
|
9
|
+
"test:dev": "NODE_ENV=test ./node_modules/.bin/mocha --forbid-only",
|
|
10
10
|
"test:only": "NODE_ENV=test ./node_modules/.bin/mocha --watch",
|
|
11
11
|
"test:pretty": "NODE_ENV=test ./node_modules/.bin/mocha --reporter mocha-simple-html-reporter --reporter-options output=./test-results/result.html --exit",
|
|
12
12
|
"last-commit": "node last-commit.js"
|
|
@@ -30,11 +30,12 @@
|
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"amqplib": "^0.8.0",
|
|
32
32
|
"async": "^2.6.3",
|
|
33
|
-
"axios": "^0.21.
|
|
33
|
+
"axios": "^0.21.4",
|
|
34
|
+
"deep-diff": "^1.0.2",
|
|
34
35
|
"fast-safe-stringify": "^2.0.7",
|
|
35
36
|
"git-rev": "^0.2.1",
|
|
36
37
|
"lodash": "^4.17.21",
|
|
37
|
-
"url-parse": "^1.5.
|
|
38
|
+
"url-parse": "^1.5.3",
|
|
38
39
|
"uuid": "^3.4.0",
|
|
39
40
|
"winston": "^3.3.3",
|
|
40
41
|
"winston-logstash": "^0.4.0"
|
package/package.tar.gz
CHANGED
|
Binary file
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Sequelize Paper Trail
|
|
2
|
+
|
|
3
|
+
A paper trail/sequelize hook plugin. This plugin has been modified to use the node14 CLS API called AsyncLocalStorage as well as fix some of the postgres related bugs. We have kept our own implemnetation and various changes were needed to support the express framework and pushing this to the public would hinder the general usability of this library for all.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
In your database setup, setup your models and then attach the paperTrail to them. Many default options need to be changed to support underscore notation and the use of Account instead of User as the default table, thus suggestions options for TWS projects are shown below.
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
import { paperTrail } from "@sera4/essentia"
|
|
11
|
+
import Sequelize from 'sequelize';
|
|
12
|
+
|
|
13
|
+
...
|
|
14
|
+
|
|
15
|
+
const sequelize = new Sequelize(...);
|
|
16
|
+
|
|
17
|
+
PaperTrail.init(sequelize, Sequelize, { UUID: true,
|
|
18
|
+
underscored: true, enableMigration: true, underscoredAttributes: true,
|
|
19
|
+
debug: false, userModel: 'Account', enableCompression: true });
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const db = {};
|
|
23
|
+
// for any model you want enabled
|
|
24
|
+
db["Tenant"] = TenantModel(sequelize, Sequelize);
|
|
25
|
+
|
|
26
|
+
db.PaperTrail = PaperTrail.init(sequelize, Sequelize, { UUID: true, underscored: true,
|
|
27
|
+
enableMigration: true, underscoredAttributes: true, continuationKey: "accountId",
|
|
28
|
+
debug: false, userModel: 'Account', userModelAttribute: 'account_id',
|
|
29
|
+
enableCompression: true });
|
|
30
|
+
|
|
31
|
+
db.PaperTrail.defineModels();
|
|
32
|
+
|
|
33
|
+
.. and for any model that needs a papertrail, call hasPaperTrail() on it ..
|
|
34
|
+
db["Tenant"].hasPaperTrail();
|
|
35
|
+
db["TenantLicense"].hasPaperTrail();
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
and similarly, to use the automatic ALS (asyncLocalStorage) for automatic setting of the account/userID - add this into your middleware
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
import db from "../database/models/index.js";
|
|
42
|
+
|
|
43
|
+
let alsEnabled = db.PaperTrail.alsEnabled();
|
|
44
|
+
|
|
45
|
+
const localStorageMiddleware = async (req, res, next) => {
|
|
46
|
+
await asyncLocalStorage.run(new Map(), () => {
|
|
47
|
+
if (!alsEnabled) {
|
|
48
|
+
alsEnabled = true;
|
|
49
|
+
db.PaperTrail.setupALS(asyncLocalStorage);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
next();
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
```
|
|
56
|
+
as this will ensure that the ALS context is initialized AFTER the first `run()` invocation.
|
|
57
|
+
|
|
58
|
+
`Please note`: For automatic accountId fetching, it is imperitive that you add
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
asyncLocalStorage.getStore().set("accountId", accountId);
|
|
62
|
+
```
|
|
63
|
+
somewhere into your express middleware so the database hooks can pickup the accountId when changes are made.
|
|
64
|
+
|
|
65
|
+
## History
|
|
66
|
+
|
|
67
|
+
This is a private fork/implementation of https://github.com/nielsgl/sequelize-paper-trail which has been released under MIT license. This was privitized due to the number of commented out items/untested in their project as well as the change from CLS (continuation-local-storage) which is experimental to the supported AsyncLocalStorage which was stabilized in Node 14 as the CLS API 101.
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import pkg from 'deep-diff';
|
|
2
|
+
import _ from 'lodash';
|
|
3
|
+
|
|
4
|
+
const { diff } = pkg;
|
|
5
|
+
const capitalizeFirstLetter = string =>
|
|
6
|
+
string.charAt(0).toUpperCase() + string.slice(1);
|
|
7
|
+
|
|
8
|
+
const toUnderscored = obj => {
|
|
9
|
+
_.forEach(obj, (k, v) => {
|
|
10
|
+
obj[k] = v
|
|
11
|
+
.replace(/(?:^|\.?)([A-Z])/g, (x, y) => `_${y.toLowerCase()}`)
|
|
12
|
+
.replace(/^_/, '');
|
|
13
|
+
});
|
|
14
|
+
return obj;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const calcDelta = (current, next, exclude, strict, DEBUG = false) => {
|
|
18
|
+
if (DEBUG) {
|
|
19
|
+
console.log('current', current);
|
|
20
|
+
console.log('next', next);
|
|
21
|
+
console.log('exclude', exclude);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const di = diff(current, next);
|
|
25
|
+
|
|
26
|
+
if (DEBUG) {
|
|
27
|
+
console.log('di', di);
|
|
28
|
+
|
|
29
|
+
let diffs2 = [];
|
|
30
|
+
if (di) {
|
|
31
|
+
diffs2 = di.map(i =>
|
|
32
|
+
JSON.parse(JSON.stringify(i).replace('"__data",', '')),
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
console.log('diffs2', diffs2);
|
|
37
|
+
|
|
38
|
+
console.log(
|
|
39
|
+
'filter1',
|
|
40
|
+
diffs2.filter(i => i.path.join(',').indexOf('_') === -1),
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
console.log(
|
|
44
|
+
'filter2',
|
|
45
|
+
diffs2.filter(i => exclude.every(x => i.path.indexOf(x) === -1)),
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let diffs = [];
|
|
50
|
+
if (di) {
|
|
51
|
+
diffs = di
|
|
52
|
+
.map(i => JSON.parse(JSON.stringify(i).replace('"__data",', '')))
|
|
53
|
+
.filter(i => {
|
|
54
|
+
if (!strict && i.kind === 'E') {
|
|
55
|
+
// eslint-disable-next-line eqeqeq
|
|
56
|
+
if (i.lhs != i.rhs) return i;
|
|
57
|
+
} else return i;
|
|
58
|
+
return false;
|
|
59
|
+
})
|
|
60
|
+
.filter(i => exclude.every(x => i.path.indexOf(x) === -1));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (diffs.length > 0) {
|
|
64
|
+
return diffs;
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const diffToString = val => {
|
|
70
|
+
if (typeof val === 'undefined' || val === null)
|
|
71
|
+
return '';
|
|
72
|
+
|
|
73
|
+
if (val === true)
|
|
74
|
+
return '1';
|
|
75
|
+
|
|
76
|
+
if (val === false)
|
|
77
|
+
return '0';
|
|
78
|
+
|
|
79
|
+
if (typeof val === 'string')
|
|
80
|
+
return val;
|
|
81
|
+
|
|
82
|
+
if (!Number.isNaN(Number(val)))
|
|
83
|
+
return `${String(val)}`;
|
|
84
|
+
|
|
85
|
+
if ((typeof val === 'undefined' ? 'undefined' : typeof val) === 'object')
|
|
86
|
+
return `${JSON.stringify(val)}`;
|
|
87
|
+
|
|
88
|
+
if (Array.isArray(val))
|
|
89
|
+
return `${JSON.stringify(val)}`;
|
|
90
|
+
|
|
91
|
+
return '';
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export default {
|
|
95
|
+
capitalizeFirstLetter,
|
|
96
|
+
toUnderscored,
|
|
97
|
+
calcDelta,
|
|
98
|
+
diffToString,
|
|
99
|
+
};
|
|
@@ -0,0 +1,637 @@
|
|
|
1
|
+
import * as jsdiff from 'diff';
|
|
2
|
+
import _ from 'lodash';
|
|
3
|
+
import helpers from './helpers.js';
|
|
4
|
+
// import Sequelize from 'sequelize';
|
|
5
|
+
|
|
6
|
+
let failHard = false;
|
|
7
|
+
let als;
|
|
8
|
+
let Sequelize = null
|
|
9
|
+
|
|
10
|
+
const paperTrail = {};
|
|
11
|
+
|
|
12
|
+
paperTrail.init = (sequelize, sequelizePackage, optionsArg) => {
|
|
13
|
+
Sequelize = sequelizePackage;
|
|
14
|
+
// In case that options is being parsed as a readonly attribute.
|
|
15
|
+
// Or it is not passed at all
|
|
16
|
+
const optsArg = _.cloneDeep(optionsArg || {});
|
|
17
|
+
|
|
18
|
+
const defaultOptions = {
|
|
19
|
+
debug: false,
|
|
20
|
+
log: null,
|
|
21
|
+
exclude: [
|
|
22
|
+
'id',
|
|
23
|
+
'createdAt',
|
|
24
|
+
'updatedAt',
|
|
25
|
+
'deletedAt',
|
|
26
|
+
'created_at',
|
|
27
|
+
'updated_at',
|
|
28
|
+
'deleted_at',
|
|
29
|
+
'revision',
|
|
30
|
+
],
|
|
31
|
+
revisionAttribute: 'revision',
|
|
32
|
+
revisionModel: 'Revision',
|
|
33
|
+
revisionChangeModel: 'RevisionChange',
|
|
34
|
+
enableRevisionChangeModel: false,
|
|
35
|
+
UUID: false,
|
|
36
|
+
underscored: false,
|
|
37
|
+
underscoredAttributes: false,
|
|
38
|
+
defaultAttributes: {
|
|
39
|
+
documentId: 'documentId',
|
|
40
|
+
revisionId: 'revisionId',
|
|
41
|
+
},
|
|
42
|
+
als: null,
|
|
43
|
+
userModel: false,
|
|
44
|
+
userModelAttribute: 'userId',
|
|
45
|
+
enableCompression: false,
|
|
46
|
+
enableMigration: false,
|
|
47
|
+
enableStrictDiff: true,
|
|
48
|
+
continuationNamespace: null,
|
|
49
|
+
continuationKey: 'userId',
|
|
50
|
+
metaDataFields: null,
|
|
51
|
+
metaDataContinuationKey: 'metaData',
|
|
52
|
+
mysql: false,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
if (optsArg.underscoredAttributes) {
|
|
56
|
+
helpers.toUnderscored(defaultOptions.defaultAttributes);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const options = _.defaults(optsArg, defaultOptions);
|
|
60
|
+
|
|
61
|
+
const log = options.log || console.log;
|
|
62
|
+
|
|
63
|
+
function createBeforeHook(operation) {
|
|
64
|
+
|
|
65
|
+
const beforeHook = function beforeHook(instance, opt) {
|
|
66
|
+
if (options.debug) {
|
|
67
|
+
log('beforeHook called');
|
|
68
|
+
log('instance:', instance);
|
|
69
|
+
log('opt:', opt);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (opt.noPaperTrail) {
|
|
73
|
+
if (options.debug) {
|
|
74
|
+
log('noPaperTrail opt: is true, not logging');
|
|
75
|
+
}
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const destroyOperation = operation === 'destroy';
|
|
80
|
+
|
|
81
|
+
let previousVersion = {};
|
|
82
|
+
let currentVersion = {};
|
|
83
|
+
if (!destroyOperation && options.enableCompression) {
|
|
84
|
+
_.forEach(opt.defaultFields, a => {
|
|
85
|
+
previousVersion[a] = instance._previousDataValues[a];
|
|
86
|
+
currentVersion[a] = instance.dataValues[a];
|
|
87
|
+
});
|
|
88
|
+
} else {
|
|
89
|
+
previousVersion = instance._previousDataValues;
|
|
90
|
+
currentVersion = instance.dataValues;
|
|
91
|
+
}
|
|
92
|
+
// Supported nested models.
|
|
93
|
+
previousVersion = _.omitBy(
|
|
94
|
+
previousVersion,
|
|
95
|
+
i => i != null && typeof i === 'object' && !(i instanceof Date),
|
|
96
|
+
);
|
|
97
|
+
previousVersion = _.omit(previousVersion, options.exclude);
|
|
98
|
+
|
|
99
|
+
currentVersion = _.omitBy(
|
|
100
|
+
currentVersion,
|
|
101
|
+
i => i != null && typeof i === 'object' && !(i instanceof Date),
|
|
102
|
+
);
|
|
103
|
+
currentVersion = _.omit(currentVersion, options.exclude);
|
|
104
|
+
|
|
105
|
+
// Disallow change of revision
|
|
106
|
+
instance.set(
|
|
107
|
+
options.revisionAttribute,
|
|
108
|
+
instance._previousDataValues[options.revisionAttribute],
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
// Get diffs
|
|
112
|
+
const delta = helpers.calcDelta(
|
|
113
|
+
previousVersion,
|
|
114
|
+
currentVersion,
|
|
115
|
+
options.exclude,
|
|
116
|
+
options.enableStrictDiff,
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
const currentRevisionId = instance.get(options.revisionAttribute);
|
|
120
|
+
|
|
121
|
+
if (failHard && !currentRevisionId && opt.type === 'UPDATE') {
|
|
122
|
+
throw new Error('Revision Id was undefined');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (options.debug) {
|
|
126
|
+
log('delta:', delta);
|
|
127
|
+
log('revisionId', currentRevisionId);
|
|
128
|
+
}
|
|
129
|
+
// Check if all required fields have been provided to the opts / ALS
|
|
130
|
+
if (options.metaDataFields) {
|
|
131
|
+
// get all required field keys as an array
|
|
132
|
+
const requiredFields = _.keys(
|
|
133
|
+
_.pickBy(
|
|
134
|
+
options.metaDataFields,
|
|
135
|
+
function isMetaDataFieldRequired(required) {
|
|
136
|
+
return required;
|
|
137
|
+
},
|
|
138
|
+
),
|
|
139
|
+
);
|
|
140
|
+
if (requiredFields && requiredFields.length) {
|
|
141
|
+
const store = options.als && options.als.getStore();
|
|
142
|
+
const metaData = (store && store.get(metaDataContinuationKey)) || opt.metaData
|
|
143
|
+
const requiredFieldsProvided = _.filter(
|
|
144
|
+
requiredFields,
|
|
145
|
+
function isMetaDataFieldNonUndefined(field) {
|
|
146
|
+
return metaData[field] !== undefined;
|
|
147
|
+
},
|
|
148
|
+
);
|
|
149
|
+
if (
|
|
150
|
+
requiredFieldsProvided.length !== requiredFields.length
|
|
151
|
+
) {
|
|
152
|
+
log(
|
|
153
|
+
'Required fields: ',
|
|
154
|
+
options.metaDataFields,
|
|
155
|
+
requiredFields,
|
|
156
|
+
);
|
|
157
|
+
log(
|
|
158
|
+
'Required fields provided: ',
|
|
159
|
+
metaData,
|
|
160
|
+
requiredFieldsProvided,
|
|
161
|
+
);
|
|
162
|
+
throw new Error(
|
|
163
|
+
'Not all required fields are provided to paper trail!',
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (destroyOperation || (delta && delta.length > 0)) {
|
|
170
|
+
const revisionId = (currentRevisionId || 0) + 1;
|
|
171
|
+
instance.set(options.revisionAttribute, revisionId);
|
|
172
|
+
|
|
173
|
+
if (!instance.context) {
|
|
174
|
+
instance.context = {};
|
|
175
|
+
}
|
|
176
|
+
instance.context.delta = delta;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (options.debug) {
|
|
180
|
+
log('end of beforeHook');
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
return beforeHook;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function createAfterHook(operation) {
|
|
187
|
+
const afterHook = function afterHook(instance, opt) {
|
|
188
|
+
const store = options.als && options.als.getStore()
|
|
189
|
+
if (options.debug) {
|
|
190
|
+
log('afterHook called');
|
|
191
|
+
log('instance:', instance);
|
|
192
|
+
log('opt:', opt);
|
|
193
|
+
|
|
194
|
+
if (store) {
|
|
195
|
+
log(
|
|
196
|
+
`ALS ${options.continuationKey}:`,
|
|
197
|
+
store.get(options.continuationKey),
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const destroyOperation = operation === 'destroy';
|
|
203
|
+
|
|
204
|
+
if (
|
|
205
|
+
instance.context &&
|
|
206
|
+
((instance.context.delta &&
|
|
207
|
+
instance.context.delta.length > 0) ||
|
|
208
|
+
destroyOperation)
|
|
209
|
+
) {
|
|
210
|
+
const Revision = sequelize.model(options.revisionModel);
|
|
211
|
+
let RevisionChange;
|
|
212
|
+
|
|
213
|
+
if (options.enableRevisionChangeModel) {
|
|
214
|
+
RevisionChange = sequelize.model(
|
|
215
|
+
options.revisionChangeModel,
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const { delta } = instance.context;
|
|
220
|
+
|
|
221
|
+
let previousVersion = {};
|
|
222
|
+
let currentVersion = {};
|
|
223
|
+
if (!destroyOperation && options.enableCompression) {
|
|
224
|
+
_.forEach(opt.defaultFields, a => {
|
|
225
|
+
previousVersion[a] = instance._previousDataValues[a];
|
|
226
|
+
currentVersion[a] = instance.dataValues[a];
|
|
227
|
+
});
|
|
228
|
+
} else {
|
|
229
|
+
previousVersion = instance._previousDataValues;
|
|
230
|
+
currentVersion = instance.dataValues;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Supported nested models.
|
|
234
|
+
previousVersion = _.omitBy(
|
|
235
|
+
previousVersion,
|
|
236
|
+
i =>
|
|
237
|
+
i != null &&
|
|
238
|
+
typeof i === 'object' &&
|
|
239
|
+
!(i instanceof Date),
|
|
240
|
+
);
|
|
241
|
+
previousVersion = _.omit(previousVersion, options.exclude);
|
|
242
|
+
|
|
243
|
+
currentVersion = _.omitBy(
|
|
244
|
+
currentVersion,
|
|
245
|
+
i =>
|
|
246
|
+
i != null &&
|
|
247
|
+
typeof i === 'object' &&
|
|
248
|
+
!(i instanceof Date),
|
|
249
|
+
);
|
|
250
|
+
currentVersion = _.omit(currentVersion, options.exclude);
|
|
251
|
+
|
|
252
|
+
if (!options.als)
|
|
253
|
+
options.als = asyncLocalStorage;
|
|
254
|
+
|
|
255
|
+
const store = options.als && options.als.getStore()
|
|
256
|
+
if (store && !store.has(options.continuationKey)) {
|
|
257
|
+
if (failHard)
|
|
258
|
+
throw new Error(
|
|
259
|
+
`The ALS continuationKey ${options.continuationKey} was not defined.`,
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
let document = currentVersion;
|
|
264
|
+
|
|
265
|
+
if (options.mysql) {
|
|
266
|
+
document = JSON.stringify(document);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Build revision
|
|
270
|
+
const query = {
|
|
271
|
+
model: this.name,
|
|
272
|
+
document,
|
|
273
|
+
operation,
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
// Add all extra data fields to the query object
|
|
277
|
+
if (options.metaDataFields) {
|
|
278
|
+
const metaData =
|
|
279
|
+
(store && store.get(options.metaDataContinuationKey)) ||
|
|
280
|
+
opt.metaData;
|
|
281
|
+
if (metaData) {
|
|
282
|
+
_.forEach(
|
|
283
|
+
options.metaDataFields,
|
|
284
|
+
function getMetaDataValues(required, field) {
|
|
285
|
+
const value = metaData[field];
|
|
286
|
+
if (options.debug) {
|
|
287
|
+
log(
|
|
288
|
+
`Adding metaData field to Revision - ${field} => ${value}`,
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
if (!(field in query)) {
|
|
292
|
+
query[field] = value;
|
|
293
|
+
} else if (options.debug) {
|
|
294
|
+
log(
|
|
295
|
+
`Revision object already has a value at ${field} => ${query[field]}`,
|
|
296
|
+
);
|
|
297
|
+
log('Not overwriting the original value');
|
|
298
|
+
}
|
|
299
|
+
},
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// in case of custom user models that are not 'userId'
|
|
305
|
+
query[options.userModelAttribute] =
|
|
306
|
+
(store && store.get(options.continuationKey)) || opt.userId;
|
|
307
|
+
|
|
308
|
+
query[options.defaultAttributes.documentId] = instance.id;
|
|
309
|
+
|
|
310
|
+
const revision = Revision.build(query);
|
|
311
|
+
|
|
312
|
+
revision[options.revisionAttribute] = instance.get(
|
|
313
|
+
options.revisionAttribute,
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
// Save revision(
|
|
317
|
+
return revision
|
|
318
|
+
.save({ transaction: opt.transaction })
|
|
319
|
+
.then(objectRevision => {
|
|
320
|
+
// Loop diffs and create a revision-diff for each
|
|
321
|
+
if (options.enableRevisionChangeModel) {
|
|
322
|
+
_.forEach(delta, difference => {
|
|
323
|
+
const o = helpers.diffToString(
|
|
324
|
+
difference.item
|
|
325
|
+
? difference.item.lhs
|
|
326
|
+
: difference.lhs,
|
|
327
|
+
);
|
|
328
|
+
const n = helpers.diffToString(
|
|
329
|
+
difference.item
|
|
330
|
+
? difference.item.rhs
|
|
331
|
+
: difference.rhs,
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
// let document = difference;
|
|
335
|
+
document = difference;
|
|
336
|
+
let diff = o || n ? jsdiff.diffChars(o, n) : [];
|
|
337
|
+
|
|
338
|
+
if (options.mysql) {
|
|
339
|
+
document = JSON.stringify(document);
|
|
340
|
+
diff = JSON.stringify(diff);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const d = RevisionChange.build({
|
|
344
|
+
path: difference.path[0],
|
|
345
|
+
document,
|
|
346
|
+
diff,
|
|
347
|
+
revisionId: objectRevision.id,
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
d.save({ transaction: opt.transaction })
|
|
351
|
+
.then(savedD => {
|
|
352
|
+
// Add diff to revision
|
|
353
|
+
objectRevision[
|
|
354
|
+
`add${helpers.capitalizeFirstLetter(
|
|
355
|
+
options.revisionChangeModel,
|
|
356
|
+
)}`
|
|
357
|
+
](savedD);
|
|
358
|
+
|
|
359
|
+
return null;
|
|
360
|
+
})
|
|
361
|
+
.catch(err => {
|
|
362
|
+
log('RevisionChange save error', err);
|
|
363
|
+
throw err;
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
return null;
|
|
368
|
+
})
|
|
369
|
+
.catch(err => {
|
|
370
|
+
log('Revision save error', err);
|
|
371
|
+
throw err;
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (options.debug) {
|
|
376
|
+
log('end of afterHook');
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return null;
|
|
380
|
+
};
|
|
381
|
+
return afterHook;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// order in which sequelize processes the hooks
|
|
385
|
+
// (1)
|
|
386
|
+
// beforeBulkCreate(instances, options, fn)
|
|
387
|
+
// beforeBulkDestroy(instances, options, fn)
|
|
388
|
+
// beforeBulkUpdate(instances, options, fn)
|
|
389
|
+
// (2)
|
|
390
|
+
// beforeValidate(instance, options, fn)
|
|
391
|
+
// (-)
|
|
392
|
+
// validate
|
|
393
|
+
// (3)
|
|
394
|
+
// afterValidate(instance, options, fn)
|
|
395
|
+
// - or -
|
|
396
|
+
// validationFailed(instance, options, error, fn)
|
|
397
|
+
// (4)
|
|
398
|
+
// beforeCreate(instance, options, fn)
|
|
399
|
+
// beforeDestroy(instance, options, fn)
|
|
400
|
+
// beforeUpdate(instance, options, fn)
|
|
401
|
+
// (-)
|
|
402
|
+
// create
|
|
403
|
+
// destroy
|
|
404
|
+
// update
|
|
405
|
+
// (5)
|
|
406
|
+
// afterCreate(instance, options, fn)
|
|
407
|
+
// afterDestroy(instance, options, fn)
|
|
408
|
+
// afterUpdate(instance, options, fn)
|
|
409
|
+
// (6)
|
|
410
|
+
// afterBulkCreate(instances, options, fn)
|
|
411
|
+
// afterBulkDestroy(instances, options, fn)
|
|
412
|
+
// afterBulkUpdate(instances, options, fn)
|
|
413
|
+
|
|
414
|
+
// Extend model prototype with "hasPaperTrail" function
|
|
415
|
+
// Call model.hasPaperTrail() to enable revisions for model
|
|
416
|
+
_.assignIn(Sequelize.Model, {
|
|
417
|
+
hasPaperTrail: function hasPaperTrail() {
|
|
418
|
+
if (options.debug) {
|
|
419
|
+
log('Enabling paper trail on', this.name);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
this.rawAttributes[options.revisionAttribute] = {
|
|
423
|
+
type: Sequelize.INTEGER,
|
|
424
|
+
defaultValue: 0,
|
|
425
|
+
};
|
|
426
|
+
this.revisionable = true;
|
|
427
|
+
|
|
428
|
+
// not sure if we need this
|
|
429
|
+
this.refreshAttributes();
|
|
430
|
+
|
|
431
|
+
if (options.enableMigration) {
|
|
432
|
+
const tableName = this.getTableName();
|
|
433
|
+
|
|
434
|
+
const queryInterface = sequelize.getQueryInterface();
|
|
435
|
+
|
|
436
|
+
queryInterface.describeTable(tableName).then(attributes => {
|
|
437
|
+
if (!attributes[options.revisionAttribute]) {
|
|
438
|
+
if (options.debug) {
|
|
439
|
+
log('adding revision attribute to the database');
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
queryInterface
|
|
443
|
+
.addColumn(tableName, options.revisionAttribute, {
|
|
444
|
+
type: Sequelize.INTEGER,
|
|
445
|
+
defaultValue: 0,
|
|
446
|
+
})
|
|
447
|
+
.then(() => null)
|
|
448
|
+
.catch(err => {
|
|
449
|
+
log('something went really wrong..', err);
|
|
450
|
+
return null;
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
return null;
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
this.addHook('beforeCreate', createBeforeHook('create'));
|
|
458
|
+
this.addHook('beforeDestroy', createBeforeHook('destroy'));
|
|
459
|
+
this.addHook('beforeUpdate', createBeforeHook('update'));
|
|
460
|
+
this.addHook('afterCreate', createAfterHook('create'));
|
|
461
|
+
this.addHook('afterDestroy', createAfterHook('destroy'));
|
|
462
|
+
this.addHook('afterUpdate', createAfterHook('update'));
|
|
463
|
+
|
|
464
|
+
// create association
|
|
465
|
+
return this.hasMany(sequelize.models[options.revisionModel], {
|
|
466
|
+
foreignKey: options.defaultAttributes.documentId,
|
|
467
|
+
constraints: false,
|
|
468
|
+
scope: {
|
|
469
|
+
model: this.name,
|
|
470
|
+
},
|
|
471
|
+
});
|
|
472
|
+
},
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
return {
|
|
476
|
+
// TWS-1032 since ALS must be set after a .run()
|
|
477
|
+
// invocation this CANNOT be set on init(). If you would
|
|
478
|
+
// like to use ALS, set it after the first invocation of run()
|
|
479
|
+
setupALS: function(asyncLocalStorage) {
|
|
480
|
+
options.als = asyncLocalStorage;
|
|
481
|
+
},
|
|
482
|
+
alsEnabled: function() {
|
|
483
|
+
return (options.als === true)
|
|
484
|
+
},
|
|
485
|
+
defineModels: function defineModels(db) {
|
|
486
|
+
// Attributes for RevisionModel
|
|
487
|
+
let attributes = {
|
|
488
|
+
model: {
|
|
489
|
+
type: Sequelize.TEXT,
|
|
490
|
+
allowNull: false,
|
|
491
|
+
},
|
|
492
|
+
document: {
|
|
493
|
+
type: Sequelize.JSONB,
|
|
494
|
+
allowNull: false,
|
|
495
|
+
},
|
|
496
|
+
operation: Sequelize.STRING(7),
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
if (options.mysql) {
|
|
500
|
+
attributes.document.type = Sequelize.TEXT('MEDIUMTEXT');
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
attributes[options.defaultAttributes.documentId] = {
|
|
504
|
+
type: Sequelize.INTEGER,
|
|
505
|
+
allowNull: false,
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
attributes[options.revisionAttribute] = {
|
|
509
|
+
type: Sequelize.INTEGER,
|
|
510
|
+
allowNull: false,
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
attributes[options.userModelAttribute] = {
|
|
514
|
+
type: Sequelize.INTEGER,
|
|
515
|
+
allowNull: true
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
if (options.UUID) {
|
|
519
|
+
attributes.id = {
|
|
520
|
+
primaryKey: true,
|
|
521
|
+
type: Sequelize.UUID,
|
|
522
|
+
defaultValue: Sequelize.UUIDV4,
|
|
523
|
+
};
|
|
524
|
+
attributes[options.defaultAttributes.documentId].type =
|
|
525
|
+
Sequelize.UUID;
|
|
526
|
+
|
|
527
|
+
console.log("userModelatribute", options.userModelAttribute)
|
|
528
|
+
attributes[options.userModelAttribute].type = Sequelize.UUID;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
if (options.debug) {
|
|
532
|
+
log('attributes', attributes);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Revision model
|
|
536
|
+
const Revision = sequelize.define(
|
|
537
|
+
options.revisionModel,
|
|
538
|
+
attributes,
|
|
539
|
+
{
|
|
540
|
+
underscored: options.underscored,
|
|
541
|
+
tableName: options.tableName,
|
|
542
|
+
},
|
|
543
|
+
);
|
|
544
|
+
Revision.associate = function associate(models) {
|
|
545
|
+
if (options.debug) {
|
|
546
|
+
log('models', models);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
Revision.belongsTo(
|
|
550
|
+
sequelize.model(options.userModel),
|
|
551
|
+
options.belongsToUserOptions,
|
|
552
|
+
);
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
if (options.enableRevisionChangeModel) {
|
|
556
|
+
// Attributes for RevisionChangeModel
|
|
557
|
+
attributes = {
|
|
558
|
+
path: {
|
|
559
|
+
type: Sequelize.TEXT,
|
|
560
|
+
allowNull: false,
|
|
561
|
+
},
|
|
562
|
+
document: {
|
|
563
|
+
type: Sequelize.JSONB,
|
|
564
|
+
allowNull: false,
|
|
565
|
+
},
|
|
566
|
+
diff: {
|
|
567
|
+
type: Sequelize.JSONB,
|
|
568
|
+
allowNull: false,
|
|
569
|
+
},
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
if (options.mysql) {
|
|
573
|
+
attributes.document.type = Sequelize.TEXT('MEDIUMTEXT');
|
|
574
|
+
attributes.diff.type = Sequelize.TEXT('MEDIUMTEXT');
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
if (options.UUID) {
|
|
578
|
+
attributes.id = {
|
|
579
|
+
primaryKey: true,
|
|
580
|
+
type: Sequelize.UUID,
|
|
581
|
+
defaultValue: Sequelize.UUIDV4,
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
// RevisionChange model
|
|
585
|
+
const RevisionChange = sequelize.define(
|
|
586
|
+
options.revisionChangeModel,
|
|
587
|
+
attributes,
|
|
588
|
+
{
|
|
589
|
+
underscored: options.underscored,
|
|
590
|
+
},
|
|
591
|
+
);
|
|
592
|
+
|
|
593
|
+
// Set associations
|
|
594
|
+
Revision.hasMany(RevisionChange, {
|
|
595
|
+
foreignKey: options.defaultAttributes.revisionId,
|
|
596
|
+
constraints: false,
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
// https://github.com/nielsgl/sequelize-paper-trail/issues/10
|
|
600
|
+
// RevisionChange.belongsTo(Revision, {
|
|
601
|
+
// foreignKey: options.defaultAttributes.revisionId,
|
|
602
|
+
// });
|
|
603
|
+
RevisionChange.belongsTo(Revision);
|
|
604
|
+
|
|
605
|
+
if (db) db[RevisionChange.name] = RevisionChange;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
if (db) db[Revision.name] = Revision;
|
|
609
|
+
|
|
610
|
+
/*
|
|
611
|
+
* We could extract this to a separate function so that having a
|
|
612
|
+
* user model doesn't require different loading
|
|
613
|
+
*
|
|
614
|
+
* or perhaps we could omit this because we are creating the
|
|
615
|
+
* association through the associate call above.
|
|
616
|
+
*/
|
|
617
|
+
if (options.userModel) {
|
|
618
|
+
Revision.belongsTo(
|
|
619
|
+
sequelize.model(options.userModel),
|
|
620
|
+
options.belongsToUserOptions,
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
return Revision;
|
|
625
|
+
},
|
|
626
|
+
};
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Throw exceptions when the user identifier from ALS is not set or if the
|
|
631
|
+
* revisionAttribute was not loaded on the model.
|
|
632
|
+
*/
|
|
633
|
+
paperTrail.enableFailHard = () => {
|
|
634
|
+
failHard = true;
|
|
635
|
+
};
|
|
636
|
+
|
|
637
|
+
export default paperTrail;
|