anote-server-libs 0.5.3 → 0.6.1

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/package.json CHANGED
@@ -1,36 +1,36 @@
1
- {
2
- "name": "anote-server-libs",
3
- "version": "0.5.3",
4
- "description": "Helpers for express-TS servers",
5
- "scripts": {
6
- "test": "echo \"Error: no test specified\" && exit 1",
7
- "build": "tsc -p tsconfig.json"
8
- },
9
- "repository": {
10
- "type": "git",
11
- "url": "https://gitlab.anotemusic.com/anote/anote-server-libs.git"
12
- },
13
- "keywords": [
14
- "Express"
15
- ],
16
- "author": "Mathonet Gregoire",
17
- "license": "MIT",
18
- "dependencies": {
19
- "@types/memcached": "2.2.9",
20
- "@types/mssql": "9.1.2",
21
- "@types/pg": "8.10.7",
22
- "ajv": "8.12.0",
23
- "memcached": "2.2.2",
24
- "mssql": "10.0.1",
25
- "pg": "8.11.3"
26
- },
27
- "peerDependencies": {
28
- "express": "4.18.2",
29
- "winston": "3.11.0"
30
- },
31
- "devDependencies": {
32
- "@types/express": "4.17.20",
33
- "@types/node": "18.15.0",
34
- "typescript": "5.2.2"
35
- }
36
- }
1
+ {
2
+ "name": "anote-server-libs",
3
+ "version": "0.6.1",
4
+ "description": "Helpers for express-TS servers",
5
+ "scripts": {
6
+ "test": "echo \"Error: no test specified\" && exit 1",
7
+ "build": "tsc -p tsconfig.json"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://gitlab.anotemusic.com/anote/anote-server-libs.git"
12
+ },
13
+ "keywords": [
14
+ "Express"
15
+ ],
16
+ "author": "Mathonet Gregoire",
17
+ "license": "MIT",
18
+ "dependencies": {
19
+ "@types/memcached": "2.2.10",
20
+ "@types/mssql": "9.1.5",
21
+ "@types/pg": "8.11.6",
22
+ "ajv": "8.13.0",
23
+ "memcached": "2.2.2",
24
+ "mssql": "10.0.2",
25
+ "pg": "8.11.5"
26
+ },
27
+ "peerDependencies": {
28
+ "express": "4.19.2",
29
+ "winston": "3.13.0"
30
+ },
31
+ "devDependencies": {
32
+ "@types/express": "4.17.21",
33
+ "@types/node": "18.15.0",
34
+ "typescript": "5.4.5"
35
+ }
36
+ }
@@ -1,60 +1,60 @@
1
- import {Request, Response} from 'express';
2
- import Ajv, {_} from 'ajv';
3
-
4
- export const ajv = new Ajv();
5
- ajv.addKeyword({
6
- keyword: 'maxDigits',
7
- type: 'number',
8
- schemaType: 'number',
9
- validate: (schema: number, data: any) => typeof data === 'number' && Math.round(data * 10 ** schema) / (10 ** schema) === data
10
- });
11
-
12
- export function WithBody(schema: any) {
13
- if(schema.type === 'object') {
14
- const required: string[] = [];
15
- const keys = Object.getOwnPropertyNames(schema.properties);
16
- keys.forEach(key => {
17
- if(schema.properties[key].required === true) {
18
- required.push(key);
19
- }
20
- if(schema.properties[key].required === true || schema.properties[key].required === false) {
21
- delete schema.properties[key].required;
22
- }
23
- });
24
- schema.required = required;
25
- }
26
- const validate = ajv.compile(schema);
27
- return function(_: any, __: string, descriptor: PropertyDescriptor) {
28
- if(typeof descriptor.value === 'function') {
29
- const previousMethod = descriptor.value;
30
- descriptor.value = function(this: any, req: Request, res: Response) {
31
- if(req.method.toUpperCase() !== 'POST' && req.method.toUpperCase() !== 'PUT') {
32
- return previousMethod.call(this, req, res);
33
- }
34
- if(!req.body) {
35
- res.status(400).json({
36
- error: {
37
- errorKey: 'client.body.missing',
38
- additionalInfo: 'client.extended.badPayload'
39
- }
40
- });
41
- } else {
42
- const valid = validate(req.body);
43
- if(valid) {
44
- return previousMethod.call(this, req, res);
45
- } else {
46
- res.status(400).json({
47
- error: {
48
- errorKey: 'client.body.missing',
49
- additionalInfo: 'client.extended.badPayload',
50
- detailedInfo: validate.errors
51
- }
52
- });
53
- }
54
- }
55
- };
56
- return descriptor;
57
- }
58
- return undefined;
59
- };
60
- }
1
+ import {Request, Response} from 'express';
2
+ import Ajv, {_} from 'ajv';
3
+
4
+ export const ajv = new Ajv();
5
+ ajv.addKeyword({
6
+ keyword: 'maxDigits',
7
+ type: 'number',
8
+ schemaType: 'number',
9
+ validate: (schema: number, data: any) => typeof data === 'number' && Math.round(data * 10 ** schema) / (10 ** schema) === data
10
+ });
11
+
12
+ export function WithBody(schema: any) {
13
+ if(schema.type === 'object') {
14
+ const required: string[] = [];
15
+ const keys = Object.getOwnPropertyNames(schema.properties);
16
+ keys.forEach(key => {
17
+ if(schema.properties[key].required === true) {
18
+ required.push(key);
19
+ }
20
+ if(schema.properties[key].required === true || schema.properties[key].required === false) {
21
+ delete schema.properties[key].required;
22
+ }
23
+ });
24
+ schema.required = required;
25
+ }
26
+ const validate = ajv.compile(schema);
27
+ return function(_: any, __: string, descriptor: PropertyDescriptor) {
28
+ if(typeof descriptor.value === 'function') {
29
+ const previousMethod = descriptor.value;
30
+ descriptor.value = function(this: any, req: Request, res: Response) {
31
+ if(req.method.toUpperCase() !== 'POST' && req.method.toUpperCase() !== 'PUT') {
32
+ return previousMethod.call(this, req, res);
33
+ }
34
+ if(!req.body) {
35
+ res.status(400).json({
36
+ error: {
37
+ errorKey: 'client.body.missing',
38
+ additionalInfo: 'client.extended.badPayload'
39
+ }
40
+ });
41
+ } else {
42
+ const valid = validate(req.body);
43
+ if(valid) {
44
+ return previousMethod.call(this, req, res);
45
+ } else {
46
+ res.status(400).json({
47
+ error: {
48
+ errorKey: 'client.body.missing',
49
+ additionalInfo: 'client.extended.badPayload',
50
+ detailedInfo: validate.errors
51
+ }
52
+ });
53
+ }
54
+ }
55
+ };
56
+ return descriptor;
57
+ }
58
+ return undefined;
59
+ };
60
+ }
@@ -1,139 +1,139 @@
1
- import {NextFunction, Request, Response} from 'express';
2
- import {Logger} from 'winston';
3
- import {BaseModelRepository} from '../models/repository/BaseModelRepository';
4
- import {jsonStringify, utils} from './utils';
5
-
6
- export const enum SystemLock {
7
- CHECK_CROSSING = 1,
8
- FLUSH_CALLS = 2,
9
- TX_CHECK = 3
10
- }
11
-
12
- export const withTransactionConfig = {
13
- timeoutMillis: 15000
14
- };
15
-
16
- export function withTransaction(repo: BaseModelRepository, logger: Logger, previousMethod: (req: Request, res: Response, next: NextFunction) => void, lock?: SystemLock) {
17
- return function(req: Request, res: Response, next: NextFunction) {
18
- const endTerminator = res.end.bind(res);
19
- const jsonTerminator = (obj: any) => {
20
- res.write(jsonStringify(obj) || '{}');
21
- endTerminator();
22
- };
23
- const connectTimeoutHandler = setTimeout(() => {
24
- // Timed out getting a client, restart worker or process...
25
- logger.error('Error timed out getting a client, exiting...');
26
- process.exit(22);
27
- }, withTransactionConfig.timeoutMillis);
28
- Promise.all([
29
- repo.db ? repo.db.connect() : Promise.resolve(undefined),
30
- repo.dbMssql ? Promise.resolve(repo.dbMssql.transaction()) : Promise.resolve(undefined)
31
- ]).then(([c1, c2]) => {
32
- const dbClient = c1 || c2;
33
- clearTimeout(connectTimeoutHandler);
34
- // On error, will rollback...
35
- utils.logger = logger;
36
- dbClient.removeListener('error', utils.clientErrorHandler);
37
- dbClient.on('error', utils.clientErrorHandler);
38
-
39
- res.locals.dbClient = dbClient;
40
- res.locals.dbClientCommited = false;
41
- res.locals.dbClientCommit = (cb: (err: any) => any) => {
42
- if(!res.locals.dbClientCommited) {
43
- res.locals.dbClientCommited = true;
44
- (repo.db ? dbClient.query('COMMIT') : dbClient.commit()).catch((err: any) => err).then((err: any) => {
45
- if(repo.db) dbClient.release();
46
- if(!(err instanceof Error)) {
47
- for(let i = 0; i < res.locals.dbClientOnCommit.length; i++) {
48
- res.locals.dbClientOnCommit[i]();
49
- }
50
- }
51
- cb(err instanceof Error ? err : undefined);
52
- });
53
- } else {
54
- cb(undefined);
55
- }
56
- };
57
- res.locals.dbClientOnCommit = [];
58
- return (repo.db ? dbClient.query('BEGIN') : dbClient.begin()).then(() => {
59
- const finish = () => {
60
- res.json = (obj: any) => {
61
- if(res.statusCode > 303 && res.statusCode !== 412) {
62
- if(logger) {
63
- if(res.statusCode > 499) {
64
- logger.error('Uncaught 500: %j', obj.error.additionalInfo);
65
- } else {
66
- logger.warn('Client error 4XX: %j', obj.error);
67
- }
68
- }
69
- (repo.db ? dbClient.query('ROLLBACK') : dbClient.rollback()).catch((err: any) => obj.error.additionalInfo2 = {message: err.message}).then(() => {
70
- if(repo.db) dbClient.release();
71
- jsonTerminator(obj);
72
- });
73
- } else {
74
- res.locals.dbClientCommit((err: any) => {
75
- if(err) {
76
- res.status(500);
77
- jsonTerminator({
78
- error: {
79
- errorKey: 'internal.db',
80
- additionalInfo: {message: err.message}
81
- }
82
- });
83
- } else jsonTerminator(obj);
84
- });
85
- }
86
- return res;
87
- };
88
- res.end = () => {
89
- if(res.statusCode > 303 && res.statusCode !== 412) {
90
- if(logger && res.statusCode > 499) {
91
- logger.error('Uncaught 500 with no details...');
92
- }
93
- (repo.db ? dbClient.query('ROLLBACK') : dbClient.rollback()).catch((): any => undefined).then(() => {
94
- if(repo.db) dbClient.release();
95
- endTerminator();
96
- });
97
- } else {
98
- res.locals.dbClientCommit((err: any) => {
99
- if(err) {
100
- res.status(500);
101
- jsonTerminator({
102
- error: {
103
- errorKey: 'internal.db',
104
- additionalInfo: {message: err.message}
105
- }
106
- });
107
- } else endTerminator();
108
- });
109
- }
110
- return res;
111
- };
112
- return previousMethod.call(this, req, res, next);
113
- };
114
-
115
- if(lock) {
116
- dbClient.query('SELECT pg_advisory_xact_lock(' + lock + ')').then(() => finish()).catch((err: any) => {
117
- res.status(500).json({
118
- error: {
119
- errorKey: 'internal.db',
120
- additionalInfo: {message: err.message}
121
- }
122
- });
123
- dbClient.release();
124
- });
125
- } else {
126
- finish();
127
- }
128
- });
129
- }).catch(err => {
130
- // Error connecting to database or acquiring transaction, restarting worker after the timeout as well...
131
- res.status(500).json({
132
- error: {
133
- errorKey: 'internal.db',
134
- additionalInfo: {message: err.message}
135
- }
136
- });
137
- });
138
- };
139
- }
1
+ import {NextFunction, Request, Response} from 'express';
2
+ import {Logger} from 'winston';
3
+ import {BaseModelRepository} from '../models/repository/BaseModelRepository';
4
+ import {jsonStringify, utils} from './utils';
5
+
6
+ export const enum SystemLock {
7
+ CHECK_CROSSING = 1,
8
+ FLUSH_CALLS = 2,
9
+ TX_CHECK = 3
10
+ }
11
+
12
+ export const withTransactionConfig = {
13
+ timeoutMillis: 15000
14
+ };
15
+
16
+ export function withTransaction(repo: BaseModelRepository, logger: Logger, previousMethod: (req: Request, res: Response, next: NextFunction) => void, lock?: SystemLock) {
17
+ return function(req: Request, res: Response, next: NextFunction) {
18
+ const endTerminator = res.end.bind(res);
19
+ const jsonTerminator = (obj: any) => {
20
+ res.write(jsonStringify(obj) || '{}');
21
+ endTerminator();
22
+ };
23
+ const connectTimeoutHandler = setTimeout(() => {
24
+ // Timed out getting a client, restart worker or process...
25
+ logger.error('Error timed out getting a client, exiting...');
26
+ process.exit(22);
27
+ }, withTransactionConfig.timeoutMillis);
28
+ Promise.all([
29
+ repo.db ? repo.db.connect() : Promise.resolve(undefined),
30
+ repo.dbMssql ? Promise.resolve(repo.dbMssql.transaction()) : Promise.resolve(undefined)
31
+ ]).then(([c1, c2]) => {
32
+ const dbClient = c1 || c2;
33
+ clearTimeout(connectTimeoutHandler);
34
+ // On error, will rollback...
35
+ utils.logger = logger;
36
+ dbClient.removeListener('error', utils.clientErrorHandler);
37
+ dbClient.on('error', utils.clientErrorHandler);
38
+
39
+ res.locals.dbClient = dbClient;
40
+ res.locals.dbClientCommited = false;
41
+ res.locals.dbClientCommit = (cb: (err: any) => any) => {
42
+ if(!res.locals.dbClientCommited) {
43
+ res.locals.dbClientCommited = true;
44
+ (repo.db ? dbClient.query('COMMIT') : dbClient.commit()).catch((err: any) => err).then((err: any) => {
45
+ if(repo.db) dbClient.release();
46
+ if(!(err instanceof Error)) {
47
+ for(let i = 0; i < res.locals.dbClientOnCommit.length; i++) {
48
+ res.locals.dbClientOnCommit[i]();
49
+ }
50
+ }
51
+ cb(err instanceof Error ? err : undefined);
52
+ });
53
+ } else {
54
+ cb(undefined);
55
+ }
56
+ };
57
+ res.locals.dbClientOnCommit = [];
58
+ return (repo.db ? dbClient.query('BEGIN') : dbClient.begin()).then(() => {
59
+ const finish = () => {
60
+ res.json = (obj: any) => {
61
+ if(res.statusCode > 303 && res.statusCode !== 412) {
62
+ if(logger) {
63
+ if(res.statusCode > 499) {
64
+ logger.error('Uncaught 500: %j', obj.error.additionalInfo);
65
+ } else {
66
+ logger.warn('Client error 4XX: %j', obj.error);
67
+ }
68
+ }
69
+ (repo.db ? dbClient.query('ROLLBACK') : dbClient.rollback()).catch((err: any) => obj.error.additionalInfo2 = {message: err.message}).then(() => {
70
+ if(repo.db) dbClient.release();
71
+ jsonTerminator(obj);
72
+ });
73
+ } else {
74
+ res.locals.dbClientCommit((err: any) => {
75
+ if(err) {
76
+ res.status(500);
77
+ jsonTerminator({
78
+ error: {
79
+ errorKey: 'internal.db',
80
+ additionalInfo: {message: err.message}
81
+ }
82
+ });
83
+ } else jsonTerminator(obj);
84
+ });
85
+ }
86
+ return res;
87
+ };
88
+ res.end = () => {
89
+ if(res.statusCode > 303 && res.statusCode !== 412) {
90
+ if(logger && res.statusCode > 499) {
91
+ logger.error('Uncaught 500 with no details...');
92
+ }
93
+ (repo.db ? dbClient.query('ROLLBACK') : dbClient.rollback()).catch((): any => undefined).then(() => {
94
+ if(repo.db) dbClient.release();
95
+ endTerminator();
96
+ });
97
+ } else {
98
+ res.locals.dbClientCommit((err: any) => {
99
+ if(err) {
100
+ res.status(500);
101
+ jsonTerminator({
102
+ error: {
103
+ errorKey: 'internal.db',
104
+ additionalInfo: {message: err.message}
105
+ }
106
+ });
107
+ } else endTerminator();
108
+ });
109
+ }
110
+ return res;
111
+ };
112
+ return previousMethod.call(this, req, res, next);
113
+ };
114
+
115
+ if(lock) {
116
+ dbClient.query('SELECT pg_advisory_xact_lock(' + lock + ')').then(() => finish()).catch((err: any) => {
117
+ res.status(500).json({
118
+ error: {
119
+ errorKey: 'internal.db',
120
+ additionalInfo: {message: err.message}
121
+ }
122
+ });
123
+ dbClient.release();
124
+ });
125
+ } else {
126
+ finish();
127
+ }
128
+ });
129
+ }).catch(err => {
130
+ // Error connecting to database or acquiring transaction, restarting worker after the timeout as well...
131
+ res.status(500).json({
132
+ error: {
133
+ errorKey: 'internal.db',
134
+ additionalInfo: {message: err.message}
135
+ }
136
+ });
137
+ });
138
+ };
139
+ }
package/services/utils.js CHANGED
@@ -140,22 +140,22 @@ function idempotent(repo, debug, logger) {
140
140
  }
141
141
  exports.idempotent = idempotent;
142
142
  function sendSelfPostableMessage(res, code, messageType, err) {
143
- res.type('text/html').status(code).write(`
144
- <!DOCTYPE HTML>
145
- <html>
146
- <head>
147
- <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
148
- </head>
149
- <body>
150
- <script type="text/javascript">
151
- window.parent.postMessage({
152
- type: '${messageType}',
153
- confirm: ${!err},
154
- error: JSON.parse('${JSON.stringify(err) || 'null'}')
155
- }, '*');
156
- </script>
157
- </body>
158
- </html>
143
+ res.type('text/html').status(code).write(`
144
+ <!DOCTYPE HTML>
145
+ <html>
146
+ <head>
147
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
148
+ </head>
149
+ <body>
150
+ <script type="text/javascript">
151
+ window.parent.postMessage({
152
+ type: '${messageType}',
153
+ confirm: ${!err},
154
+ error: JSON.parse('${JSON.stringify(err) || 'null'}')
155
+ }, '*');
156
+ </script>
157
+ </body>
158
+ </html>
159
159
  `);
160
160
  res.end();
161
161
  }