@akc42/server-utils 3.4.1 → 4.0.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.
package/README.md CHANGED
@@ -1,50 +1,109 @@
1
1
  # server-utils
2
2
  A Set of Utilities that I generally use on SPA projects for the server side of the project
3
3
 
4
- It consists of 4 separate packages 6 entry points.
4
+ It consists of 4 separate packages 8 entry points.
5
5
 
6
6
  The packages are:-
7
7
 
8
- `logger` provides a logging service for the app. It is controlled by three environment variables LOG_NONE prevents it from logging anything.
9
- This is designed to be used during testing of the server side of the app so that nothing is logged. LOG_NO_DATE omits the date and time from
10
- the logged output. This is generally used when another logger (e.g PM2 log output) is also adding date/time. Finally LOG_HIDDEN_IP is used
11
- to say to try and anonomise client ip addresses (see below). `logger` is called so `logger([clientip,] level, ...messages);`.
8
+ ## The Debug Suite
12
9
 
13
- `Responder` is a class to provide the ability to stream JSON responses to a node js http request. It is instanciated
14
- with `new Responder(response);` and the resultant object has three methods;
10
+ This package is a complete debugging solution. The basic concept is that messages get written to a database to be
11
+ available for examination in the future. Messages can be immediately written to the console (that is called `logging`),
12
+ Also if the message was a "crash", the previous `<n>` (where `<n>` is determined from the `DEBUG_CACHE_SIZE` environment
13
+ if it exists, else 100) is re-read from the database and also written to the console.
15
14
 
16
- - `addSection(name [,value])` creates a new section in the response of the given name, with an optional value (which should
17
- be the entirety of a section).
18
- - `write` allows you add an array row to an existing open section (one where `addSection` is called without a value). It will return a
19
- promise which resolves when any blockage is cleared.
20
- - `end` signifies the end of stream. Any attempt to call the other two methods after this has been called will throw an error.
21
-
22
- `Version` provides an async function with a single parameter, the path to you
23
- project root) that ultimately resolves to an object which has two
24
- fields. `version` which is the version string and `year` which is the copyright
25
- year. The project root is where either the `.git` directory exists (in which case
26
- `version` will ask git for the version and calculate the copyright year from the
27
- last git log entry) or where a `release.info` file is sitting (in which case
28
- `version` will expect that to contain a version string and have a modification
29
- time from which the copyright year can be derived). If neither of those
30
- possibilities exist it will try to get the version info from the `package.json` file.
31
-
32
- `Debug` module provides three entry points, `Debug`, `dumpDebugCache` and
33
- `setDebugConfig`. The `Debug` entry point is the main one, the user calls this a
34
- string representing the topic for the debug stream and we return a function that
35
- will allow him to call with string arguments which will be concatenated (with a
36
- space separator) to form a debug string. If `setDebugConfig` has already been
37
- called to specify that the topic is to be logged (by providing a colon
38
- separated list of topics to be logged), then this is output. Regardless, all
39
- debug calls are stored in a 50 line cache, and will be output (newest first) on a call
40
- to `dumpDebugCache`
41
-
42
- Breaking change as of 3.0.0 logger is now an async function returning a promise fulfilled when (if set) a log file entry is made
43
-
44
- both `Debug` and `logger` have had their file logging removed as its causing more issues that its worth
45
-
46
-
47
- These are installed with as many of few of the items that you want like so:-
48
- ```
49
- import {logger,Responder,Debug} from '@akc42/server-utils';
15
+ The **COLOURS** constant is an object contains a set of predefined colours (using the `npm chalk` package) for its properties of `app`,`db`,`api`,`client`, `log`, `mail`, `auth` and `error`. A *colourspec* is defined as one of those predefined colours or a hex string (hex digits preceeded by a `#`) or an rgb value (a string of three comma separated numbers between 0 and 255) and that will be used to colour the message itself when (and if) is is written to the console.
16
+
17
+ If the *shortdate* is defined (defaults to false) then the message, when eventually written to the console, is formatted as "YYYY-MM-DD hh:mm" otherwise it is
18
+ formatted as "YYYY-MM-DD hh:mm:ss.sss" (ie to millisecond accuracy).
19
+
20
+ The *immediate* parameter says to immediately, after logging to the database, to format the message and output it.
21
+
22
+ When called the **Debug** function returns a function which is the actual *logger*. This function can then be called with any number of parameters. The first three, if present, are checked to match the requirement, but if not are assumed not to be present. These are
23
+
24
+ - *crash* the literal string "crash" - see above for its meaning. In this case the *colourspec* is ignored and the
25
+ message is printed in white on a red background.
26
+ - *logtime* A unix timestamp with millisecond accuracy (e.g the result from `Date.now()`), Its only considered valid if
27
+ it is for today, although it can be earlier than the current time.
28
+ - *ipaddress* An ipv4 address as a string.
29
+ - *...messages* Any number of parameters following which are joined together with a space.
30
+
31
+ Formally **Debug** is called like this:-
32
+
33
+ ```javascript
34
+ const debug = Debug(topic,colourspec, shortdate, immediate);
35
+
36
+ debug([crash,][,logtime][ipaddress,]...messages);
50
37
  ```
38
+
39
+ This `debug` instance also remembers the time between calls and this time is logged (and subsequently printed) as a "gap". This is printed in milliseconds unless it was a "shortdate" in which case it is printed in minutes.
40
+
41
+ **Logger** is a function that is a wrapper for *Debug* where `shortdate` and `immediate` are both true.
42
+
43
+ **messageFormatter** is the routine that formats the raw message that has been written to the database.
44
+
45
+ It is called with the following parameters in order:-
46
+
47
+ - *logid* The `logid` (the primary key) of the message in the database (only used in the message output if the item was
48
+ a crash).
49
+ - *logtime* This is either a unix timestamp *or* a string with the date and/or time in it. It should be in the same
50
+ format as being formatted (see above).
51
+ - *crash* a 0 or 1 dependant on if this message was a crash or not.
52
+ - *shortdate* a 0 or 1 dependant on if this message has a short date or not,
53
+ - *ipaddress* should be a valid ip address or `null`.
54
+ - *topic*
55
+ - *message* Just a single string
56
+ - *colourspec*
57
+ - *gap* Gap in milliseconds (this routine does the conversion to minutes if a `shortdate`).
58
+
59
+ It returns an Object with 4 properties
60
+
61
+ - *dayoutput* If the first message of the day, text with the date (only) in it, otherwise a zero length string.
62
+ - *message* The complete formatted message
63
+ - *logid* The `logid` the formatter was called with.
64
+ - *ip* The `ipaddress` the formatter was called with.
65
+
66
+ **DebugHelper** is a helper function for *Debug* and performs most of its work. It is called with the same parameters as *Debug* plus an additional one; *writer*. *writer* should be a callback function that can do something with the message and then return the return object that a debug call does. In the use by *Debug* this function is the one writes the data to the database, but other writers can be provided. For instance the *Debug* function is the `@akc42/app-utils` package uses the writer to send the message from the client to the server.
67
+
68
+ *writer* is called with the following parameters (all described above for *debug*, although in this case they *must* be supplied)
69
+
70
+ - *logtime* (unix timestamp)
71
+ - *crash* (0 or 1)
72
+ - *shortdate* (0 or 1)
73
+ - *ipaddress* (or `null`)
74
+ - *topic*
75
+ - *colourspec*
76
+ - *gap*
77
+ - *immediate* (`true` or `false`)
78
+
79
+ ## Responder
80
+
81
+ **Responder** is a class to provide the ability to stream JSON responses to a node js http request. It is instanciated
82
+ with `new Responder(response);` and the resultant object has three methods;
83
+
84
+ - *addSection* called like
85
+
86
+ ```javascript
87
+ addSection(name [,value])
88
+ ```
89
+ which creates a new *Object property* in the response of the given name, with an optional value (which should be the entirety of a section).
90
+
91
+ - *write* allows you add an array row to an existing open section (one where *addSection* is called without a value). It
92
+ will return a promise which resolves when any blockage is cleared. It is recommended to use this when an array of
93
+ database rows will return more than a very limited number.
94
+ - *end* when signifies the end of stream. Any attempt to call the other two methods after this has been called will throw an error.
95
+
96
+ ## Version
97
+
98
+ **getVersion** is an async function with a single parameter, the path to your project root.
99
+
100
+ It ultimately resolves to an object which has two fields. `version` which is the version string and `year` which is the
101
+ copyright year. The project root is where either the `.git` directory exists (in which case `version` will ask git for
102
+ the version and calculate the copyright year from the last git log entry) or where a `release.info` file is sitting (in
103
+ which case `version` will expect that to contain a version string and have a modification time from which the copyright
104
+ year can be derived). If neither of those possibilities exist it will try to get the version info from the
105
+ `package.json` file.
106
+
107
+ ## Utils
108
+
109
+ **nullif0len** is the only function currently in this package. It is a function that takes a single parameter. If that parameters is either undefined or a string that has zero length it returns null. Otherwise it returns what was input.
@@ -0,0 +1,89 @@
1
+ /**
2
+ @licence
3
+ Copyright (c) 2025 Alan Chandler, all rights reserved
4
+
5
+ This file is part of PASv5, an implementation of the Patient Administration
6
+ System used to support Accuvision's Laser Eye Clinics.
7
+
8
+ PASv5 is licenced to Accuvision (and its successors in interest) free of royality payments
9
+ and in perpetuity in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the
10
+ implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Accuvision
11
+ may modify, or employ an outside party to modify, any of the software provided that
12
+ this modified software is only used as part of Accuvision's internal business processes.
13
+
14
+ The software may be run on either Accuvision's own computers or on external computing
15
+ facilities provided by a third party, provided that the software remains soley for use
16
+ by Accuvision (or by potential or existing customers in interacting with Accuvision).
17
+ */
18
+ export function hexToBase32(input) {
19
+ let output = '';
20
+ let next = 0;
21
+ let lshift = 0;
22
+ for(let i = 0; i< input.length; i++) {
23
+ let digit = parseInt(input.charAt(i), 16);
24
+ if (!Number.isInteger(digit)) throw new RangeError('Expected string to include only HEX digits');
25
+ if (lshift > 0) {
26
+ const rshift = 4 - lshift;
27
+ const remainder = digit & (2**rshift - 1);
28
+ next |= digit >>> rshift;
29
+ output += alphabet.charAt(next);
30
+ lshift = (lshift + 1) % 5;
31
+ next = remainder << lshift;
32
+ } else {
33
+ lshift++;
34
+ next = digit << lshift;
35
+ }
36
+ }
37
+ if (lshift > 0) output += alphabet.charAt(next);
38
+ return output;
39
+ }
40
+
41
+ /*
42
+
43
+ The code below was obtained from elsewhere with the attached notice
44
+
45
+ MIT License
46
+
47
+ Copyright (c) 2016-2021 Linus Unnebäck
48
+
49
+ Permission is hereby granted, free of charge, to any person obtaining a copy
50
+ of this software and associated documentation files (the "Software"), to deal
51
+ in the Software without restriction, including without limitation the rights
52
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
53
+ copies of the Software, and to permit persons to whom the Software is
54
+ furnished to do so, subject to the following conditions:
55
+
56
+ The above copyright notice and this permission notice shall be included in all
57
+ copies or substantial portions of the Software.
58
+
59
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
60
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
61
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
62
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
63
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
64
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
65
+ SOFTWARE.
66
+ */
67
+
68
+
69
+ const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'
70
+
71
+ export function base32ToHex (input) {
72
+ input = input.replace(/=+$/, '')
73
+ const length = input.length
74
+ let bits = 0;
75
+ let value = 0;
76
+ let index = 0;
77
+ const output = new Uint8Array(Math.ceil(length * 5 / 8) | 0)
78
+ for (var i = 0; i < length; i++) {
79
+ value = (value << 5) | alphabet.indexOf(input[i].toUpperCase())
80
+ bits += 5
81
+ if (bits >= 8) {
82
+ output[index++] = (value >>> (bits - 8)) & 255
83
+ bits -= 8
84
+ }
85
+ }
86
+ return Buffer.from(output).toString('hex');
87
+
88
+ }
89
+
@@ -0,0 +1,60 @@
1
+ /**
2
+ @licence
3
+ Copyright (c) 2025 Alan Chandler, all rights reserved
4
+
5
+ This file is part of PASv5, an implementation of the Patient Administration
6
+ System used to support Accuvision's Laser Eye Clinics.
7
+
8
+ PASv5 is licenced to Accuvision (and its successors in interest) free of royality payments
9
+ and in perpetuity in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the
10
+ implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Accuvision
11
+ may modify, or employ an outside party to modify, any of the software provided that
12
+ this modified software is only used as part of Accuvision's internal business processes.
13
+
14
+ The software may be run on either Accuvision's own computers or on external computing
15
+ facilities provided by a third party, provided that the software remains soley for use
16
+ by Accuvision (or by potential or existing customers in interacting with Accuvision).
17
+ */
18
+
19
+ export function DebugHelper(topic, colourspec, shortdate, immediate, writer) {
20
+ const t = topic;
21
+ const cs = (colourspec in COLOURS.COLOURS) || COLOURS.hexmatch.test(colourspec) || COLOURS.rgbmatch.test(colourspec) ? colourspec : null;
22
+ const sd = shortdate? 1:0;
23
+ let timestamp = Date.now();
24
+ const i = immediate;
25
+ return function (c, logtime ,ip, ...args) {
26
+ let crash = 1;
27
+ if (c !== 'crash') {
28
+ if (ip !== undefined) {
29
+ if (Array.isArray(ip)) args = ip.concat(args); else args.unshift(ip);
30
+ }
31
+ ip = logtime;
32
+ logtime = c;
33
+ crash = 0;
34
+ }
35
+
36
+ const fromDate = new Date(); //logtime is only possible if later than midnight last night.
37
+ const from = fromDate.setHours(0,0,0,0)
38
+ if (!(Number.isInteger(logtime) && logtime > from)) {
39
+ if (ip !== undefined) {
40
+ if (Array.isArray(ip)) args = ip.concat(args); else args.unshift(ip);
41
+ }
42
+ ip = logtime;
43
+ logtime = Date.now();
44
+ }
45
+ if (!ipmatch.test(ip)) {
46
+ if (ip !== undefined){
47
+ if (Array.isArray(ip)) args = ip.concat(args); else args.unshift(ip);
48
+ }
49
+ ip = null
50
+ }
51
+ const now = Date.now();
52
+ const gap = now - timestamp;
53
+ timestamp = now;
54
+ const message = args.reduce((cum, arg) => {
55
+ if (arg === undefined) return cum;
56
+ return `${cum} ${arg}`.trim();
57
+ },'');
58
+ return writer(logtime, crash, sd, ip, t, message, cs, gap,i)
59
+ }
60
+ };
package/debug.js CHANGED
@@ -1,85 +1,264 @@
1
1
  /**
2
- @licence
3
- Copyright (c) 2021 Alan Chandler, all rights reserved
2
+ @licence
3
+ Copyright (c) 2025 Alan Chandler, all rights reserved
4
4
 
5
- This file is part of Server Utils.
5
+ This file is part of PASv5, an implementation of the Patient Administration
6
+ System used to support Accuvision's Laser Eye Clinics.
6
7
 
7
- Server Utils is free software: you can redistribute it and/or modify
8
- it under the terms of the GNU General Public License as published by
9
- the Free Software Foundation, either version 3 of the License, or
10
- (at your option) any later version.
8
+ PASv5 is licenced to Accuvision (and its successors in interest) free of royality payments
9
+ and in perpetuity in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the
10
+ implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Accuvision
11
+ may modify, or employ an outside party to modify, any of the software provided that
12
+ this modified software is only used as part of Accuvision's internal business processes.
11
13
 
12
- Server Utils is distributed in the hope that it will be useful,
13
- but WITHOUT ANY WARRANTY; without even the implied warranty of
14
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
- GNU General Public License for more details.
14
+ The software may be run on either Accuvision's own computers or on external computing
15
+ facilities provided by a third party, provided that the software remains soley for use
16
+ by Accuvision (or by potential or existing customers in interacting with Accuvision).
17
+ */
18
+
19
+ import { EventEmitter } from 'node:events';
20
+ import { setTimeout } from 'node:timers/promises';
21
+ import chalk from "chalk";
22
+ import {openDatabase} from '@akc42/sqlite-db';
23
+ import { DebugHelper, messageFormatter, COLOURS } from './debug-utils.js';
16
24
 
17
- You should have received a copy of the GNU General Public License
18
- along with Server Utils. If not, see <http://www.gnu.org/licenses/>.
25
+ class DebugLogEvents extends EventEmitter {}
26
+ /*
27
+ The following events are emitted by the debug log
28
+
29
+ 'log-write': (<formatted message>, <day boundary>, <logid>, <crash>, <ipaddress>, <topic>, <colourspec> )
30
+ <logid> is current message unless <day boundary> is true, in which case it is the logid of the last message of the
31
+ previous day, or 0 if its the first message since startup. If <day-boundary> is true the remaining parameters will be
32
+ null,
33
+ <crash> a true of false setting which says if it was crash or not,
34
+ <ipaddress> if not null is the ip address of the client that caused the message (as a string)
35
+ <topic> the topic of the message.
36
+ <colourspec> One of name of standard colors [app,db,api,client,log,mail,auth,error] else ''
37
+
38
+ 'log-raw' (<log object>)
39
+ <log-object> contains the following fields
40
+ logid, This is the same logid as <logid> above, except the <day-boundary> message is not emitted in the raw feed
41
+ logtime, The number of milliseconds since Unix Epoch, or as a datetime string with optional milliseconds
42
+ logmin, The number of millisecons since Unix Epoch of logtime rounded to the minute boundary before it.
43
+ crash, If 1, this entry is a crash report, otherwise its a normal message
44
+ shortdate, If 1. the request is only to log to the nearest minute, rather than in milliseconds
45
+ ipaddress, If not null, is the ip address of the client that caused the message (as a string)
46
+ topic, The topic of this message
47
+ message, The text of the message is self
48
+ colourspec, One of name of standard colors [app,db,api,client,log,mail,auth,error], a hex color string, an rgb,
49
+ comma seperated, string of three values 0-255
50
+ gap gap in milliseconds since the last message of the same topic.
19
51
  */
52
+ export const DebugLog = new DebugLogEvents();
53
+
54
+ const db = await openDatabase(`${process.env.SQLITE_DB_NAME}-log`)
55
+ db.exec(`
56
+ CREATE TABLE IF NOT EXISTS Log (
57
+ logid INTEGER PRIMARY KEY,
58
+ logtime DATETIME NOT NULL DEFAULT (datetime('now','subsec')),
59
+ logmin INTEGER AS (CAST(round((unixepoch(logtime)/60) - 0.5) AS INT)) STORED,
60
+ crash BOOLEAN NOT NULL DEFAULT 0,
61
+ shortdate BOOLEAN NOT NULL DEFAULT 0,
62
+ ipaddress TEXT,
63
+ topic TEXT,
64
+ message TEXT,
65
+ colourspec TEXT,
66
+ gap INTEGER
67
+ );
68
+ CREATE INDEX IF NOT EXISTS IX_Log_topic ON Log(topic);
69
+ CREATE INDEX IF NOT EXISTS IX_Log_logmin ON Log(logmin);
70
+ CREATE INDEX IF NOT EXISTS IX_Log_crash ON Log(crash);
71
+ PRAGMA journal_mode=WAL;
72
+ `);
20
73
 
21
- import chalk from 'chalk';
22
- let config = process.env.DEBUG || '';
23
- let cache = [];
24
- let cachelimit = 50;
25
-
26
- export function Debug (topic) {
27
- const t = topic;
28
- let timestamp = new Date().getTime();
29
- return function (...args) {
30
- let enabled = false;
31
- if (config) {
32
- const topics = config.split(':');
33
- if (topics.includes(t)) enabled = true;
74
+
75
+ export async function awaitTransaction() {
76
+ return new Promise((resolve) => {
77
+ if (db.inTransaction) {
78
+ if (timerAbort.signal.aborted) {
79
+ setTimeout(200,'awaittransaction').then(resolve);
80
+ } else {
81
+ timerAbort.signal.once('abort',resolve);
82
+ timerAbort.abort();
34
83
  }
35
- const now = new Date().getTime();
36
- const gap = now - timestamp;
37
- timestamp = now;
38
- const message = args.reduce((cum, arg) => {
39
- return `${cum} ${arg}`.trim();
40
- }, '');
41
- const output = `${chalk.greenBright(topic)} ${chalk.cyan(message)} ${chalk.whiteBright(`(${gap}ms)`)}`
42
- if (enabled) {
43
- let logLine = '';
44
- if (typeof process.env.LOG_NO_DATE === 'undefined') {
45
- if (typeof process.env.LOG_ISO_DATE !== 'undefined') {
46
- logLine += new Date().toISOString() + ': ';
47
- } else {
48
- logLine += new Date().toISOString().substring(0,10) + ' ' + new Date().toLocaleTimeString() + ': ';
49
- }
50
- }
51
- logLine += output;
52
- //eslint-disable-next-line no-console
53
- console.log(logLine);
84
+ } else {
85
+ resolve();
86
+ }
87
+ });
88
+ }
89
+
90
+ let timerAbort = new AbortController();
91
+
92
+ const insertLogTime = db.prepare(`INSERT INTO Log (logtime,crash,shortdate,ipaddress, topic,message,colourspec,gap) VALUES
93
+ (datetime(?,'unixepoch','subsec'),?,?,?,?,?,?,?)`);
94
+
95
+ const insertLogNoTime = db.prepare(`INSERT INTO LOG(crash,shortdate,ipaddress, topic,message,colourspec,gap) VALUES (?,?,?,?,?,?,?)`);
96
+
97
+ const getLogTime = db.prepare('SELECT logtime FROM Log WHERE logid = ?');
98
+
99
+ /*
100
+ Writes the output to the log, pretty much assumed to be raw
101
+ logtime is in milliseconds since epoch (ie what date gives from Date.getTime())
102
+ */
103
+ let shuttingDown = false;
104
+
105
+ export function logWriter(logtime, crash, shortdate,ipaddress, topic, message, colourspec,gap) {
106
+ if (shuttingDown) return messageFormatter(0,logtime, crash, shortdate,ipaddress, topic, message, colourspec,gap);
107
+ if (!db.isOpen) db.open();
108
+ if (!db.inTransaction) {
109
+ db.exec('BEGIN TRANSACTION;');
110
+ timerAbort = new AbortController();
111
+ setTimeout(1000,'logwriter',timerAbort.signal).then(() => {
112
+ if (db.inTransaction) {
113
+ db.exec('COMMIT;');
54
114
  }
55
- const time = chalk.whiteBright(new Date().toISOtring().substring(11,23)); //store millisecond timing
56
- cache.push(`${time} ${output}`);
57
- if (cache.length > cachelimit) cache.splice(0,cache.length - cachelimit); //prevent it getting too big
115
+ })
58
116
  }
59
- };
60
- export function dumpDebugCache() {
61
- const time = chalk.whiteBright(new Date().toISOString());
62
- const output = chalk.white.bgBlue('Above are all the debug calls (most recent first) which lead up to, and then followed on from, the error above');
63
- cache.reverse();
64
- for(const line of cache) {
65
- //eslint-disable-next-line no-console
66
- console.log(line);
117
+
118
+ let logid;
119
+
120
+ if (logtime) {
121
+ const {lastInsertRowid} = insertLogTime.run((logtime/1000), crash, shortdate,ipaddress, topic, message, colourspec,gap);
122
+ logid = lastInsertRowid;
123
+
124
+ } else {
125
+ const {lastInsertRowid} = insertLogNoTime.run(crash, shortdate,ipaddress, topic, message, colourspec,gap);
126
+ logid = lastInsertRowid;
127
+ const logtimereq = getLogTime.get(logid);
128
+ logtime = logtimereq.logtime;
67
129
  }
68
- cache.reverse();
69
- //eslint-disable-next-line no-console
70
- console.log(`${time} - ${output}`);
130
+ if (crash === 1) timerAbort.abort(); //force the transaction to close;
131
+
132
+ const output = messageFormatter(logid,logtime, crash, shortdate,ipaddress, topic, message, colourspec,gap);
133
+ if (output.dayoutput.length > 0) {
134
+ DebugLog.emit('log-day', output.dayoutput);
135
+ }
136
+ delete output.dayoutput;
137
+ DebugLog.emit(`log-write`,output.message, logid, (crash === 1), ipaddress, topic, (colourspec in COLOURS.COLOURS)? colourspec: '');
138
+ return output;
139
+
71
140
  };
72
- export function setDebugConfig(con, limit = 50) {
73
- cachelimit = limit;
74
- if (con !== config) {
75
- config = con;
76
- const output = `${chalk.greenBright('debug server config')} ${chalk.redBright(`new server config "${config}"`)}`
77
- //eslint-disable-next-line no-console
78
- console.log(output);
141
+
142
+ function logWrapper(logtime, crash, shortdate,ipaddress, topic, message, colourspec,gap, i) {
143
+ const output = logWriter(logtime, crash, shortdate, ipaddress, topic, message, colourspec, gap);
144
+ if (i) console.log(output.message);
145
+ return output;
146
+ }
147
+
148
+ /*
149
+ getDebugLog
150
+
151
+ will read the debug log that occured just before the provided logid, and for each row returned will call the callback
152
+ function. The callback function may be asynchronous. the "no" parameter is how many to fetch. The logger uses the
153
+ DEBUG_CACHE_SIZE environment variable to specify this.
154
+ */
155
+
156
+ async function getDebugLog(callback, loid, no, ip) {
157
+ const lid = loid;
158
+ const limit = no
159
+ const ipadd = ip;
160
+ db.open();
161
+ try {
162
+ await awaitTransaction(); //make sure we have all the info committed before looking for it.
163
+ //we are looking from log entries from the crash backwards in time
164
+ const getLogtime = db.prepare(`SELECT unixepoch(logtime,'subsec') AS logtime FROM Log WHERE logid = ?`)
165
+ const {logtime:lt } = getLogtime.get(lid)??{logtime:0}
166
+ if (lt > 0) {
167
+ const fetchRecords = db.prepare(`SELECT logid, logtime,crash,shortdate,ipaddress, topic,message,colourspec,gap FROM Log
168
+ WHERE (unixepoch(logtime,'subsec')) * 1000 <= ? AND logid <> ? AND ipaddress = ? ORDER BY unixepoch(logtime,'subsec') DESC LIMIT ?`)
169
+ if (!db.inTransaction) db.exec('BEGIN TRANSACTION');
170
+ for (const {logid,logtime,crash,shortdate,ipaddress,topic,message,colourspec,gap} of fetchRecords.iterate(lt, lid, ipadd??null, limit)) {
171
+ const output = messageFormatter(logid,logtime,crash,shortdate,ipaddress,topic,message,colourspec,gap)
172
+ await callback(output.logid, output.message);
173
+ }
174
+
175
+ } else {
176
+ console.log(chalk.white.bgBlue('The transaction provided has not yet cleared its transaction, so no records to list.'));
177
+ }
178
+ } catch(e) {
179
+ console.log(chalk.white.bgRed('failed with error'), e.stack);
180
+ throw e;
181
+ } finally {
182
+ if (db.inTransaction) db.exec('ROLLBACK'); //make sure callback hasn't changed anything
183
+ db.close();
79
184
  }
185
+ }
186
+
187
+
188
+
189
+ /*
190
+ Debug creates an instance of a debug function
191
+
192
+ parameters:
193
+ topic - a value that can be searched for. Useful for dividing into different sections
194
+ colourspec - One of name of standard colors [app,db,api,client,log,mail,auth,error], a hex color string, an rgb,
195
+ comma seperated, string of three values 0-255
196
+ shortdate - if true, then dates will be output as YYYY-MM-DD hh:mm else YYYY-MM-DD hh:mm:ss.ms
197
+
198
+ immediate - if set, the message is output (formatted) to the console.
199
+
200
+ Returns a function that will write a row into the log, using the parameters above and some optional extra values
201
+ these extra parameters are
202
+
203
+ crash - the literal word "crash". if set, then this will be highlighted in the output. Don't provide this as
204
+ the first parameter if a normal call
205
+ logtime - a unix millisecond timestamp. If provided if must be for today, otherwise it will be as
206
+ though it were not provided. If provided it will be the logtime, otherwise "Now" will be used.
207
+ ipaddress - an optional parameter container a string representation of an ip address. Ignored if not
208
+ a valid adddress. If provided its value will be highlighted and surrounded in "[]"
209
+ ...messages - As many parameters containing parts of the message. The message will be joined together
210
+ with a space separation and displayed with the colourspec parameter.
211
+ */
212
+
213
+ export function Debug (topic, colourspec, shortdate, immediate = false) {
214
+ return DebugHelper(topic, colourspec, shortdate, immediate, logWrapper);
80
215
  };
81
216
 
82
217
 
83
218
 
219
+ /*
220
+ Logger is like Debug (indeed its a wrapper for it) except
221
+
222
+ - It doesn't need short date, or immediate parameters as thats whats assumed
223
+
224
+ - If a crash, all the messages just before the crash (especially the debug ones that were not output before), are also
225
+ printed to the consolein reverse order
226
+ */
227
+
228
+ export function Logger(topic, colourspec) {
229
+ const debug = Debug(topic, colourspec, 1,1);
230
+ return function(c, ip, ...args) {
231
+ const crash = (c === 'crash');
232
+ const output = debug(c,ip,args);
233
+ console.log(output.message);
234
+ if (crash) {
235
+ let lt;
236
+ getDebugLog(async(logid,message) => {
237
+ if (lt === undefined) lt=message.substring(0,24);
238
+ console.log(chalk.whiteBright(logid), message)
239
+ },output.logid,Number(process.env.DEBUG_CACHE_SIZE??100), output.ip).then(() => {
240
+ console.log(chalk.whiteBright(lt),chalk.white.bgBlue('Above are all the debug calls (most recent first) which lead up to the error above') )
241
+
242
+ });
243
+ }
244
+ }
245
+ }
246
+
247
+ /*
248
+ Closes the database and ensures any partial transactions are written
249
+ */
250
+
251
+ export function close() {
252
+ shuttingDown = true;
253
+ DebugLog.emit('close');
254
+ if (db.inTransaction) {
255
+ db.exec('COMMIT;');
256
+ }
257
+ if(db.isOpen) db.close();
258
+
259
+
260
+ };
261
+
262
+
84
263
 
85
264
 
@@ -0,0 +1,84 @@
1
+ /**
2
+ @licence
3
+ Copyright (c) 2026 Alan Chandler, all rights reserved
4
+
5
+ This file is part of Server Utils.
6
+
7
+ Server Utils is free software: you can redistribute it and/or modify
8
+ it under the terms of the GNU General Public License as published by
9
+ the Free Software Foundation, either version 3 of the License, or
10
+ (at your option) any later version.
11
+
12
+ Server Utils is distributed in the hope that it will be useful,
13
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ GNU General Public License for more details.
16
+
17
+ You should have received a copy of the GNU General Public License
18
+ along with Server Utils. If not, see <http://www.gnu.org/licenses/>.
19
+ */
20
+ import chalk from "chalk";
21
+ export const COLOURS = {
22
+ hexmatch: /^#(?:[0-9a-fA-F]{3}){1,2}$/,
23
+ rgbmatch: /^(\d{1,3}), ?(\d{1,3}), ?(\d{1,3})$/,
24
+ COLOURS: {
25
+ app: chalk.rgb(255, 136, 0).bold, //orange,
26
+ db: chalk.greenBright,
27
+ api: chalk.magentaBright,
28
+ client: chalk.redBright,
29
+ log: chalk.hex('#ff651d'),
30
+ mail: chalk.cyanBright,
31
+ //error like have backGround colouring
32
+ auth: chalk.whiteBright.bgBlue,
33
+ error: chalk.whiteBright.bgHex('#ff1165')
34
+ }
35
+ };
36
+ const ipmatch = /^((25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)(\.|$)){4}$/;
37
+ const datematch =/^(\d{4})-([01]\d)-([0-3]\d) ([0-2]\d):([0-5]\d)(:([0-5]\d)(\.(\d{1,3}))?)?$/;
38
+ let lastdate = '';
39
+
40
+ export function messageFormatter(logid,logtime, crash, shortdate, ipaddress, topic, message, colourspec, gap) {
41
+ let matches;
42
+ if (typeof logtime === 'string') {
43
+ matches = logtime.match(datematch);
44
+ } else {
45
+ matches = [logtime];
46
+ }
47
+ const logdate = new Date(matches[0]);
48
+ logdate.setMinutes(logdate.getMinutes() + logdate.getTimezoneOffset());
49
+ const displaydate = `${logdate.getFullYear()}-${(logdate.getMonth() + 1).toString().padStart(2,'0')}-${logdate.getDate().toString().padStart(2,'0')}`;
50
+ const displaytime = `${logdate.getHours().toString().padStart(2,'0')}:${logdate.getMinutes().toString().padStart(2,'0')}:${
51
+ logdate.getSeconds().toString().padStart(2,'0')}.${
52
+ (typeof logtime === 'string')? (matches[9]??'').toString().padStart(3,'0') : logdate.getMilliseconds().toString().padStart(3,'0')}`;
53
+ const d = chalk.blueBright(`${displaydate} ${shortdate === 1? displaytime.slice(0,-7): displaytime}`);
54
+ const ip = ipmatch.test(ipaddress)? chalk.red(` [${ipaddress}]`) : '';
55
+ const t = chalk.greenBright(`(${topic})`);
56
+ let m;
57
+ let l = '';
58
+ if (crash === 1) {
59
+ l = chalk.whiteBright(` ${logid}`)
60
+ m = chalk.white.bgRed(message);
61
+ } else if (colourspec in COLOURS.COLOURS) {
62
+ m = COLOURS.COLOURS[colourspec](message);
63
+ } else if (COLOURS.hexmatch.test(colourspec)) {
64
+ m = chalk.hex(colourspec)(message);
65
+ } else if (COLOURS.rgbmatch.test(colourspec)) {
66
+ const matches = COLOURS.rgbmatch.exec(colourspec);
67
+ m = chalk.rgb(matches[1], matches[2], matches[3])(message);
68
+ } else {
69
+ m = chalk.cyan(message)
70
+ }
71
+ const g = Number.isInteger(gap)?chalk.whiteBright(` gap: ${shortdate? Math.round(gap/60000) + ' mins': gap + 'ms'}`):'';
72
+ let dayoutput = '';
73
+ if (lastdate !== displaydate) {
74
+ dayoutput = `${chalk.whiteBright(displaydate)}:\n`
75
+ lastdate = displaydate;
76
+ }
77
+
78
+ return {
79
+ dayoutput: dayoutput,
80
+ message:`${d}${l}${ip} ${t} ${m}${g}`,
81
+ logid: logid,
82
+ ip: ipaddress
83
+ }
84
+ };
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@akc42/server-utils",
3
- "version": "3.4.1",
3
+ "version": "4.0.0",
4
4
  "type": "module",
5
5
  "description": "A Set of Utilities to use on the Server Side of a Project",
6
- "main": "./utils.js",
6
+ "main": "./server-utils.js",
7
7
  "scripts": {
8
8
  "test": "echo \"Error: no test specified\" && exit 1"
9
9
  },
@@ -22,6 +22,7 @@
22
22
  },
23
23
  "homepage": "https://github.com/akc42/server-utils#readme",
24
24
  "dependencies": {
25
- "chalk": "^5.3.0"
25
+ "@akc42/sqlite-db": "3.0.6",
26
+ "chalk": "^5.6.2"
26
27
  }
27
28
  }
package/responder.js CHANGED
@@ -18,13 +18,8 @@
18
18
  along with Server Utils. If not, see <http://www.gnu.org/licenses/>.
19
19
  */
20
20
 
21
- import Debug from 'debug';
22
-
23
- const debug = Debug('responder');
24
-
25
- export default class Responder {
21
+ export class Responder {
26
22
  constructor(response) {
27
- debug('Starting responder');
28
23
  this.response = response;
29
24
  this.doneFirstRow = false;
30
25
  this.doneFirstSection = false;
@@ -50,11 +45,9 @@ export default class Responder {
50
45
  if (typeof value !== 'undefined') {
51
46
  this.response.write(JSON.stringify(value));
52
47
  this.inSection = false;
53
- debug('Write conplete section',name, 'with' , JSON.stringify(value));
54
48
  } else {
55
49
  this.response.write('[');
56
50
  this.inSection = true;
57
- debug('Started Section',name);
58
51
  }
59
52
  this.doneFirstSection = true;
60
53
  this.doneFirstRow = false;
@@ -80,15 +73,12 @@ export default class Responder {
80
73
  if (reply) {
81
74
  return Promise.resolve();
82
75
  }
83
- debug('False reply from write so need return the promise of a drain');
84
76
  if (!this.awaitingDrain) {
85
77
  this.awaitingDrain = true;
86
78
  const self = this;
87
- debug('create a drain promise as we do not have one');
88
79
  this.drainPromise = new Promise(resolve => {
89
80
  self.response.once('drain', () => {
90
81
  self.awaitingDrain = false;
91
- debug('drained so resolve promise of drain');
92
82
  resolve();
93
83
  });
94
84
  });
@@ -98,7 +88,6 @@ export default class Responder {
98
88
  return Promise.reject(); //mark as blocked
99
89
  }
100
90
  end() {
101
- debug('End Responder');
102
91
  if (!this.ended) {
103
92
  if (this.inSection) {
104
93
  this.response.write(']');
@@ -0,0 +1,38 @@
1
+ /**
2
+ @licence
3
+ Copyright (c) 2026 Alan Chandler, all rights reserved
4
+
5
+ This file is part of Server Utils.
6
+
7
+ Server Utils is free software: you can redistribute it and/or modify
8
+ it under the terms of the GNU General Public License as published by
9
+ the Free Software Foundation, either version 3 of the License, or
10
+ (at your option) any later version.
11
+
12
+ Server Utils is distributed in the hope that it will be useful,
13
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ GNU General Public License for more details.
16
+
17
+ You should have received a copy of the GNU General Public License
18
+ along with Server Utils. If not, see <http://www.gnu.org/licenses/>.
19
+ */
20
+
21
+
22
+
23
+ import {getVersion}from './version.js';
24
+ import {Responder} from './responder.js';
25
+ import { messageFormatter, COLOURS } from './message-formatter.js';
26
+ import { DebugHelper } from './debug-helper.js';
27
+ import { Debug, Logger} from './debug.js';
28
+ import { nullif0len } from './utils.js';
29
+ export {
30
+ COLOURS,
31
+ Debug,
32
+ DebugHelper,
33
+ getVersion,
34
+ Logger,
35
+ messageFormatter,
36
+ nullif0len,
37
+ Responder
38
+ };
package/utils.js CHANGED
@@ -1,35 +1,27 @@
1
1
  /**
2
2
  @licence
3
- Copyright (c) 2021 Alan Chandler, all rights reserved
3
+ Copyright (c) 2026 Alan Chandler, all rights reserved
4
4
 
5
- This file is part of Server Utils.
5
+ This file is part of Server Utils.
6
6
 
7
- Server Utils is free software: you can redistribute it and/or modify
8
- it under the terms of the GNU General Public License as published by
9
- the Free Software Foundation, either version 3 of the License, or
10
- (at your option) any later version.
7
+ Server Utils is free software: you can redistribute it and/or modify
8
+ it under the terms of the GNU General Public License as published by
9
+ the Free Software Foundation, either version 3 of the License, or
10
+ (at your option) any later version.
11
11
 
12
- Server Utils is distributed in the hope that it will be useful,
13
- but WITHOUT ANY WARRANTY; without even the implied warranty of
14
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
- GNU General Public License for more details.
12
+ Server Utils is distributed in the hope that it will be useful,
13
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ GNU General Public License for more details.
16
16
 
17
- You should have received a copy of the GNU General Public License
18
- along with Server Utils. If not, see <http://www.gnu.org/licenses/>.
17
+ You should have received a copy of the GNU General Public License
18
+ along with Server Utils. If not, see <http://www.gnu.org/licenses/>.
19
19
  */
20
20
 
21
21
 
22
- import logger from './logger.js';
23
- import Version from './version.js';
24
- import Responder from './responder.js';
25
- import { Debug, dumpDebugCache, setDebugConfig} from './debug.js';
22
+ export function nullif0len (str) {
23
+ if (typeof str === 'undefined') return null;
24
+ if (typeof str === 'string' && str.length === 0) return null;
25
+ return str;
26
+ };
26
27
 
27
-
28
- export {
29
- logger,
30
- Version,
31
- Responder,
32
- Debug,
33
- dumpDebugCache,
34
- setDebugConfig
35
- }
package/version.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  @licence
3
- Copyright (c) 2021 Alan Chandler, all rights reserved
3
+ Copyright (c) 2026 Alan Chandler, all rights reserved
4
4
 
5
5
  This file is part of Server Utils.
6
6
 
@@ -17,16 +17,11 @@
17
17
  You should have received a copy of the GNU General Public License
18
18
  along with Server Utils. If not, see <http://www.gnu.org/licenses/>.
19
19
  */
20
-
21
- import {Debug} from './debug.js';
22
20
  import {access, readFile, stat} from 'node:fs/promises';
23
21
  import { resolve } from 'node:path';
24
22
  import { exec } from 'node:child_process';
25
23
 
26
- const debug = Debug('version');
27
-
28
24
  async function shCmd(cmd, root) {
29
- debug('About to execute Command ', cmd);
30
25
  return new Promise(async (accept, reject) => {
31
26
  exec(cmd, { cwd: root }, (err, stdout, stderr) => {
32
27
  if (stderr) {
@@ -34,21 +29,16 @@ async function shCmd(cmd, root) {
34
29
  reject(err);
35
30
  } else {
36
31
  const out = stdout.trim();
37
- debug('Command ', cmd, 'Success with ', out);
38
32
  accept(out);
39
33
  }
40
34
  });
41
35
  });
42
36
  }
43
- export default async function(root) {
44
-
37
+ export async function getVersion(root) {
45
38
  let version;
46
39
  let vtime;
47
-
48
40
  try {
49
- debug('Look for git')
50
41
  await access(resolve(root, '.git'));
51
- debug('Git found, so use it to get data')
52
42
  //we get here if there is a git directory, so we can look up version and latest commit from them
53
43
  version = await shCmd('git describe --abbrev=0 --tags');
54
44
  //git is installed and we found a tag
@@ -60,7 +50,6 @@ export default async function(root) {
60
50
  } catch (e) {
61
51
  //no git, or no tag, so we must look for a version file
62
52
  try {
63
- debug('Git approach failed, so look for release info');
64
53
  version = await readFile(resolve(root, 'release.info'), 'utf8');
65
54
  try {
66
55
  const { mtime } = await stat(resolve(root, 'release.info'));
@@ -88,7 +77,6 @@ export default async function(root) {
88
77
  } finally {
89
78
  const finalversion = version.replace(/\s+/g, ' ').trim(); //trim out new lines and multiple spaces just one.
90
79
  const copyrightTime = new Date(vtime);
91
- debug('Resolving with Git copyright Year is ', copyrightTime.getUTCFullYear());
92
80
  return({ version: finalversion, year: copyrightTime.getUTCFullYear() });
93
81
  }
94
82
  };
package/logger.js DELETED
@@ -1,77 +0,0 @@
1
- /**
2
- @licence
3
- Copyright (c) 2021 Alan Chandler, all rights reserved
4
-
5
- This file is part of Server Utils.
6
-
7
- Server Utils is free software: you can redistribute it and/or modify
8
- it under the terms of the GNU General Public License as published by
9
- the Free Software Foundation, either version 3 of the License, or
10
- (at your option) any later version.
11
-
12
- Server Utils is distributed in the hope that it will be useful,
13
- but WITHOUT ANY WARRANTY; without even the implied warranty of
14
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
- GNU General Public License for more details.
16
-
17
- You should have received a copy of the GNU General Public License
18
- along with Server Utils. If not, see <http://www.gnu.org/licenses/>.
19
- */
20
-
21
- import chalk from 'chalk';
22
- import { isIP } from 'node:net';
23
-
24
- const COLOURS = {
25
- app: chalk.rgb(255, 136, 0).bold, //orange,
26
- db: chalk.greenBright,
27
- api: chalk.magentaBright,
28
- client: chalk.redBright,
29
- log: chalk.yellowBright,
30
- mail: chalk.cyanBright,
31
- //error like have backGround colouring
32
- auth: chalk.black.bgCyan,
33
- err: chalk.white.bgBlue,
34
- error: chalk.white.bgRed
35
-
36
- };
37
- function cyrb53 (str, seed = 0) {
38
- let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed;
39
- for (let i = 0, ch; i < str.length; i++) {
40
- ch = str.charCodeAt(i);
41
- h1 = Math.imul(h1 ^ ch, 2654435761);
42
- h2 = Math.imul(h2 ^ ch, 1597334677);
43
- }
44
- h1 = Math.imul(h1 ^ h1 >>> 16, 2246822507) ^ Math.imul(h2 ^ h2 >>> 13, 3266489909);
45
- h2 = Math.imul(h2 ^ h2 >>> 16, 2246822507) ^ Math.imul(h1 ^ h1 >>> 13, 3266489909);
46
- return 4294967296 * (2097151 & h2) + (h1 >>> 0);
47
- }
48
-
49
- export default function logger(ip,level, ...messages) {
50
- if (process.env.LOG_NONE === undefined) {
51
- let logLine = '';
52
- if (typeof process.env.LOG_NO_DATE === 'undefined') {
53
- if (typeof process.env.LOG_ISO_DATE !== 'undefined') {
54
- logLine += new Date().toISOString() + ': ';
55
- } else {
56
- logLine += new Date().toISOString().substring(0,10) + ' ' + new Date().toLocaleTimeString() + ': ';
57
- }
58
- }
59
- let message;
60
- let logcolor;
61
- if (isIP(ip) === 0 ) {
62
- logcolor = ip;
63
- message = level + messages.join(' ');
64
- } else {
65
- const client = typeof process.env.LOG_IP_HIDDEN !== 'undefined' ? cyrb53(ip): ip;
66
- logLine += COLOURS.client(client + ': ');
67
- logcolor = level
68
- message = messages.join(' ');
69
- }
70
- logLine += COLOURS[logcolor](message);
71
- //eslint-disable-next-line no-console
72
- console.log(logLine.trim());
73
- }
74
- }
75
-
76
-
77
-