web_plsql 0.5.0 → 0.6.0

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.
Files changed (96) hide show
  1. package/README.md +123 -110
  2. package/examples/server_apex.js +28 -0
  3. package/examples/server_sample.js +30 -0
  4. package/examples/sql/{doc_table.sql → doctable.sql} +1 -1
  5. package/examples/sql/install.sql +6 -9
  6. package/examples/sql/sample_package.sql +27 -0
  7. package/examples/sql/sample_package_body.sql +245 -0
  8. package/examples/static/sample.css +11 -10
  9. package/package.json +90 -83
  10. package/src/cgi.js +127 -0
  11. package/src/error.js +24 -0
  12. package/src/errorPage.js +296 -0
  13. package/src/file.js +77 -0
  14. package/src/handlerLogger.js +21 -0
  15. package/src/handlerMetrics.js +46 -0
  16. package/src/handlerPlSql.js +65 -0
  17. package/src/handlerUpload.js +32 -0
  18. package/src/index.js +17 -0
  19. package/src/oracle.js +80 -0
  20. package/src/parsePage.js +193 -0
  21. package/src/procedure.js +260 -0
  22. package/src/procedureError.js +40 -0
  23. package/src/procedureNamed.js +233 -0
  24. package/src/procedureSanitize.js +215 -0
  25. package/src/procedureVariable.js +57 -0
  26. package/src/request.js +103 -0
  27. package/src/{requestError.ts → requestError.js} +8 -4
  28. package/src/sendResponse.js +104 -0
  29. package/src/server.js +106 -0
  30. package/src/shutdown.js +53 -0
  31. package/src/stream.js +28 -0
  32. package/src/trace.js +74 -0
  33. package/src/tty.js +48 -0
  34. package/src/types.js +146 -0
  35. package/src/upload.js +92 -0
  36. package/src/version.js +23 -0
  37. package/types/cgi.d.ts +4 -0
  38. package/types/error.d.ts +1 -0
  39. package/types/errorPage.d.ts +10 -0
  40. package/types/file.d.ts +5 -0
  41. package/types/handlerLogger.d.ts +2 -0
  42. package/types/handlerMetrics.d.ts +4 -0
  43. package/types/handlerPlSql.d.ts +8 -0
  44. package/types/handlerUpload.d.ts +7 -0
  45. package/types/index.d.ts +10 -0
  46. package/types/oracle.d.ts +5 -0
  47. package/types/parsePage.d.ts +3 -0
  48. package/types/procedure.d.ts +10 -0
  49. package/types/procedureError.d.ts +23 -0
  50. package/types/procedureNamed.d.ts +14 -0
  51. package/types/procedureSanitize.d.ts +14 -0
  52. package/types/procedureVariable.d.ts +9 -0
  53. package/types/request.d.ts +7 -0
  54. package/types/requestError.d.ts +8 -0
  55. package/types/sendResponse.d.ts +4 -0
  56. package/types/server.d.ts +7 -0
  57. package/types/shutdown.d.ts +2 -0
  58. package/types/stream.d.ts +1 -0
  59. package/types/trace.d.ts +5 -0
  60. package/types/tty.d.ts +4 -0
  61. package/types/types.d.ts +251 -0
  62. package/types/upload.d.ts +5 -0
  63. package/types/version.d.ts +7 -0
  64. package/.editorconfig +0 -8
  65. package/.eslintignore +0 -3
  66. package/.eslintrc.js +0 -347
  67. package/CHANGELOG.md +0 -127
  68. package/examples/apex.js +0 -70
  69. package/examples/credentials.js +0 -22
  70. package/examples/oracledb_example.js +0 -30
  71. package/examples/sample.js +0 -101
  72. package/examples/sql/sample.pkb +0 -223
  73. package/examples/sql/sample.pks +0 -24
  74. package/jest.config.js +0 -204
  75. package/src/cgi.ts +0 -95
  76. package/src/config.ts +0 -97
  77. package/src/errorPage.ts +0 -286
  78. package/src/fileUpload.ts +0 -126
  79. package/src/index.ts +0 -65
  80. package/src/page.ts +0 -275
  81. package/src/procedure.ts +0 -360
  82. package/src/procedureError.ts +0 -27
  83. package/src/request.ts +0 -139
  84. package/src/stream.ts +0 -26
  85. package/src/trace.ts +0 -194
  86. package/test/.eslintrc.json +0 -5
  87. package/test/__tests__/cgi.ts +0 -96
  88. package/test/__tests__/config.ts +0 -41
  89. package/test/__tests__/errorPage.ts +0 -101
  90. package/test/__tests__/oracledb_mock.ts +0 -98
  91. package/test/__tests__/server.ts +0 -495
  92. package/test/__tests__/stream.ts +0 -21
  93. package/test/mock/oracledb.ts +0 -85
  94. package/test/static/static.html +0 -1
  95. package/tsconfig.json +0 -24
  96. package/tsconfig.src.json +0 -23
@@ -0,0 +1,215 @@
1
+ import debugModule from 'debug';
2
+ const debug = debugModule('webplsql:procedureSanitize');
3
+
4
+ import oracledb from 'oracledb';
5
+ import z from 'zod';
6
+ import {RequestError} from './requestError.js';
7
+ import {errorToString} from './error.js';
8
+
9
+ /**
10
+ * @typedef {import('express').Request} Request
11
+ * @typedef {import('express').Response} Response
12
+ * @typedef {import('oracledb').Connection} Connection
13
+ * @typedef {import('oracledb').Result<unknown>} Result
14
+ * @typedef {import('./types.js').argObjType} argObjType
15
+ * @typedef {import('./types.js').fileUploadType} fileUploadType
16
+ * @typedef {import('./types.js').environmentType} environmentType
17
+ * @typedef {import('./types.js').configPlSqlHandlerType} configPlSqlHandlerType
18
+ * @typedef {import('./types.js').BindParameterConfig} BindParameterConfig
19
+ */
20
+
21
+ const DEFAULT_EXCLUSION_LIST = ['sys.', 'dbms_', 'utl_', 'owa_', 'htp.', 'htf.', 'wpg_docload.', 'ctxsys.', 'mdsys.'];
22
+
23
+ // NOTE: Consider using a separate cache for each database pool to avoid possible conflicts.
24
+ /**
25
+ * @typedef {{hitCount: number, valid: boolean}} cacheEntryType
26
+ */
27
+ /** @type {Map<string, cacheEntryType>} */
28
+ const REQUEST_VALIDATION_FUNCTION_CACHE = new Map();
29
+ const REQUEST_VALIDATION_FUNCTION_CACHE_MAX_COUNT = 10000;
30
+
31
+ /**
32
+ * Sanitize the procedure name.
33
+ *
34
+ * @param {string} procName - The procedure name.
35
+ * @param {Connection} databaseConnection - The database connection
36
+ * @param {configPlSqlHandlerType} options - the options for the middleware.
37
+ * @returns {Promise<string>} Promise resolving to final procedure name.
38
+ */
39
+ export const sanitizeProcName = async (procName, databaseConnection, options) => {
40
+ debug('sanitizeProcName', procName);
41
+
42
+ // make lowercase and trim
43
+ let finalProcName = procName.toLowerCase().trim();
44
+
45
+ // remove special characters
46
+ finalProcName = removeSpecialCharacters(finalProcName);
47
+
48
+ // check for default exclusions
49
+ for (const i of DEFAULT_EXCLUSION_LIST) {
50
+ if (finalProcName.startsWith(i)) {
51
+ const error = `Procedure name "${procName}" is in default exclusion list "${DEFAULT_EXCLUSION_LIST.join(',')}"`;
52
+ debug(error);
53
+ throw new RequestError(error);
54
+ }
55
+ }
56
+
57
+ // check for custom exclusions
58
+ if (options.exclusionList && options.exclusionList.length > 0) {
59
+ for (const i of options.exclusionList) {
60
+ if (finalProcName.startsWith(i)) {
61
+ const error = `Procedure name "${procName}" is in custom exclusion list "${options.exclusionList.join(',')}"`;
62
+ debug(error);
63
+ throw new RequestError(error);
64
+ }
65
+ }
66
+ }
67
+
68
+ // Check request validation function
69
+ if (options.requestValidationFunction && options.requestValidationFunction.length > 0) {
70
+ const valid = await requestValidationFunction(finalProcName, options.requestValidationFunction, databaseConnection);
71
+ if (!valid) {
72
+ const error = `Procedure name "${procName}" is not valid according to the request validation function "${options.requestValidationFunction}"`;
73
+ debug(error);
74
+ throw new RequestError(error);
75
+ }
76
+ }
77
+
78
+ return finalProcName;
79
+ };
80
+
81
+ /**
82
+ * @param {string} str - string
83
+ * @returns {string} - string
84
+ */
85
+ const removeSpecialCharacters = (str) => {
86
+ if (str === null || str === undefined) {
87
+ return '';
88
+ }
89
+
90
+ const chars = [];
91
+
92
+ for (const c of str) {
93
+ if ((c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c === '.' || c === '_' || c === '#' || c === '$') {
94
+ chars.push(c);
95
+ }
96
+ }
97
+
98
+ return chars.join('');
99
+ };
100
+
101
+ /**
102
+ * @param {string} procName - The procedure name.
103
+ * @param {string} requestValidationFunction - The request validation function.
104
+ * @param {Connection} databaseConnection - The database connection
105
+ * @returns {Promise<boolean>} Promise resolving to final procedure name.
106
+ */
107
+ const loadRequestValid = async (procName, requestValidationFunction, databaseConnection) => {
108
+ /** @type {BindParameterConfig} */
109
+ const bind = {
110
+ proc: {dir: oracledb.BIND_IN, type: oracledb.STRING, val: procName},
111
+ valid: {dir: oracledb.BIND_OUT, type: oracledb.NUMBER},
112
+ };
113
+
114
+ const SQL = [
115
+ 'DECLARE',
116
+ ' l_valid NUMBER := 0;',
117
+ 'BEGIN',
118
+ ` IF (${requestValidationFunction}(:proc)) THEN`,
119
+ ' l_valid := 1;',
120
+ ' END IF;',
121
+ ' :valid := l_valid;',
122
+ 'END;',
123
+ ].join('\n');
124
+
125
+ /** @type {Result} */
126
+ let result = {};
127
+ try {
128
+ result = await databaseConnection.execute(SQL, bind);
129
+ } catch (err) {
130
+ debug('result', result);
131
+ /* istanbul ignore next */
132
+ const message = `Error when validating procedure name "${procName}"\n${SQL}\n${errorToString(err)}`;
133
+ /* istanbul ignore next */
134
+ throw new RequestError(message);
135
+ }
136
+
137
+ try {
138
+ const data = z.object({valid: z.number()}).strict().parse(result.outBinds);
139
+ return data.valid === 1;
140
+ } catch (err) {
141
+ debug('result', result.outBinds);
142
+ /* istanbul ignore next */
143
+ const message = `Internal error when parsing ${result.outBinds}\n${errorToString(err)}`;
144
+ /* istanbul ignore next */
145
+ throw new Error(message);
146
+ }
147
+ };
148
+
149
+ /**
150
+ * Remove the cache entries with the lowest hitCount.
151
+ * @param {number} count - Number of entries to remove
152
+ * @returns {void}
153
+ */
154
+ const removeLowestHitCountEntries = (count) => {
155
+ // Convert cache entries to an array
156
+ const entries = Array.from(REQUEST_VALIDATION_FUNCTION_CACHE.entries());
157
+
158
+ // Sort entries by hitCount in ascending order
159
+ entries.sort((a, b) => a[1].hitCount - b[1].hitCount);
160
+
161
+ // Get the keys of the `count` entries with the lowest hitCount
162
+ const keysToRemove = entries.slice(0, count).map(([key]) => key);
163
+
164
+ // Remove these entries from the cache
165
+ for (const key of keysToRemove) {
166
+ REQUEST_VALIDATION_FUNCTION_CACHE.delete(key);
167
+ }
168
+ };
169
+
170
+ /**
171
+ * Request validation function.
172
+ *
173
+ * @param {string} procName - The procedure name.
174
+ * @param {string} requestValidationFunction - The request validation function.
175
+ * @param {Connection} databaseConnection - The database connection
176
+ * @returns {Promise<boolean>} Promise resolving to final procedure name.
177
+ */
178
+ const requestValidationFunction = async (procName, requestValidationFunction, databaseConnection) => {
179
+ debug('requestValidationFunction', procName, requestValidationFunction);
180
+
181
+ // calculate the key
182
+ //const key = `${databaseConnection.connectString}_${databaseConnection.user}_${procName.toLowerCase()}`;
183
+ const key = procName.toLowerCase();
184
+
185
+ // lookup in the cache
186
+ const cacheEntry = REQUEST_VALIDATION_FUNCTION_CACHE.get(key);
187
+
188
+ // if we fount the procedure in the cache, we increase the hit cound and return
189
+ if (cacheEntry) {
190
+ cacheEntry.hitCount++;
191
+ if (debug.enabled) {
192
+ debug(`findArguments: procedure "${procName}" found in cache with "${cacheEntry.hitCount}" hits`);
193
+ }
194
+ return cacheEntry.valid;
195
+ }
196
+
197
+ // if the cache is full, we remove the 1000 least used cache entries
198
+ if (REQUEST_VALIDATION_FUNCTION_CACHE.size > REQUEST_VALIDATION_FUNCTION_CACHE_MAX_COUNT) {
199
+ if (debug.enabled) {
200
+ debug(`findArguments: cache is full. size=${REQUEST_VALIDATION_FUNCTION_CACHE.size} max=${REQUEST_VALIDATION_FUNCTION_CACHE_MAX_COUNT}`);
201
+ }
202
+ removeLowestHitCountEntries(1000);
203
+ }
204
+
205
+ // load from database
206
+ if (debug.enabled) {
207
+ debug(`findArguments: procedure "${procName}" not found in cache and must be loaded`);
208
+ }
209
+ const valid = await loadRequestValid(procName, requestValidationFunction, databaseConnection);
210
+
211
+ // add to the cache
212
+ REQUEST_VALIDATION_FUNCTION_CACHE.set(key, {hitCount: 0, valid});
213
+
214
+ return valid;
215
+ };
@@ -0,0 +1,57 @@
1
+ /*
2
+ * Invoke the Oracle procedure and return the raw content of the page
3
+ */
4
+
5
+ import debugModule from 'debug';
6
+ const debug = debugModule('webplsql:procedureVariable');
7
+
8
+ import oracledb from 'oracledb';
9
+ import {sanitizeProcName} from './procedureSanitize.js';
10
+
11
+ /**
12
+ * @typedef {import('oracledb').Connection} Connection
13
+ * @typedef {import('oracledb').Result<unknown>} Result
14
+ * @typedef {import('./types.js').configPlSqlHandlerType} configPlSqlHandlerType
15
+ * @typedef {import('./types.js').argObjType} argObjType
16
+ * @typedef {import('./types.js').BindParameterConfig} BindParameterConfig
17
+ */
18
+
19
+ /**
20
+ * Get the sql statement and bindings for the procedure to execute for a variable number of arguments
21
+ * @param {string} procName - The procedure to execute
22
+ * @param {argObjType} argObj - The arguments to pass to the procedure
23
+ * @param {Connection} databaseConnection - The database connection
24
+ * @param {configPlSqlHandlerType} options - the options for the middleware.
25
+ * @returns {Promise<{sql: string; bind: BindParameterConfig}>} - The SQL statement and bindings for the procedure to execute
26
+ */
27
+ export const getProcedureVariable = async (procName, argObj, databaseConnection, options) => {
28
+ if (debug.enabled) {
29
+ debug(`getProcedureVariable: ${procName} arguments=`, argObj);
30
+ }
31
+
32
+ const names = [];
33
+ const values = [];
34
+
35
+ for (const key in argObj) {
36
+ const value = argObj[key];
37
+ if (typeof value === 'string') {
38
+ names.push(key);
39
+ values.push(value);
40
+ } else if (Array.isArray(value)) {
41
+ value.forEach((item) => {
42
+ names.push(key);
43
+ values.push(item);
44
+ });
45
+ }
46
+ }
47
+
48
+ const sanitizedProcName = await sanitizeProcName(procName, databaseConnection, options);
49
+
50
+ return {
51
+ sql: `${sanitizedProcName}(:argnames, :argvalues);`,
52
+ bind: {
53
+ argnames: {dir: oracledb.BIND_IN, type: oracledb.STRING, val: names},
54
+ argvalues: {dir: oracledb.BIND_IN, type: oracledb.STRING, val: values},
55
+ },
56
+ };
57
+ };
package/src/request.js ADDED
@@ -0,0 +1,103 @@
1
+ /*
2
+ * Process the http request
3
+ */
4
+
5
+ import debugModule from 'debug';
6
+ const debug = debugModule('webplsql:request');
7
+
8
+ import util from 'node:util';
9
+ import {invokeProcedure} from './procedure.js';
10
+ import {getCGI} from './cgi.js';
11
+ import {getFiles} from './upload.js';
12
+ import {RequestError} from './requestError.js';
13
+
14
+ /**
15
+ * @typedef {import('express').Request} Request
16
+ * @typedef {import('express').Response} Response
17
+ * @typedef {import('oracledb').Pool} Pool
18
+ * @typedef {import('oracledb').Connection} Connection
19
+ * @typedef {import('./types.js').argObjType} argObjType
20
+ * @typedef {import('./types.js').configPlSqlHandlerType} configPlSqlHandlerType
21
+ */
22
+
23
+ /**
24
+ * Execute the request
25
+ *
26
+ * @param {Request} req - The req object represents the HTTP request.
27
+ * @param {Response} res - The res object represents the HTTP response that an Express app sends when it gets an HTTP request.
28
+ * @param {configPlSqlHandlerType} options - the options for the middleware.
29
+ * @param {Pool} connectionPool - The connection pool.
30
+ * @returns {Promise<void>} - Promise resolving to th page
31
+ */
32
+ export const processRequest = async (req, res, options, connectionPool) => {
33
+ debug('executeRequest: ENTER');
34
+
35
+ // open database connection
36
+ const connection = await connectionPool.getConnection();
37
+
38
+ // Get the CGI
39
+ const cgiObj = getCGI(req, options.documentTable ?? '', options.cgi ?? {});
40
+ debug('executeRequest: cgiObj=', cgiObj);
41
+
42
+ // Does the request contain any files
43
+ const filesToUpload = getFiles(req);
44
+ debug('executeRequest: filesToUpload=', filesToUpload);
45
+
46
+ // Add the query properties
47
+ /** @type {argObjType} */
48
+ const argObj = {};
49
+ Object.assign(argObj, req.query);
50
+
51
+ // Add the files to the arguments
52
+ filesToUpload.reduce((aggregator, file) => {
53
+ aggregator[file.fieldname] = file.originalname;
54
+ return aggregator;
55
+ }, argObj);
56
+
57
+ // Does the request contain a body
58
+ Object.assign(argObj, normalizeBody(req));
59
+
60
+ // invoke the Oracle procedure and get the page contenst
61
+ await invokeProcedure(req, res, argObj, cgiObj, filesToUpload, options, connection);
62
+
63
+ // close database connection
64
+ await connection.release();
65
+
66
+ debug('executeRequest: EXIT');
67
+ };
68
+
69
+ /**
70
+ * Is the given value a string or an array of strings
71
+ * @param {unknown} value - The value to check.
72
+ * @returns {value is string | string[]} - True if the value is a string or an array of strings
73
+ */
74
+ const isStringOrArrayOfString = (value) => typeof value === 'string' || (Array.isArray(value) && value.every((element) => typeof element === 'string'));
75
+
76
+ /**
77
+ * Normalize the body by making sure that only "simple" parameters and no nested objects are submitted
78
+ * @param {Request} req - The req object represents the HTTP request.
79
+ * @returns {Record<string, string | string[]>} - The normalized body.
80
+ */
81
+ const normalizeBody = (req) => {
82
+ /** @type {Record<string, string | string[]>} */
83
+ const args = {};
84
+
85
+ /* istanbul ignore else */
86
+ if (typeof req.body === 'object' && req.body !== null) {
87
+ for (const key in req.body) {
88
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
89
+ const value = req.body[key];
90
+ /* istanbul ignore else */
91
+ if (isStringOrArrayOfString(value)) {
92
+ args[key] = value;
93
+ } else {
94
+ /* istanbul ignore next */
95
+ throw new RequestError(
96
+ `The element "${key}" in the body is not a string or an array of strings!\n${util.inspect(req.body, {showHidden: false, depth: null, colors: false})}`,
97
+ );
98
+ }
99
+ }
100
+ }
101
+
102
+ return args;
103
+ };
@@ -1,11 +1,15 @@
1
1
  /*
2
- * RequestError
3
- */
2
+ * RequestError
3
+ */
4
4
 
5
5
  export class RequestError extends Error {
6
- timestamp: Date;
6
+ /** @type {Date} */
7
+ timestamp;
7
8
 
8
- constructor(message: string) {
9
+ /**
10
+ * @param {string} message - The error message.
11
+ */
12
+ constructor(message) {
9
13
  super(message);
10
14
 
11
15
  // Maintains proper stack trace for where our error was thrown (only available on V8)
@@ -0,0 +1,104 @@
1
+ /*
2
+ * Page the raw page content and return the content to the client
3
+ */
4
+
5
+ import debugModule from 'debug';
6
+ const debug = debugModule('webplsql:sendResponse');
7
+
8
+ import {getBlock} from './trace.js';
9
+
10
+ /**
11
+ * @typedef {import('express').Request} Request
12
+ * @typedef {import('express').Response} Response
13
+ * @typedef {import('./types.js').pageType} pageType
14
+ */
15
+
16
+ /**
17
+ * Send "default" response to the browser
18
+ * @param {Request} req - The req object represents the HTTP request.
19
+ * @param {Response} res - The res object represents the HTTP response that an Express app sends when it gets an HTTP request.
20
+ * @param {pageType} page - The page to send.
21
+ * @returns {void}
22
+ */
23
+ export const sendResponse = (req, res, page) => {
24
+ /** @type {string[]} */
25
+ const debugText = [];
26
+
27
+ // Send the "cookies"
28
+ page.head.cookies.forEach((cookie) => {
29
+ const {name, value} = cookie;
30
+
31
+ if (debug.enabled) {
32
+ debugText.push(`res.cookie("${name}", "${value}")`);
33
+ }
34
+
35
+ res.cookie(name, value);
36
+ });
37
+
38
+ // If there is a "redirectLocation" header, we immediately redirect and return
39
+ if (typeof page.head.redirectLocation === 'string' && page.head.redirectLocation.length > 0) {
40
+ if (debug.enabled) {
41
+ debugText.push(`res.redirect(302, "${page.head.redirectLocation}")`);
42
+ debug(getBlock('RESPONSE', debugText.join('\n')));
43
+ }
44
+
45
+ res.redirect(302, page.head.redirectLocation);
46
+ return;
47
+ }
48
+
49
+ // Send all the "otherHeaders"
50
+ for (const key in page.head.otherHeaders) {
51
+ if (debug.enabled) {
52
+ debugText.push(`res.set("${key}", "${page.head.otherHeaders[key]}")`);
53
+ }
54
+
55
+ res.set(key, page.head.otherHeaders[key]);
56
+ }
57
+
58
+ // If this is a file download, we eventually set the "Content-Type" and the file content and then return.
59
+ if (page.file.fileType === 'B' || page.file.fileType === 'F') {
60
+ if (typeof page.head.contentType === 'string' && page.head.contentType.length > 0) {
61
+ if (debug.enabled) {
62
+ debugText.push(`res.writeHead("Content-Type", "${page.head.contentType}")`);
63
+ }
64
+
65
+ res.writeHead(200, {'Content-Type': page.head.contentType});
66
+ }
67
+
68
+ if (debug.enabled) {
69
+ debugText.push(`res.end("${page.file.fileType}")`);
70
+ debug(getBlock('RESPONSE', debugText.join('\n')));
71
+ }
72
+
73
+ res.end(page.file.fileBlob, 'binary');
74
+ return;
75
+ }
76
+
77
+ // Is the a "contentType" header
78
+ if (typeof page.head.contentType === 'string' && page.head.contentType.length > 0) {
79
+ if (debug.enabled) {
80
+ debugText.push(`res.set("Content-Type", "${page.head.contentType}")`);
81
+ }
82
+
83
+ res.set('Content-Type', page.head.contentType);
84
+ }
85
+
86
+ // If we have a "Status" header, we send the header and then return.
87
+ if (typeof page.head.statusCode === 'number') {
88
+ if (debug.enabled) {
89
+ debugText.push(`res.status(page.head.statusCode).send("${page.head.statusDescription}")`);
90
+ debug(getBlock('RESPONSE', debugText.join('\n')));
91
+ }
92
+
93
+ res.status(page.head.statusCode).send(page.head.statusDescription);
94
+ return;
95
+ }
96
+
97
+ // Send the body
98
+ if (debug.enabled) {
99
+ debugText.push(`${'-'.repeat(60)}\n${page.body}`);
100
+ debug(getBlock('RESPONSE', debugText.join('\n')));
101
+ }
102
+
103
+ res.send(page.body);
104
+ };
package/src/server.js ADDED
@@ -0,0 +1,106 @@
1
+ import debugModule from 'debug';
2
+ const debug = debugModule('webplsql:server');
3
+
4
+ import express from 'express';
5
+ import bodyParser from 'body-parser';
6
+ import cookieParser from 'cookie-parser';
7
+ import compression from 'compression';
8
+ import {z$configType} from './types.js';
9
+ import {installShutdown} from './shutdown.js';
10
+ import {writeAfterEraseLine} from './tty.js';
11
+ import {poolCreate, poolsClose} from '../src/oracle.js';
12
+ import {handlerUpload} from './handlerUpload.js';
13
+ import {handlerLogger} from './handlerLogger.js';
14
+ import {initMetrics, handlerMetrics} from './handlerMetrics.js';
15
+ import {handlerWebPlSql} from './handlerPlSql.js';
16
+ import {getPackageVersion} from './version.js';
17
+
18
+ /**
19
+ * @typedef {import('express').Request} Request
20
+ * @typedef {import('express').Response} Response
21
+ * @typedef {import('express').NextFunction} NextFunction
22
+ * @typedef {import('oracledb').Pool} Pool
23
+ * @typedef {import('./types.js').environmentType} environmentType
24
+ * @typedef {import('./types.js').configType} configType
25
+ */
26
+
27
+ /**
28
+ * Start generic server.
29
+ * @param {configType} config - The config.
30
+ * @returns {Promise<void>} - Promise.
31
+ */
32
+ export const startServer = async (config) => {
33
+ debug('startServer', config);
34
+
35
+ config = z$configType.parse(config);
36
+
37
+ console.log(`WEB_PL/SQL ${getPackageVersion()}`);
38
+
39
+ /** @type {Pool[]} */
40
+ const pools = [];
41
+
42
+ // Install shutdown handler
43
+ installShutdown(async () => {
44
+ // Close database pools.
45
+ await poolsClose(pools);
46
+ pools.length = 0;
47
+
48
+ // Close server
49
+ return new Promise((resolve) => server.close(() => resolve()));
50
+ });
51
+
52
+ // Create express app
53
+ const app = express();
54
+
55
+ // Metrics
56
+ const metrics = initMetrics();
57
+ app.use(handlerMetrics(metrics));
58
+
59
+ // Serving static files
60
+ for (const i of config.routeStatic) {
61
+ app.use(i.route, express.static(i.directoryPath));
62
+ console.log(`Static resources on "${i.route}" from directory ${i.directoryPath}`);
63
+ }
64
+
65
+ // Default middleware
66
+ app.use(handlerUpload());
67
+ app.use(bodyParser.json());
68
+ app.use(bodyParser.urlencoded({extended: true}));
69
+ app.use(cookieParser());
70
+ app.use(compression());
71
+
72
+ // Access log
73
+ if (config.loggerFilename.length > 0) {
74
+ console.log(`Access log in "${config.loggerFilename}"`);
75
+ app.use(handlerLogger(config.loggerFilename));
76
+ }
77
+
78
+ // Oracle pl/sql express middleware
79
+ for (const i of config.routePlSql) {
80
+ // Allocate the Oracle database pool
81
+ const pool = await poolCreate(i.user, i.password, i.connectString);
82
+ pools.push(pool);
83
+
84
+ app.use(`${i.route}/:name?`, handlerWebPlSql(pool, i));
85
+
86
+ console.log(`Application route "http://localhost:${config.port}${i.route}"`);
87
+ console.log(` Oracle user: ${i.user}`);
88
+ console.log(` Oracle server: ${i.connectString}`);
89
+ console.log(` Oracle document table: ${i.documentTable}`);
90
+ }
91
+
92
+ // Update metrics every second
93
+ if (config.monitorConsole) {
94
+ setInterval(() => {
95
+ // Update requests per second
96
+ const requestsLastInterval = metrics.requestsInLastInterval;
97
+ metrics.requestsInLastInterval = 0;
98
+
99
+ // Clear console and display metrics
100
+ writeAfterEraseLine(`Total requests: ${metrics.totalRequests}, requests per second: ${requestsLastInterval}`);
101
+ }, 1000);
102
+ }
103
+
104
+ // listen on port
105
+ const server = app.listen(config.port);
106
+ };
@@ -0,0 +1,53 @@
1
+ import debugModule from 'debug';
2
+ const debug = debugModule('webplsql:shutdown');
3
+
4
+ /**
5
+ * @param {() => Promise<void>} handler - Shutdown handler
6
+ * @returns {void}
7
+ */
8
+ const shutdown = (handler) => {
9
+ handler().then(
10
+ () => process.exit(0),
11
+ () => process.exit(1),
12
+ );
13
+
14
+ process.exit(0);
15
+ };
16
+
17
+ /**
18
+ * Install a shutdown handler.
19
+ * @param {() => Promise<void>} handler - Shutdown handler
20
+ * @returns {void}
21
+ */
22
+ export const installShutdown = (handler) => {
23
+ debug('installShutdown');
24
+
25
+ /*
26
+ * The 'unhandledRejection' event is emitted whenever a Promise is rejected and no error handler is attached to the promise within a turn of the event loop.
27
+ */
28
+ process.on('unhandledRejection', (reason, p) => {
29
+ console.log('\nUnhandled promise rejection. Graceful shutdown...', reason, p);
30
+ shutdown(handler);
31
+ });
32
+
33
+ // install signal event handler
34
+ process.on('SIGTERM', function sigterm() {
35
+ console.log('\nGot SIGINT (aka ctrl-c in docker). Graceful shutdown...');
36
+ shutdown(handler);
37
+ });
38
+
39
+ process.on('SIGINT', function sigint() {
40
+ console.log('\nGot SIGTERM (aka docker container stop). Graceful shutdown...');
41
+ shutdown(handler);
42
+ });
43
+ };
44
+
45
+ /**
46
+ * Force a shutdown.
47
+ * @returns {void}
48
+ */
49
+ export const forceShutdown = () => {
50
+ debug('forceShutdown');
51
+
52
+ process.kill(process.pid, 'SIGTERM');
53
+ };
package/src/stream.js ADDED
@@ -0,0 +1,28 @@
1
+ import stream from 'node:stream';
2
+
3
+ /**
4
+ * Convert a readable stream to a buffer.
5
+ *
6
+ * @param {stream.Readable} readable - The readable stream.
7
+ * @returns {Promise<Buffer>} The buffer.
8
+ */
9
+ export const streamToBuffer = async (readable) => {
10
+ return new Promise((resolve, reject) => {
11
+ /** @type {Buffer[]} */
12
+ const buffers = [];
13
+
14
+ readable.on('data', (buffer) => {
15
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
16
+ buffers.push(buffer);
17
+ });
18
+
19
+ readable.on('end', () => {
20
+ resolve(Buffer.concat(buffers));
21
+ });
22
+
23
+ readable.on('error', (err) => {
24
+ // istanbul ignore next
25
+ reject(err);
26
+ });
27
+ });
28
+ };