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 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();
@@ -1,86 +1,100 @@
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'
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 = 'clientRoom'
6
+ export const CLIENT_ROOM = "clientRoom";
7
7
 
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(()=>{qt = localhostTunnel(port, notifier)}, RECONNECT_SOCKET_DELAY);
13
- };
14
- qt.on('url', (qtUrl)=>{
15
- // console.log('qt url', qtUrl)
16
- tunnelUrl = qtUrl;
17
- })
18
- .on('connected', (conn)=>{
19
- // console.log('qt connect', conn)
20
- notifier(tunnelUrl);
21
- console.log(`${new Date()}: Connected to Direct Hub at public URL ${tunnelUrl}`)
22
- })
23
- .on('disconnected', (conn)=>{
24
- // console.log('qt disconnect', conn)
25
- notifier('');
26
- qt.stop();
27
- })
28
- .on('error', (err)=>{
29
- // console.log('qt err', err)
30
- notifier('');
31
- qt.stop();
32
- })
33
- .on('exit', (code)=>{
34
- // console.log('qt exit', code)
35
- notifier('');
36
- console.log(`${new Date()}: Direct Hub is not available, will try to reinitiate it in ${RECONNECT_SOCKET_DELAY/1000} seconds. Error code : `, code);
37
- reconnectTunnel();
38
- // })
39
- // on('stdout', (data)=>{
40
- // console.log('qt stdout', data)
41
- // })
42
- // on('stderr', (data)=>{
43
- // console.log('qt stderr', data)
44
- });
45
- return qt
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
- serverSocket.on('connection', (clientSocket) => {
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
- const setConnectListeners = (sock)=>{
52
- // shall not log in production mode
53
- if (process.env.NODE_ENV !== 'production') {
54
- const id = `<${sock.handshake?.query?.isClient ? `Client` : sock.handshake?.query?.isDeploy ? 'Deployer' : 'Anonymous'}> ${sock.id}`;
55
- console.log(id, `connected`)
56
- sock.on('disconnect', (reason)=>{
57
- console.log(id, `disconnected. Reason : ${reason}`)
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='127.0.0.1'){
77
- return new ioServer(
78
- httpServer,
79
- {
80
- cors: {origin: ['https://biz-a.id', 'https://test.biz-a.id', /\.biz-a\.id$/, 'vscode-file://vscode-app', /\.vscode-cdn\.net$/].concat((process.env.NODE_ENV === 'production') ? [] : [`http://${cliIpAddress}:4200`, 'http://localhost:4200'])},
81
- maxHttpBufferSize: 1e8, // 100 MB
82
- pingInterval: 35000, // default = 25000
83
- pingTimeout: 30000 // default = 20000
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 * 30;
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
- const result = await axios.request({
137
- method: data.method,
138
- url: `${process.env.HOST || "http://localhost"}:${argv.serverport}/cb${path || ""}`,
139
- data: remainData,
140
- });
141
- callback(result.data);
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
- try {
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
+ };