@mark1russell7/harness 0.11.1 → 0.11.2
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/build/index.d.ts +10 -3
- package/build/index.js +21 -1
- package/build/index.js.map +1 -1
- package/image/pg/Dockerfile +11 -0
- package/image/pg/client-pg.mjs +162 -0
- package/image/pg/npm-lock.json +160 -0
- package/image/pg/package.json +9 -0
- package/package.json +5 -1
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# The harness's PostgreSQL client: node-postgres (pg 8.23.1 with pg-pool 3.14.0) on Node 24.21.0. It
|
|
2
|
+
# runs as a container in the client's network namespace, as the harness's own TCP client does, and
|
|
3
|
+
# prints the same JSON lines, with the events of the pool beside them.
|
|
4
|
+
# lab.ts stages the build context: this folder, under harness/image/pg. The lockfile is
|
|
5
|
+
# npm-lock.json here, because npm leaves each package-lock.json out of a published tarball.
|
|
6
|
+
FROM node:24.21.0-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401c70e0e762c5b53302f905ec1c1
|
|
7
|
+
WORKDIR /opt/harness/pg
|
|
8
|
+
COPY harness/image/pg/package.json ./
|
|
9
|
+
COPY harness/image/pg/npm-lock.json ./package-lock.json
|
|
10
|
+
RUN npm ci --omit=dev --ignore-scripts --no-audit --no-fund
|
|
11
|
+
COPY harness/image/pg/client-pg.mjs ./
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// client-pg HOST PORT --mode idle|inflight|stream [--interval-ms N] [--max-s N] [--go PATH]
|
|
2
|
+
// [--query TEXT] [--before] [--options JSON]
|
|
3
|
+
//
|
|
4
|
+
// The client of a PostgreSQL cell: node-postgres (pg 8.23.1 with pg-pool 3.14.0) on Node 24.21.0.
|
|
5
|
+
// Each event is a JSON line with `ev` and `t`, the time on CLOCK_MONOTONIC in nanoseconds
|
|
6
|
+
// (process.hrtime, which libuv reads from it). The prober stamps the records of the kernel on the
|
|
7
|
+
// same clock, and the injector stamps each fault on it.
|
|
8
|
+
//
|
|
9
|
+
// The lines:
|
|
10
|
+
// - start: the versions of pg, pg-pool and Node, the mode and the options of the pool.
|
|
11
|
+
// - socket, socket-end, socket-error: each socket of the pool, with its local address.
|
|
12
|
+
// - connected: the first socket. The TCP client of the harness names its own socket so.
|
|
13
|
+
// - pool: each event of the pool: connect, acquire, release, remove, and error with its errno.
|
|
14
|
+
// - ready: the reply to the first query: the connection is idle in the pool (idle, inflight), or
|
|
15
|
+
// the stream starts (stream).
|
|
16
|
+
// - sent, ack, fail: each query by its sequence number, with its error.
|
|
17
|
+
// - data, error: the end of the query in flight, as the TCP client of the harness tells it. In idle
|
|
18
|
+
// mode, an error of the pool for the first socket is an error line too: the pool removes it.
|
|
19
|
+
// - done, deadline: the end of the client.
|
|
20
|
+
//
|
|
21
|
+
// The modes: idle sends one query, gives the client back to the pool, then holds until --max-s.
|
|
22
|
+
// inflight sends one query, waits for the file --go, sends one more query into the fault, and holds
|
|
23
|
+
// after its end. stream sends a query each --interval-ms after the reply before, until --max-s or
|
|
24
|
+
// SIGTERM. --before starts the client into a fault that holds already: its first connect meets it.
|
|
25
|
+
|
|
26
|
+
import { readFileSync } from "node:fs";
|
|
27
|
+
import { existsSync } from "node:fs";
|
|
28
|
+
|
|
29
|
+
const now = () => process.hrtime.bigint();
|
|
30
|
+
const emit = (ev, fields = {}, at = now()) => process.stdout.write(`${JSON.stringify({ ev, t: Number(at), ...fields })}\n`);
|
|
31
|
+
|
|
32
|
+
const argv = process.argv.slice(2);
|
|
33
|
+
const flag = (name) => {
|
|
34
|
+
const at = argv.indexOf(name);
|
|
35
|
+
return at === -1 ? undefined : argv[at + 1];
|
|
36
|
+
};
|
|
37
|
+
const [host, port] = argv;
|
|
38
|
+
const mode = flag("--mode");
|
|
39
|
+
if (host === undefined || port === undefined || !["idle", "inflight", "stream"].includes(mode)) {
|
|
40
|
+
emit("fail", { why: "client-pg HOST PORT --mode idle|inflight|stream" });
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
const interval = Number(flag("--interval-ms") ?? 50);
|
|
44
|
+
const max = Number(flag("--max-s") ?? 120) * 1000;
|
|
45
|
+
const go = flag("--go") ?? "/tmp/go";
|
|
46
|
+
const query = flag("--query") ?? "SELECT 1";
|
|
47
|
+
const before = argv.includes("--before");
|
|
48
|
+
// the options of the pool. A cell gives each option that it changes, and pg-pool gives the others
|
|
49
|
+
const options = JSON.parse(flag("--options") ?? "{}");
|
|
50
|
+
|
|
51
|
+
const { default: pg } = await import("pg");
|
|
52
|
+
const versionOf = (name) => JSON.parse(readFileSync(new URL(`./node_modules/${name}/package.json`, import.meta.url), "utf8")).version;
|
|
53
|
+
|
|
54
|
+
const bootId = () => {
|
|
55
|
+
try {
|
|
56
|
+
return readFileSync("/proc/sys/kernel/random/boot_id", "utf8").trim();
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
emit("start", { boot_id: bootId(), client: "pg", driver: versionOf("pg"), pool: versionOf("pg-pool"), node: process.versions.node, mode, query, options });
|
|
62
|
+
|
|
63
|
+
const pool = new pg.Pool({ host, port: Number(port), user: "postgres", database: "postgres", ...options });
|
|
64
|
+
|
|
65
|
+
/** This function gives the error of pg as data: its name, its code, its severity and its message. */
|
|
66
|
+
const failure = (error) =>
|
|
67
|
+
error === undefined || error === null
|
|
68
|
+
? null
|
|
69
|
+
: { name: error.name, code: error.code ?? null, errno: error.errno ?? null, severity: error.severity ?? null, message: error.message };
|
|
70
|
+
/** This function gives the errno of an error: the code of the socket (ECONNRESET), the SQLSTATE of the server, or the message of pg. */
|
|
71
|
+
const errnoOf = (error) => error?.code ?? error?.message ?? null;
|
|
72
|
+
|
|
73
|
+
// ---- each socket of the pool: pg-pool emits connect after the first ReadyForQuery of a new client
|
|
74
|
+
let connected = null;
|
|
75
|
+
const localOf = (client) => {
|
|
76
|
+
const socket = client?.connection?.stream;
|
|
77
|
+
return socket?.localPort === undefined ? null : `${socket.localAddress}:${socket.localPort}`;
|
|
78
|
+
};
|
|
79
|
+
pool.on("connect", (client) => {
|
|
80
|
+
const socket = client.connection.stream;
|
|
81
|
+
const local = localOf(client);
|
|
82
|
+
emit("socket", { role: "pool", local });
|
|
83
|
+
emit("pool", { event: "connect", local });
|
|
84
|
+
if (connected === null && local !== null) {
|
|
85
|
+
connected = local;
|
|
86
|
+
emit("connected", { local });
|
|
87
|
+
}
|
|
88
|
+
socket.once("close", (hadError) => emit("socket-end", { role: "pool", local, error: hadError }));
|
|
89
|
+
socket.once("error", (error) => emit("socket-error", { role: "pool", local, errno: errnoOf(error), message: error.message }));
|
|
90
|
+
});
|
|
91
|
+
pool.on("acquire", (client) => emit("pool", { event: "acquire", local: localOf(client) }));
|
|
92
|
+
pool.on("release", (error, client) => emit("pool", { event: "release", local: localOf(client), failure: failure(error) }));
|
|
93
|
+
// the pool removes a client: after its idle timer, its lifetime or an error. A driver band names the close CLOSED
|
|
94
|
+
pool.on("remove", (client) => emit("pool", { event: "remove", name: "CLOSED", local: localOf(client) }));
|
|
95
|
+
// an idle client that fails: the pool removes it and emits error. Without this listener, Node throws
|
|
96
|
+
pool.on("error", (error, client) => {
|
|
97
|
+
const local = localOf(client);
|
|
98
|
+
emit("pool", { event: "error", local, failure: failure(error) });
|
|
99
|
+
// the socket of the client in idle mode: its error is the detection, as for the TCP client
|
|
100
|
+
if (mode === "idle" && local !== null && local === connected) emit("error", { errno: errnoOf(error) });
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// ---- the end of the client
|
|
104
|
+
let stopped = false;
|
|
105
|
+
const stop = (code = 0) => {
|
|
106
|
+
if (stopped) return;
|
|
107
|
+
stopped = true;
|
|
108
|
+
emit("done");
|
|
109
|
+
pool.end().finally(() => setTimeout(() => process.exit(code), 50));
|
|
110
|
+
};
|
|
111
|
+
process.on("SIGTERM", () => stop(0));
|
|
112
|
+
setTimeout(() => {
|
|
113
|
+
emit("deadline");
|
|
114
|
+
stop(0);
|
|
115
|
+
}, max);
|
|
116
|
+
|
|
117
|
+
/** This function sends query n through the pool. Its stamp is before the call. It gives null for a reply, or the error. */
|
|
118
|
+
const operate = async (n) => {
|
|
119
|
+
const at = now();
|
|
120
|
+
const reply = pool.query(query);
|
|
121
|
+
emit("sent", { n }, at);
|
|
122
|
+
try {
|
|
123
|
+
const result = await reply;
|
|
124
|
+
emit("ack", { n, rows: result.rowCount ?? null });
|
|
125
|
+
return null;
|
|
126
|
+
} catch (error) {
|
|
127
|
+
emit("fail", { n, ...failure(error) });
|
|
128
|
+
return error;
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
133
|
+
|
|
134
|
+
if (mode === "stream") {
|
|
135
|
+
emit("ready", { at: `${host}:${port}` });
|
|
136
|
+
for (let n = 1; !stopped; n += 1) {
|
|
137
|
+
await operate(n);
|
|
138
|
+
await sleep(interval);
|
|
139
|
+
}
|
|
140
|
+
} else {
|
|
141
|
+
// the first query makes the connection of the pool. Its reply is the last data before the fault
|
|
142
|
+
const failed = await operate(1);
|
|
143
|
+
if (failed !== null) {
|
|
144
|
+
emit("error", { errno: errnoOf(failed), message: failed.message });
|
|
145
|
+
stop(before ? 0 : 1);
|
|
146
|
+
} else {
|
|
147
|
+
// pool.query gives the client back to the pool at its reply: the connection is idle there
|
|
148
|
+
emit("ready", { reply: "ack" });
|
|
149
|
+
if (mode === "inflight") {
|
|
150
|
+
await new Promise((resolve) => {
|
|
151
|
+
const wait = setInterval(() => {
|
|
152
|
+
if (!existsSync(go)) return;
|
|
153
|
+
clearInterval(wait);
|
|
154
|
+
resolve();
|
|
155
|
+
}, 2);
|
|
156
|
+
});
|
|
157
|
+
const error = await operate(2);
|
|
158
|
+
if (error === null) emit("data", { n: 2 });
|
|
159
|
+
else emit("error", { errno: errnoOf(error), message: error.message });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "loom-harness-pg-client",
|
|
3
|
+
"lockfileVersion": 3,
|
|
4
|
+
"requires": true,
|
|
5
|
+
"packages": {
|
|
6
|
+
"": {
|
|
7
|
+
"name": "loom-harness-pg-client",
|
|
8
|
+
"dependencies": {
|
|
9
|
+
"pg": "8.23.1",
|
|
10
|
+
"pg-pool": "3.14.0"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"node_modules/pg": {
|
|
14
|
+
"version": "8.23.1",
|
|
15
|
+
"resolved": "https://registry.npmjs.org/pg/-/pg-8.23.1.tgz",
|
|
16
|
+
"integrity": "sha512-aL96AHANtWjPLDOLqnhx+ngp9+UK7ETEU8VJrDCGvsSSi/mGLcWYsS6Herg7lmaBJe4uwrfqsa7gTEFaSizDoQ==",
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"pg-connection-string": "^2.14.1",
|
|
20
|
+
"pg-pool": "^3.14.0",
|
|
21
|
+
"pg-protocol": "^1.16.1",
|
|
22
|
+
"pg-types": "2.2.0",
|
|
23
|
+
"pgpass": "1.0.5"
|
|
24
|
+
},
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">= 16.0.0"
|
|
27
|
+
},
|
|
28
|
+
"optionalDependencies": {
|
|
29
|
+
"pg-cloudflare": "^1.4.1"
|
|
30
|
+
},
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"pg-native": ">=3.0.1"
|
|
33
|
+
},
|
|
34
|
+
"peerDependenciesMeta": {
|
|
35
|
+
"pg-native": {
|
|
36
|
+
"optional": true
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"node_modules/pg-cloudflare": {
|
|
41
|
+
"version": "1.4.1",
|
|
42
|
+
"resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.1.tgz",
|
|
43
|
+
"integrity": "sha512-6PQbsFWZcp9EmJEwy5cGQ2La+AMWpP46lgbb8X+U/XsHIUweYDNCpeuKck5RxL2MdVFi7krbbEi5nX4Zh7JhrQ==",
|
|
44
|
+
"license": "MIT",
|
|
45
|
+
"optional": true
|
|
46
|
+
},
|
|
47
|
+
"node_modules/pg-connection-string": {
|
|
48
|
+
"version": "2.14.1",
|
|
49
|
+
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.14.1.tgz",
|
|
50
|
+
"integrity": "sha512-qR3kGNPBLpCNtz0evbKA0Y/MRFXwSSdT+pTJvYp/bXTcReZbvX1kzF0IyTc1QnxqF7AZbOeBhNL8R5mYQZV/MA==",
|
|
51
|
+
"license": "MIT"
|
|
52
|
+
},
|
|
53
|
+
"node_modules/pg-int8": {
|
|
54
|
+
"version": "1.0.1",
|
|
55
|
+
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
|
|
56
|
+
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
|
|
57
|
+
"license": "ISC",
|
|
58
|
+
"engines": {
|
|
59
|
+
"node": ">=4.0.0"
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"node_modules/pg-pool": {
|
|
63
|
+
"version": "3.14.0",
|
|
64
|
+
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz",
|
|
65
|
+
"integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==",
|
|
66
|
+
"license": "MIT",
|
|
67
|
+
"peerDependencies": {
|
|
68
|
+
"pg": ">=8.0"
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
"node_modules/pg-protocol": {
|
|
72
|
+
"version": "1.16.1",
|
|
73
|
+
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.16.1.tgz",
|
|
74
|
+
"integrity": "sha512-p9VOFMiHB/ZbJATetbg+99PxssTVSQRnyuPSQ67mN1+1KBOjZaZ83ZQzltnxPhJwSsC3nwVjJ10DVJlerbFzLg==",
|
|
75
|
+
"license": "MIT"
|
|
76
|
+
},
|
|
77
|
+
"node_modules/pg-types": {
|
|
78
|
+
"version": "2.2.0",
|
|
79
|
+
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
|
|
80
|
+
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
|
|
81
|
+
"license": "MIT",
|
|
82
|
+
"dependencies": {
|
|
83
|
+
"pg-int8": "1.0.1",
|
|
84
|
+
"postgres-array": "~2.0.0",
|
|
85
|
+
"postgres-bytea": "~1.0.0",
|
|
86
|
+
"postgres-date": "~1.0.4",
|
|
87
|
+
"postgres-interval": "^1.1.0"
|
|
88
|
+
},
|
|
89
|
+
"engines": {
|
|
90
|
+
"node": ">=4"
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
"node_modules/pgpass": {
|
|
94
|
+
"version": "1.0.5",
|
|
95
|
+
"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
|
|
96
|
+
"integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
|
|
97
|
+
"license": "MIT",
|
|
98
|
+
"dependencies": {
|
|
99
|
+
"split2": "^4.1.0"
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
"node_modules/postgres-array": {
|
|
103
|
+
"version": "2.0.0",
|
|
104
|
+
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
|
|
105
|
+
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
|
|
106
|
+
"license": "MIT",
|
|
107
|
+
"engines": {
|
|
108
|
+
"node": ">=4"
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
"node_modules/postgres-bytea": {
|
|
112
|
+
"version": "1.0.1",
|
|
113
|
+
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz",
|
|
114
|
+
"integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==",
|
|
115
|
+
"license": "MIT",
|
|
116
|
+
"engines": {
|
|
117
|
+
"node": ">=0.10.0"
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
"node_modules/postgres-date": {
|
|
121
|
+
"version": "1.0.7",
|
|
122
|
+
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
|
|
123
|
+
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
|
|
124
|
+
"license": "MIT",
|
|
125
|
+
"engines": {
|
|
126
|
+
"node": ">=0.10.0"
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
"node_modules/postgres-interval": {
|
|
130
|
+
"version": "1.2.0",
|
|
131
|
+
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
|
|
132
|
+
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
|
|
133
|
+
"license": "MIT",
|
|
134
|
+
"dependencies": {
|
|
135
|
+
"xtend": "^4.0.0"
|
|
136
|
+
},
|
|
137
|
+
"engines": {
|
|
138
|
+
"node": ">=0.10.0"
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
"node_modules/split2": {
|
|
142
|
+
"version": "4.2.0",
|
|
143
|
+
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
|
|
144
|
+
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
|
|
145
|
+
"license": "ISC",
|
|
146
|
+
"engines": {
|
|
147
|
+
"node": ">= 10.x"
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
"node_modules/xtend": {
|
|
151
|
+
"version": "4.0.2",
|
|
152
|
+
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
|
153
|
+
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
|
|
154
|
+
"license": "MIT",
|
|
155
|
+
"engines": {
|
|
156
|
+
"node": ">=0.4"
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package",
|
|
3
3
|
"name": "@mark1russell7/harness",
|
|
4
|
-
"version": "0.11.
|
|
4
|
+
"version": "0.11.2",
|
|
5
5
|
"description": "A fault-injection harness for connection timers: a cell runs a client against a stock target in Docker, through a topology of routers and proxies, a replicated group under Sentinels or a replica set, injects a fault, measures the kernel's own records and judges a prediction. HTTP clients of Go, Node and undici race the keep-alive of a Node server, and the MongoDB driver meets the faults of a replica set. The WebSocket client of @procedures/client drops a silent connection on its own heartbeat, and the harness times that detection and the reconnect from the program's lines and the kernel's stamps.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"fault-injection",
|
|
@@ -61,6 +61,10 @@
|
|
|
61
61
|
"image/mongo/stepdown-mongo.mjs",
|
|
62
62
|
"image/mongo/package.json",
|
|
63
63
|
"image/mongo/npm-lock.json",
|
|
64
|
+
"image/pg/Dockerfile",
|
|
65
|
+
"image/pg/client-pg.mjs",
|
|
66
|
+
"image/pg/package.json",
|
|
67
|
+
"image/pg/npm-lock.json",
|
|
64
68
|
"image/procedures/Dockerfile",
|
|
65
69
|
"image/procedures/package.json",
|
|
66
70
|
"image/procedures/npm-lock.json",
|