biz-a-cli 2.3.72 → 2.3.74
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/bin/app.js +23 -0
- package/bin/directHubEvent.js +93 -79
- package/bin/hub.js +42 -2
- package/bin/hubEvent.js +102 -21
- package/bin/migrate.js +169 -0
- package/callbackController.js +58 -36
- package/db/db.js +37 -0
- package/db/ds.js +189 -0
- package/engine/bpm/routes.js +61 -0
- package/engine/bpm/workflow-runtime.js +1136 -0
- package/engine/bpm/workflow.js +235 -0
- package/migrations/1777727873750__apps.sql +16 -0
- package/migrations/1777727892577__bpm.sql +274 -0
- package/package.json +18 -2
- package/scheduler/datalib.js +267 -183
- package/worker/cliScriptWorker.js +63 -0
- package/worker/cliWorkerPool.js +71 -0
- package/.editorconfig +0 -16
- package/bin/log/debug.log +0 -12
- package/bin/log/error.log +0 -12
- package/bin/log/exception.log +0 -6
- package/bin/log/info.log +0 -12
- package/log/debug.log +0 -181
- package/log/error.log +0 -7
- package/log/exception.log +0 -3
- package/log/info.log +0 -6
- package/tests/app.test.js +0 -1208
- package/tests/callback.test.js +0 -42
- package/tests/config.test.js +0 -39
- package/tests/converter.test.js +0 -106
- package/tests/data.test.js +0 -487
- package/tests/deployment.test.js +0 -339
- package/tests/hub.test.js +0 -657
- package/tests/hubPublish.test.js +0 -231
- package/tests/mailCtl.test.js +0 -44
- package/tests/timer.test.js +0 -187
- package/tests/watcher.test.js +0 -352
- package/tests/watcherCtl.test.js +0 -124
package/bin/app.js
CHANGED
|
@@ -14,6 +14,7 @@ import path, { basename } from "node:path";
|
|
|
14
14
|
import { env } from "../envs/env.js";
|
|
15
15
|
import { prepareScript, encryptScript } from "./script.js";
|
|
16
16
|
import { spawn } from "node:child_process";
|
|
17
|
+
import { newMigration, runMigrations } from "./migrate.js";
|
|
17
18
|
|
|
18
19
|
const getKeyFolderPath = () => {
|
|
19
20
|
const scriptPath =
|
|
@@ -654,6 +655,28 @@ const buildCli = () =>
|
|
|
654
655
|
})();
|
|
655
656
|
},
|
|
656
657
|
)
|
|
658
|
+
.command(
|
|
659
|
+
"migrate:new",
|
|
660
|
+
"Create a new blank migration file with an epoch timestamp",
|
|
661
|
+
(yargs) => {
|
|
662
|
+
return yargs.option("n", {
|
|
663
|
+
alias: "name",
|
|
664
|
+
describe:
|
|
665
|
+
'Name of the migration feature (e.g., "BPM", "Domain", "new feature_name")',
|
|
666
|
+
type: "string",
|
|
667
|
+
demandOption: true,
|
|
668
|
+
});
|
|
669
|
+
},
|
|
670
|
+
(commandOptions) => newMigration(commandOptions),
|
|
671
|
+
)
|
|
672
|
+
.command(
|
|
673
|
+
"migrate:run",
|
|
674
|
+
"Run pending DDL migrations from the /migrations folder",
|
|
675
|
+
{},
|
|
676
|
+
async (commandOptions) => {
|
|
677
|
+
await runMigrations(commandOptions);
|
|
678
|
+
},
|
|
679
|
+
)
|
|
657
680
|
.recommendCommands()
|
|
658
681
|
.demandCommand(1, "You need at least one command before moving on")
|
|
659
682
|
.strict();
|
package/bin/directHubEvent.js
CHANGED
|
@@ -1,86 +1,100 @@
|
|
|
1
|
-
import { clientListener, RECONNECT_SOCKET_DELAY } from
|
|
2
|
-
import { deploymentListenerForVSCode } from
|
|
3
|
-
import { Tunnel as QuickTunnel } from
|
|
4
|
-
import { Server as ioServer } from
|
|
1
|
+
import { clientListener, RECONNECT_SOCKET_DELAY } from "./hubEvent.js";
|
|
2
|
+
import { deploymentListenerForVSCode } from "./deployEvent.js";
|
|
3
|
+
import { Tunnel as QuickTunnel } from "cloudflared";
|
|
4
|
+
import { Server as ioServer } from "socket.io";
|
|
5
5
|
|
|
6
|
-
export const CLIENT_ROOM =
|
|
6
|
+
export const CLIENT_ROOM = "clientRoom";
|
|
7
7
|
|
|
8
|
-
export async function localhostTunnel(port, notifier){
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
8
|
+
export async function localhostTunnel(port, notifier) {
|
|
9
|
+
let tunnelUrl = "";
|
|
10
|
+
let qt = QuickTunnel.quick("127.0.0.1:" + port);
|
|
11
|
+
function reconnectTunnel() {
|
|
12
|
+
setTimeout(() => {
|
|
13
|
+
qt = localhostTunnel(port, notifier);
|
|
14
|
+
}, RECONNECT_SOCKET_DELAY);
|
|
15
|
+
}
|
|
16
|
+
qt.on("url", (qtUrl) => {
|
|
17
|
+
// console.log('qt url', qtUrl)
|
|
18
|
+
tunnelUrl = qtUrl;
|
|
19
|
+
})
|
|
20
|
+
.on("connected", (conn) => {
|
|
21
|
+
// console.log('qt connect', conn)
|
|
22
|
+
notifier(tunnelUrl);
|
|
23
|
+
console.log(
|
|
24
|
+
`${new Date()}: Connected to Direct Hub at public URL ${tunnelUrl}`,
|
|
25
|
+
);
|
|
26
|
+
})
|
|
27
|
+
.on("disconnected", (conn) => {
|
|
28
|
+
// console.log('qt disconnect', conn)
|
|
29
|
+
notifier("");
|
|
30
|
+
qt.stop();
|
|
31
|
+
})
|
|
32
|
+
.on("error", (err) => {
|
|
33
|
+
// console.log('qt err', err)
|
|
34
|
+
notifier("");
|
|
35
|
+
qt.stop();
|
|
36
|
+
})
|
|
37
|
+
.on("exit", (code) => {
|
|
38
|
+
// console.log('qt exit', code)
|
|
39
|
+
notifier("");
|
|
40
|
+
console.log(
|
|
41
|
+
`${new Date()}: Direct Hub is not available, will try to reinitiate it in ${RECONNECT_SOCKET_DELAY / 1000} seconds. Error code : `,
|
|
42
|
+
code,
|
|
43
|
+
);
|
|
44
|
+
reconnectTunnel();
|
|
45
|
+
// })
|
|
46
|
+
// on('stdout', (data)=>{
|
|
47
|
+
// console.log('qt stdout', data)
|
|
48
|
+
// })
|
|
49
|
+
// on('stderr', (data)=>{
|
|
50
|
+
// console.log('qt stderr', data)
|
|
51
|
+
});
|
|
52
|
+
return qt;
|
|
46
53
|
}
|
|
47
54
|
|
|
48
|
-
export function directHubEvent(serverSocket, argv){
|
|
49
|
-
|
|
55
|
+
export function directHubEvent(serverSocket, argv) {
|
|
56
|
+
serverSocket.on("connection", (clientSocket) => {
|
|
57
|
+
const setConnectListeners = (sock) => {
|
|
58
|
+
// shall not log in production mode
|
|
59
|
+
if (process.env.NODE_ENV !== "production") {
|
|
60
|
+
const id = `<${sock.handshake?.query?.isClient ? `Client` : sock.handshake?.query?.isDeploy ? "Deployer" : "Anonymous"}> ${sock.id}`;
|
|
61
|
+
console.log(id, `connected`);
|
|
62
|
+
sock.on("disconnect", (reason) => {
|
|
63
|
+
console.log(id, `disconnected. Reason : ${reason}`);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
};
|
|
50
67
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if (clientSocket.handshake.query.isClient) {
|
|
63
|
-
setConnectListeners(clientSocket);
|
|
64
|
-
clientListener(clientSocket, argv);
|
|
65
|
-
clientSocket.join(CLIENT_ROOM);
|
|
66
|
-
}
|
|
67
|
-
else if (clientSocket.handshake.query.isDeploy) {
|
|
68
|
-
setConnectListeners(clientSocket);
|
|
69
|
-
deploymentListenerForVSCode(clientSocket, argv)
|
|
70
|
-
} else {
|
|
71
|
-
clientSocket.disconnect();
|
|
72
|
-
}
|
|
73
|
-
})
|
|
68
|
+
if (clientSocket.handshake.query.isClient) {
|
|
69
|
+
setConnectListeners(clientSocket);
|
|
70
|
+
clientListener(clientSocket, argv);
|
|
71
|
+
clientSocket.join(CLIENT_ROOM);
|
|
72
|
+
} else if (clientSocket.handshake.query.isDeploy) {
|
|
73
|
+
setConnectListeners(clientSocket);
|
|
74
|
+
deploymentListenerForVSCode(clientSocket, argv);
|
|
75
|
+
} else {
|
|
76
|
+
clientSocket.disconnect();
|
|
77
|
+
}
|
|
78
|
+
});
|
|
74
79
|
}
|
|
75
80
|
|
|
76
|
-
export function createSocketServer(httpServer, cliIpAddress=
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
81
|
+
export function createSocketServer(httpServer, cliIpAddress = "127.0.0.1") {
|
|
82
|
+
return new ioServer(httpServer, {
|
|
83
|
+
cors: {
|
|
84
|
+
origin: [
|
|
85
|
+
"https://biz-a.id",
|
|
86
|
+
"https://test.biz-a.id",
|
|
87
|
+
/\.biz-a\.id$/,
|
|
88
|
+
"vscode-file://vscode-app",
|
|
89
|
+
/\.vscode-cdn\.net$/,
|
|
90
|
+
].concat(
|
|
91
|
+
process.env.NODE_ENV === "production"
|
|
92
|
+
? []
|
|
93
|
+
: [`http://${cliIpAddress}:4200`, "http://localhost:4200"],
|
|
94
|
+
),
|
|
95
|
+
},
|
|
96
|
+
maxHttpBufferSize: 1e8, // 100 MB
|
|
97
|
+
pingInterval: 35000, // default = 25000
|
|
98
|
+
pingTimeout: 30000, // default = 20000
|
|
99
|
+
});
|
|
100
|
+
}
|
package/bin/hub.js
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
createSocketServer,
|
|
21
21
|
} from "./directHubEvent.js";
|
|
22
22
|
import { env } from "../envs/env.js";
|
|
23
|
+
import { setGlobalConfig } from "../db/ds.js";
|
|
23
24
|
|
|
24
25
|
const logger = createLogger({
|
|
25
26
|
level: "info",
|
|
@@ -52,7 +53,7 @@ const argv = yargs(process.argv.slice(2))
|
|
|
52
53
|
.usage("Usage: $0 [options]")
|
|
53
54
|
.options("s", {
|
|
54
55
|
alias: "server",
|
|
55
|
-
default: env.BIZA_SERVER_LINK,
|
|
56
|
+
// default: env.BIZA_SERVER_LINK, // handle by serverMode
|
|
56
57
|
describe: "(Required) Tunnel server endpoint",
|
|
57
58
|
type: "string",
|
|
58
59
|
demandOption: false,
|
|
@@ -106,12 +107,48 @@ const argv = yargs(process.argv.slice(2))
|
|
|
106
107
|
})
|
|
107
108
|
.options("hs", {
|
|
108
109
|
alias: "hubServer",
|
|
109
|
-
default: `${env.BIZA_HUB_SERVER_LINK}`,
|
|
110
|
+
// default: `${env.BIZA_HUB_SERVER_LINK}`, // handle by serverMode
|
|
110
111
|
describe: "BizA hub",
|
|
111
112
|
type: "string",
|
|
112
113
|
demandOption: false,
|
|
114
|
+
})
|
|
115
|
+
.options("serverMode", {
|
|
116
|
+
alias: "sMode",
|
|
117
|
+
describe: "Backend server mode: prod, dev, or backup",
|
|
118
|
+
type: "string",
|
|
119
|
+
// choices: ["prod", "dev", "backup"],
|
|
120
|
+
choices: ["prod", "dev"],
|
|
121
|
+
default: "prod",
|
|
113
122
|
}).argv;
|
|
114
123
|
|
|
124
|
+
// Server endpoint mapping by mode
|
|
125
|
+
const SERVER_ENDPOINTS = {
|
|
126
|
+
prod: {
|
|
127
|
+
server: env.BIZA_SERVER_LINK,
|
|
128
|
+
hub: env.BIZA_HUB_SERVER_LINK,
|
|
129
|
+
},
|
|
130
|
+
dev: {
|
|
131
|
+
server: "https://devServer.biz-a.id",
|
|
132
|
+
hub: "https://devHub.biz-a.id",
|
|
133
|
+
},
|
|
134
|
+
// backup: {
|
|
135
|
+
// server: "https://backupServer.biz-a.id",
|
|
136
|
+
// hub: "https://backupHub.biz-a.id",
|
|
137
|
+
// },
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
if (!argv.server) {
|
|
141
|
+
argv.server =
|
|
142
|
+
SERVER_ENDPOINTS[argv.serverMode]?.server ||
|
|
143
|
+
SERVER_ENDPOINTS.prod.server;
|
|
144
|
+
}
|
|
145
|
+
if (!argv.hubServer) {
|
|
146
|
+
argv.hubServer =
|
|
147
|
+
SERVER_ENDPOINTS[argv.serverMode]?.hub || SERVER_ENDPOINTS.prod.hub;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
setGlobalConfig(argv);
|
|
151
|
+
|
|
115
152
|
if (argv.help) {
|
|
116
153
|
yargs().showHelp();
|
|
117
154
|
process.exit();
|
|
@@ -137,6 +174,7 @@ import fs from "fs";
|
|
|
137
174
|
import http from "http";
|
|
138
175
|
import https from "https";
|
|
139
176
|
import path from "node:path";
|
|
177
|
+
import bpmRoutes from "../engine/bpm/routes.js";
|
|
140
178
|
|
|
141
179
|
app.use(compression());
|
|
142
180
|
app.use(cors());
|
|
@@ -154,6 +192,8 @@ app.use("/status", (req, res) => {
|
|
|
154
192
|
res.status(200).json(status(argv));
|
|
155
193
|
});
|
|
156
194
|
|
|
195
|
+
app.use("/bpm", bpmRoutes);
|
|
196
|
+
|
|
157
197
|
// create HTTP(s) Server
|
|
158
198
|
const keyFile = path.join(import.meta.dirname, "../cert/key.pem");
|
|
159
199
|
const certFile = path.join(import.meta.dirname, "../cert/cert.pem");
|
package/bin/hubEvent.js
CHANGED
|
@@ -9,8 +9,9 @@ import os from "node:os";
|
|
|
9
9
|
const packageJson = require("../package.json");
|
|
10
10
|
// import { pipeline } from 'node:stream'
|
|
11
11
|
import { deploymentListenerForHubServer } from "./deployEvent.js";
|
|
12
|
+
import { execCLIScriptWorker, workerStats } from "../worker/cliWorkerPool.js";
|
|
12
13
|
|
|
13
|
-
export const IDLE_SOCKET_TIMEOUT_MILLISECONDS = 1000 *
|
|
14
|
+
export const IDLE_SOCKET_TIMEOUT_MILLISECONDS = 1000 * 28;
|
|
14
15
|
export const RECONNECT_SOCKET_DELAY = 60 * 1000;
|
|
15
16
|
const DISCONNECT_REASON_BY_SOCKET_SERVER = "io server disconnect";
|
|
16
17
|
|
|
@@ -130,15 +131,37 @@ export const streamEvent = async (socket, argv) =>
|
|
|
130
131
|
});
|
|
131
132
|
};
|
|
132
133
|
|
|
134
|
+
// const cliReqCb = async (data, callback) => {
|
|
135
|
+
// const { path, method, ...remainData } = data;
|
|
136
|
+
|
|
137
|
+
// const result = await axios.request({
|
|
138
|
+
// method: data.method,
|
|
139
|
+
// url: `${process.env.HOST || "http://localhost"}:${argv.serverport}/cb${path || ""}`,
|
|
140
|
+
// data: remainData,
|
|
141
|
+
// });
|
|
142
|
+
// callback(result.data);
|
|
143
|
+
// };
|
|
144
|
+
|
|
133
145
|
const cliReqCb = async (data, callback) => {
|
|
134
146
|
const { path, method, ...remainData } = data;
|
|
135
147
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
148
|
+
try {
|
|
149
|
+
const result = await axios.request({
|
|
150
|
+
method: data.method,
|
|
151
|
+
url: `${process.env.HOST || "http://localhost"}:${argv.serverport}/cb${path || ""}`,
|
|
152
|
+
data: remainData,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
callback(result.data);
|
|
156
|
+
} catch (error) {
|
|
157
|
+
if (error.response) {
|
|
158
|
+
callback(error.response.data);
|
|
159
|
+
} else {
|
|
160
|
+
callback(
|
|
161
|
+
`Proxy Error: Local CLI unreachable. ${error.message}`,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
142
165
|
};
|
|
143
166
|
|
|
144
167
|
const publishReqCb = async (data, callback) => {
|
|
@@ -211,6 +234,7 @@ export const streamEvent = async (socket, argv) =>
|
|
|
211
234
|
socket.on("incomingClient", incomingHubCb);
|
|
212
235
|
socket.on("cli-req", cliReqCb);
|
|
213
236
|
socket.on("publish-req", publishReqCb);
|
|
237
|
+
socket.on("cliCommand", (data, cb) => handleCliCommand(data, cb, argv));
|
|
214
238
|
|
|
215
239
|
socket.on("disconnect", (reason) => {
|
|
216
240
|
console.log(
|
|
@@ -315,6 +339,7 @@ export const status = (argv) => {
|
|
|
315
339
|
memoryUsage: `${(process.memoryUsage().rss / (1024 * 1024)).toFixed(2)} MB`,
|
|
316
340
|
nodeVersion: process.version,
|
|
317
341
|
uptime: getUptime(),
|
|
342
|
+
workers: workerStats(),
|
|
318
343
|
},
|
|
319
344
|
api: {
|
|
320
345
|
address: `${argv.hostname}:${argv.port}`,
|
|
@@ -334,6 +359,75 @@ export const status = (argv) => {
|
|
|
334
359
|
};
|
|
335
360
|
};
|
|
336
361
|
|
|
362
|
+
const EngineRegistry = {
|
|
363
|
+
// ZERO-MEMORY LAZY REGISTRY. Lazy load the module into memory (Only impacts performance on the very first call)
|
|
364
|
+
"bpm/workflow": () => import("../engine/bpm/workflow.js"),
|
|
365
|
+
"bpm/workflowRT": () => import("../engine/bpm/workflow-runtime.js"),
|
|
366
|
+
// 'domain/system': () => import('../engine/domain/system.js') // example for Domain Engine
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
const handleCliCommand = async (data, cb, argv) => {
|
|
370
|
+
try {
|
|
371
|
+
const command = data.command.trim().toLowerCase();
|
|
372
|
+
switch (command) {
|
|
373
|
+
case "status":
|
|
374
|
+
cb(null, status(argv));
|
|
375
|
+
break;
|
|
376
|
+
case "runcliscript":
|
|
377
|
+
const liveTriggerPayload = {
|
|
378
|
+
arguments: argv,
|
|
379
|
+
body: data.scriptData ?? null,
|
|
380
|
+
path: "/live-trigger",
|
|
381
|
+
query: { scriptName: data.scriptName },
|
|
382
|
+
headers: { "user-agent": "biz-a-client-socket" },
|
|
383
|
+
};
|
|
384
|
+
cb(
|
|
385
|
+
null,
|
|
386
|
+
await execCLIScriptWorker(
|
|
387
|
+
{
|
|
388
|
+
url: `${argv["secure"] == true ? "https://" : "http://"}${argv["hostname"]}:${argv["port"]}`,
|
|
389
|
+
dbindex: argv["dbindex"],
|
|
390
|
+
subdomain: argv["subdomain"],
|
|
391
|
+
},
|
|
392
|
+
data.scriptName,
|
|
393
|
+
liveTriggerPayload,
|
|
394
|
+
),
|
|
395
|
+
);
|
|
396
|
+
break;
|
|
397
|
+
case "engine": {
|
|
398
|
+
// Ex: Extracts "bpm/instance" and "initiate" from "bpm/instance/initiate"
|
|
399
|
+
const targetParts = data.target.split("/");
|
|
400
|
+
const methodName = targetParts.pop();
|
|
401
|
+
const modulePath = targetParts.join("/");
|
|
402
|
+
|
|
403
|
+
if (!EngineRegistry[modulePath]) {
|
|
404
|
+
throw new Error(
|
|
405
|
+
`Engine target '${modulePath}' is not registered on this CLI.`,
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const engineModule = await EngineRegistry[modulePath]();
|
|
410
|
+
|
|
411
|
+
if (typeof engineModule[methodName] !== "function") {
|
|
412
|
+
throw new Error(
|
|
413
|
+
`Method '${methodName}' does not exist on target '${modulePath}'.`,
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const result = await engineModule[methodName](data.payload);
|
|
418
|
+
|
|
419
|
+
cb(null, { success: true, data: result });
|
|
420
|
+
break;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
default:
|
|
424
|
+
cb(`Unknown CLI command '${command}'`, null);
|
|
425
|
+
}
|
|
426
|
+
} catch (err) {
|
|
427
|
+
cb(err.message || err, null);
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
|
|
337
431
|
export const clientListener = (socket, argv) => {
|
|
338
432
|
socket
|
|
339
433
|
.on("apiRequest", (reqData, resCB) => {
|
|
@@ -404,19 +498,6 @@ export const clientListener = (socket, argv) => {
|
|
|
404
498
|
}
|
|
405
499
|
})
|
|
406
500
|
.on("cliCommand", async (data, cb) => {
|
|
407
|
-
|
|
408
|
-
const command = data.command.trim().toLowerCase();
|
|
409
|
-
switch (
|
|
410
|
-
command //ensure case insensitive
|
|
411
|
-
) {
|
|
412
|
-
case "status":
|
|
413
|
-
cb(null, status(argv));
|
|
414
|
-
break;
|
|
415
|
-
default:
|
|
416
|
-
cb(`Unknown CLI command '${command}'`, null);
|
|
417
|
-
}
|
|
418
|
-
} catch (err) {
|
|
419
|
-
cb(err.message || err, null);
|
|
420
|
-
}
|
|
501
|
+
handleCliCommand(data, cb, argv);
|
|
421
502
|
});
|
|
422
503
|
};
|
package/bin/migrate.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { queryData, save as dbSave, executeBlock } from "../db/db.js";
|
|
4
|
+
|
|
5
|
+
const flattenSql = (sqlString) => {
|
|
6
|
+
return sqlString
|
|
7
|
+
.replace(/--.*$/gm, "") // 1. Strip all SQL comments
|
|
8
|
+
.split(/\r?\n/) // 2. Split into array of individual lines
|
|
9
|
+
.map((line) => line.trim()) // 3. Trim leading/trailing indentation
|
|
10
|
+
.filter((line) => line.length > 0) // 4. Remove empty lines entirely
|
|
11
|
+
.join(" "); // 5. Join safely with exactly one space
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const newMigration = (argv) => {
|
|
15
|
+
const dir = path.resolve(process.cwd(), "migrations");
|
|
16
|
+
if (!fs.existsSync(dir)) {
|
|
17
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const safeName = argv.name
|
|
21
|
+
.trim()
|
|
22
|
+
.replace(/[\s-]+/g, "_")
|
|
23
|
+
.toLowerCase();
|
|
24
|
+
const filename = `${Date.now()}__${safeName}.sql`;
|
|
25
|
+
|
|
26
|
+
// Defensive Template: Bypasses are commented out by default.
|
|
27
|
+
// DDL must use `execute statement` inside the block to bypass compile-time checks.
|
|
28
|
+
const template = [
|
|
29
|
+
"-- To safely bypass this script if a main table already exists, uncomment the line below and replace MAIN_TABLE_NAME",
|
|
30
|
+
"-- -- @RunOnlyIfMissing: MAIN_TABLE_NAME",
|
|
31
|
+
"",
|
|
32
|
+
"EXECUTE BLOCK AS",
|
|
33
|
+
"BEGIN",
|
|
34
|
+
" -- Write your DDL/SQL here using execute statement",
|
|
35
|
+
" -- execute statement 'CREATE TABLE YOUR_TABLE(ID INT NOT NULL PRIMARY KEY);';",
|
|
36
|
+
"END",
|
|
37
|
+
"",
|
|
38
|
+
].join("\n");
|
|
39
|
+
|
|
40
|
+
fs.writeFileSync(path.join(dir, filename), template);
|
|
41
|
+
console.log(`[Success] Created new migration file: migrations/${filename}`);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const runMigrations = async (argv) => {
|
|
45
|
+
const migrationsDir = path.resolve(process.cwd(), "migrations");
|
|
46
|
+
|
|
47
|
+
if (!fs.existsSync(migrationsDir)) {
|
|
48
|
+
console.log(
|
|
49
|
+
`[Migrate] No 'migrations' folder found at ${migrationsDir}`,
|
|
50
|
+
);
|
|
51
|
+
return { success: false, error: "Missing migrations folder" };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const apiConfig = {
|
|
55
|
+
url: `${argv.server}:${argv.apiPort}`,
|
|
56
|
+
dbindex: argv.dbIndex,
|
|
57
|
+
subdomain: argv.subdomain,
|
|
58
|
+
timeout: 30000,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const bootstrapMigrationTable = async () => {
|
|
62
|
+
const bootstrapSql = `
|
|
63
|
+
EXECUTE BLOCK AS
|
|
64
|
+
BEGIN
|
|
65
|
+
IF (NOT EXISTS(SELECT 1 FROM RDB$RELATIONS WHERE UPPER(RDB$RELATION_NAME) = 'SYS$MIGRATIONS')) THEN
|
|
66
|
+
BEGIN
|
|
67
|
+
EXECUTE STATEMENT 'CREATE GENERATOR SYS$MIGRATIONS_GEN;';
|
|
68
|
+
EXECUTE STATEMENT 'CREATE TABLE SYS$MIGRATIONS(ID INTEGER NOT NULL PRIMARY KEY, FILE_NAME VARCHAR(255) NOT NULL, EXECUTED_AT TIMESTAMP NOT NULL, CONSTRAINT UNQ_SYS$MIGRATIONS_FILE UNIQUE (FILE_NAME));';
|
|
69
|
+
EXECUTE STATEMENT 'CREATE TRIGGER SYS$MIGRATIONS_BI FOR SYS$MIGRATIONS ACTIVE BEFORE INSERT POSITION 0 AS BEGIN IF (NEW.ID IS NULL) THEN NEW.ID = GEN_ID(SYS$MIGRATIONS_GEN, 1); IF (NEW.EXECUTED_AT IS NULL) THEN NEW.EXECUTED_AT = ''NOW''; END';
|
|
70
|
+
END
|
|
71
|
+
END
|
|
72
|
+
`;
|
|
73
|
+
await executeBlock(flattenSql(bootstrapSql), apiConfig);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const files = fs
|
|
78
|
+
.readdirSync(migrationsDir)
|
|
79
|
+
.filter((f) => f.endsWith(".sql"))
|
|
80
|
+
.sort();
|
|
81
|
+
|
|
82
|
+
if (files.length === 0) {
|
|
83
|
+
console.log(`[Migrate] Migrations folder is empty. Nothing to do.`);
|
|
84
|
+
return { success: true };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
console.log(`[Migrate] Checking SYS$MIGRATIONS table...`);
|
|
88
|
+
|
|
89
|
+
await bootstrapMigrationTable();
|
|
90
|
+
|
|
91
|
+
const appliedParam = {
|
|
92
|
+
length: -1,
|
|
93
|
+
columns: [{ data: "SYS$MIGRATIONS.FILE_NAME", key: "file" }],
|
|
94
|
+
};
|
|
95
|
+
const appliedRes = await queryData(appliedParam, apiConfig, true);
|
|
96
|
+
const appliedFiles = Array.isArray(appliedRes)
|
|
97
|
+
? appliedRes.map((r) => r.file.trim())
|
|
98
|
+
: [];
|
|
99
|
+
|
|
100
|
+
for (const file of files) {
|
|
101
|
+
if (appliedFiles.includes(file)) continue;
|
|
102
|
+
|
|
103
|
+
console.log(`[Migrate] Executing ${file}...`);
|
|
104
|
+
const sqlContent = fs.readFileSync(
|
|
105
|
+
path.join(migrationsDir, file),
|
|
106
|
+
"utf8",
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
// BYPASS LOGIC: Evaluates strict start-of-line tag
|
|
110
|
+
const baselineMatch = sqlContent.match(
|
|
111
|
+
/^--\s*@RunOnlyIfMissing:\s*([A-Za-z0-9_$]+)/im,
|
|
112
|
+
);
|
|
113
|
+
if (baselineMatch) {
|
|
114
|
+
const targetTable = baselineMatch[1].toUpperCase();
|
|
115
|
+
|
|
116
|
+
const bypassCheckParam = {
|
|
117
|
+
length: 1,
|
|
118
|
+
filter: [
|
|
119
|
+
{
|
|
120
|
+
junction: "",
|
|
121
|
+
column: "RDB$RELATIONS.RDB$RELATION_NAME",
|
|
122
|
+
operator: "=",
|
|
123
|
+
value1: `'${targetTable}'`,
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
columns: [
|
|
127
|
+
{
|
|
128
|
+
data: "RDB$RELATIONS.RDB$RELATION_NAME",
|
|
129
|
+
key: "name",
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const bypassCheckRes = await queryData(
|
|
135
|
+
bypassCheckParam,
|
|
136
|
+
apiConfig,
|
|
137
|
+
true,
|
|
138
|
+
);
|
|
139
|
+
if (bypassCheckRes && bypassCheckRes.length > 0) {
|
|
140
|
+
console.log(
|
|
141
|
+
`[Migrate] Bypass Triggered: ${targetTable} exists. Marking ${file} as done.`,
|
|
142
|
+
);
|
|
143
|
+
await dbSave(
|
|
144
|
+
{ SYS$MIGRATIONS: { id: null, file_name: file } },
|
|
145
|
+
apiConfig,
|
|
146
|
+
);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// mark migration current file as "Applied"
|
|
152
|
+
const trackerSql = `\n INSERT INTO SYS$MIGRATIONS (FILE_NAME) VALUES ('${file}');\n`;
|
|
153
|
+
let rawSql = sqlContent.replace(/END\s*$/i, trackerSql + "END");
|
|
154
|
+
|
|
155
|
+
await executeBlock(flattenSql(rawSql), apiConfig);
|
|
156
|
+
|
|
157
|
+
console.log(`[Migrate] Success: ${file}`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
console.log(`[Migrate] Database is up to date.`);
|
|
161
|
+
return { success: true };
|
|
162
|
+
} catch (error) {
|
|
163
|
+
console.error(
|
|
164
|
+
`[Migrate] CRITICAL ERROR:`,
|
|
165
|
+
error?.response?.data?.error || error.message,
|
|
166
|
+
);
|
|
167
|
+
process.exit(1);
|
|
168
|
+
}
|
|
169
|
+
};
|