biz-a-cli 2.3.73 → 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();
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
@@ -11,7 +11,7 @@ const packageJson = require("../package.json");
11
11
  import { deploymentListenerForHubServer } from "./deployEvent.js";
12
12
  import { execCLIScriptWorker, workerStats } from "../worker/cliWorkerPool.js";
13
13
 
14
- export const IDLE_SOCKET_TIMEOUT_MILLISECONDS = 1000 * 30;
14
+ export const IDLE_SOCKET_TIMEOUT_MILLISECONDS = 1000 * 28;
15
15
  export const RECONNECT_SOCKET_DELAY = 60 * 1000;
16
16
  const DISCONNECT_REASON_BY_SOCKET_SERVER = "io server disconnect";
17
17
 
@@ -131,15 +131,37 @@ export const streamEvent = async (socket, argv) =>
131
131
  });
132
132
  };
133
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
+
134
145
  const cliReqCb = async (data, callback) => {
135
146
  const { path, method, ...remainData } = data;
136
147
 
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);
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
+ }
143
165
  };
144
166
 
145
167
  const publishReqCb = async (data, callback) => {
@@ -212,6 +234,7 @@ export const streamEvent = async (socket, argv) =>
212
234
  socket.on("incomingClient", incomingHubCb);
213
235
  socket.on("cli-req", cliReqCb);
214
236
  socket.on("publish-req", publishReqCb);
237
+ socket.on("cliCommand", (data, cb) => handleCliCommand(data, cb, argv));
215
238
 
216
239
  socket.on("disconnect", (reason) => {
217
240
  console.log(
@@ -336,6 +359,75 @@ export const status = (argv) => {
336
359
  };
337
360
  };
338
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
+
339
431
  export const clientListener = (socket, argv) => {
340
432
  socket
341
433
  .on("apiRequest", (reqData, resCB) => {
@@ -406,33 +498,6 @@ export const clientListener = (socket, argv) => {
406
498
  }
407
499
  })
408
500
  .on("cliCommand", async (data, cb) => {
409
- try {
410
- const command = data.command.trim().toLowerCase();
411
- switch (
412
- command //ensure case insensitive
413
- ) {
414
- case "status":
415
- cb(null, status(argv));
416
- break;
417
- case "runcliscript":
418
- cb(
419
- null,
420
- await execCLIScriptWorker(
421
- {
422
- url: `${argv["secure"] == true ? "https://" : "http://"}${argv["hostname"]}:${argv["port"]}`,
423
- dbindex: argv["dbindex"],
424
- subdomain: argv["subdomain"],
425
- },
426
- data.scriptName,
427
- data.scriptData,
428
- ),
429
- );
430
- break;
431
- default:
432
- cb(`Unknown CLI command '${command}'`, null);
433
- }
434
- } catch (err) {
435
- cb(err.message || err, null);
436
- }
501
+ handleCliCommand(data, cb, argv);
437
502
  });
438
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
+ };
@@ -1,42 +1,64 @@
1
- import { loadCliScript, extractFunctionScript } from "./scheduler/datalib.js";
2
-
1
+ // import { loadCliScript, extractFunctionScript } from "./scheduler/datalib.js";
2
+ import { execCLIScriptWorker } from "./worker/cliWorkerPool.js";
3
3
 
4
4
  export function getInputScript(req) {
5
- const args = req.app.settings.args;
6
- return {
7
- data: {
8
- arguments: args,
9
- body: req.body.body,
10
- path: req.path,
11
- query: req.body.query,
12
- headers: req.body.headers,
13
- socket: req.app.settings.socket,
14
- },
15
- selectedConfig: {
16
- url: `http://${args.hostname}:${args.port}`,
17
- dbindex: args.dbindex,
18
- subdomain: args.subdomain
19
- }
20
- }
5
+ const args = req.app.settings.args;
6
+ return {
7
+ data: {
8
+ arguments: args,
9
+ body: req.body.body,
10
+ path: req.path,
11
+ query: req.body.query,
12
+ headers: req.body.headers,
13
+ // socket: req.app.settings.socket, // seem never used, and it can not be serialized to worker thread
14
+ },
15
+ selectedConfig: {
16
+ url: `http://${args.hostname}:${args.port}`,
17
+ dbindex: args.dbindex,
18
+ subdomain: args.subdomain,
19
+ },
20
+ };
21
21
  }
22
22
 
23
+ // export async function runCliScript(req, res) {
24
+ // try {
25
+ // const { data, selectedConfig } = getInputScript(req);
26
+ // let script = await loadCliScript(selectedConfig, 'ID', req.body.query.scriptid);
27
+ // let functions = await extractFunctionScript(selectedConfig, script);
28
+ // if (functions) {
29
+ // console.info(`## Callback ID ${req.body.query.scriptid} Starts. ##`);
30
+
31
+ // let respon = await functions.onInit(data);
32
+ // res.send(respon);
33
+
34
+ // console.info(`## Callback ID ${req.body.query.scriptid} Finish. ##`);
35
+ // } else {
36
+ // res.send('CLI Script does not exist.');
37
+ // }
38
+ // } catch (error) {
39
+ // console.error(error);
40
+ // throw new Error(error);
41
+ // }
42
+ // }
43
+
23
44
  export async function runCliScript(req, res) {
24
- try {
25
- const { data, selectedConfig } = getInputScript(req);
26
- let script = await loadCliScript(selectedConfig, 'ID', req.body.query.scriptid);
27
- let functions = await extractFunctionScript(selectedConfig, script);
28
- if (functions) {
29
- console.info(`## Callback ID ${req.body.query.scriptid} Starts. ##`);
30
-
31
- let respon = await functions.onInit(data);
32
- res.send(respon);
33
-
34
- console.info(`## Callback ID ${req.body.query.scriptid} Finish. ##`);
35
- } else {
36
- res.send('CLI Script does not exist.');
37
- }
38
- } catch (error) {
39
- console.error(error);
40
- throw new Error(error);
41
- }
45
+ try {
46
+ const { data, selectedConfig } = getInputScript(req);
47
+
48
+ console.info(
49
+ `## Callback ID ${req.body.query.scriptid} Starts. ##`,
50
+ );
51
+
52
+ let respon = await execCLIScriptWorker(
53
+ selectedConfig,
54
+ { searchBy: "ID", searchValue: req.body.query.scriptid },
55
+ data,
56
+ );
57
+
58
+ res.send(respon);
59
+ console.info(`## Callback ID ${req.body.query.scriptid} Finish. ##`);
60
+ } catch (error) {
61
+ console.error(error);
62
+ res.status(500).send(error.message || error);
63
+ }
42
64
  }
package/db/db.js ADDED
@@ -0,0 +1,37 @@
1
+ import * as adapter from "./ds.js";
2
+
3
+ export const queryData = async (param, config = {}) => {
4
+ return await adapter.queryData(param, config);
5
+ };
6
+
7
+ /**
8
+ *
9
+ * @param {Object} payload - The data to save.
10
+ * @param {Object} [config] - Optional configuration overrides.
11
+ * @param {string} [tableName] - Optional. If provided, wraps the payload for standard ORM (sendModel).
12
+ * If omitted, sends the raw payload (for Sibling/ACID orchestration) (sendORMPayload).
13
+ */
14
+ export const save = async (payload, config = {}) => {
15
+ return await adapter.save(payload, config);
16
+ };
17
+
18
+ export const deleteMD = async (id, masterTable, detailTables, config = {}) => {
19
+ const payload = {
20
+ id: id,
21
+ master: masterTable,
22
+ detail: detailTables,
23
+ };
24
+ return await adapter.removeMD(payload, config);
25
+ };
26
+
27
+ export const remove = async (payload, config = {}) => {
28
+ return await adapter.remove(payload, config);
29
+ };
30
+
31
+ export const list = async (payload, config = {}) => {
32
+ return await adapter.list(payload, config);
33
+ };
34
+
35
+ export const executeBlock = async (sqlString, config = {}) => {
36
+ return await adapter.executeBlock(sqlString, config);
37
+ };