biz-a-cli 2.3.70 → 2.3.72

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
@@ -1,268 +1,671 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import yargs from "yargs"
4
- import axios from "axios"
5
- import fs from "fs"
6
- import * as tar from "tar"
7
- import { verify, sign, privateDecrypt, constants as cryptoConstants } from "node:crypto"
8
- import path, { basename } from "node:path"
9
- import { env } from "../envs/env.js"
10
- import { prepareScript, encryptScript } from "./script.js"
11
- import { spawn } from "node:child_process"
12
-
13
- const keyFolderPath = process.argv[1].substring(0, process.argv[1].lastIndexOf("\\bin")) + "\\key"
3
+ import yargs from "yargs";
4
+ import axios from "axios";
5
+ import fs from "fs";
6
+ import * as tar from "tar";
7
+ import {
8
+ verify,
9
+ sign,
10
+ privateDecrypt,
11
+ constants as cryptoConstants,
12
+ } from "node:crypto";
13
+ import path, { basename } from "node:path";
14
+ import { env } from "../envs/env.js";
15
+ import { prepareScript, encryptScript } from "./script.js";
16
+ import { spawn } from "node:child_process";
17
+
18
+ const getKeyFolderPath = () => {
19
+ const scriptPath =
20
+ typeof process.argv[1] === "string" && process.argv[1].length > 0
21
+ ? process.argv[1]
22
+ : path.join(import.meta.dirname, "app.js");
23
+ const normalizedPath = scriptPath.replaceAll("/", "\\");
24
+ const binPathPos = normalizedPath.lastIndexOf("\\bin");
25
+ if (binPathPos >= 0) {
26
+ return normalizedPath.substring(0, binPathPos) + "\\key";
27
+ }
28
+ return path.resolve(import.meta.dirname, "..", "key");
29
+ };
30
+
31
+ const keyFolderPath = getKeyFolderPath();
14
32
 
15
33
  const options = {
16
- "s": {
17
- alias: "server",
18
- describe: `API or Server URL (ex: ${env.BIZA_SERVER_LINK} or http://192.168.1.1 or https://finaapi.imamatek.com)`,
19
- type: "string",
20
- demandOption: true,
21
- default: 'http://localhost'
22
- },
23
- "i": {
24
- alias: "dbIndex",
25
- default: 2,
26
- describe: "database index",
27
- type: "number",
28
- demandOption: false
29
- },
30
- "sub": {
31
- alias: "subdomain",
32
- describe: "Subdomain",
33
- type: "string",
34
- demandOption: false
35
- },
36
- "p": {
37
- alias: "apiPort",
38
- default: 212,
39
- describe: "FINA API Port",
40
- type: "number",
41
- demandOption: false,
42
- }
43
- }
34
+ s: {
35
+ alias: "server",
36
+ describe: `API or Server URL (ex: ${env.BIZA_SERVER_LINK} or http://192.168.1.1 or https://finaapi.imamatek.com)`,
37
+ type: "string",
38
+ demandOption: true,
39
+ default: "http://localhost",
40
+ },
41
+ i: {
42
+ alias: "dbIndex",
43
+ default: 2,
44
+ describe: "database index",
45
+ type: "number",
46
+ demandOption: false,
47
+ },
48
+ sub: {
49
+ alias: "subdomain",
50
+ describe: "Subdomain",
51
+ type: "string",
52
+ demandOption: false,
53
+ },
54
+ p: {
55
+ alias: "apiPort",
56
+ default: 212,
57
+ describe: "FINA API Port",
58
+ type: "number",
59
+ demandOption: false,
60
+ },
61
+ };
44
62
 
45
63
  const addCommandOptions = {
46
- 'd': {
47
- alias: "workingDir",
48
- describe: "Path to templates directory",
49
- type: "string",
50
- demandOption: false,
51
- default: process.cwd(),
52
- },
53
- 'v': {
54
- alias: "verbose",
55
- describe: "Print info to console",
56
- type: "boolean",
57
- demandOption: false,
58
- default: false,
59
- }
60
- }
64
+ d: {
65
+ alias: "workingDir",
66
+ describe: "Path to templates directory",
67
+ type: "string",
68
+ demandOption: false,
69
+ default: process.cwd(),
70
+ },
71
+ v: {
72
+ alias: "verbose",
73
+ describe: "Print info to console",
74
+ type: "boolean",
75
+ demandOption: false,
76
+ default: false,
77
+ },
78
+ };
61
79
 
62
80
  const removeCommandOptions = {
63
- 'n': {
64
- alias: "appName",
65
- describe: "Application name",
66
- type: "string",
67
- demandOption: true,
68
- default: ""
69
- }
81
+ n: {
82
+ alias: "appName",
83
+ describe: "Application name",
84
+ type: "string",
85
+ demandOption: true,
86
+ default: "",
87
+ },
88
+ };
89
+
90
+ const prepareKeys = async () => {
91
+ const data = Buffer.from(
92
+ JSON.stringify({ issuer: "CLI", acquirer: "Client" }),
93
+ ).toString("base64");
94
+ const privateKey = fs.readFileSync(`${keyFolderPath}\\cliPrivate.pem`);
95
+ const signature = sign("sha256", data, {
96
+ key: privateKey,
97
+ passphrase: "Biz-A@cli",
98
+ padding: cryptoConstants.RSA_PKCS1_PSS_PADDING,
99
+ }).toString("base64");
100
+ const res = await axios.get(env.BIZA_SERVER_LINK + "/api/issuerKey", {
101
+ params: { data, signature },
102
+ });
103
+ if (
104
+ res.data.data != null &&
105
+ verify(
106
+ "sha256",
107
+ res.data.data,
108
+ {
109
+ key: fs.readFileSync(`${keyFolderPath}\\serverPublic.pem`),
110
+ padding: cryptoConstants.RSA_PKCS1_PSS_PADDING,
111
+ },
112
+ Buffer.from(res.data.signature, "base64"),
113
+ )
114
+ ) {
115
+ const resData = JSON.parse(
116
+ Buffer.from(res.data.data, "base64").toString(),
117
+ );
118
+ const decryptedAESKey = privateDecrypt(
119
+ {
120
+ key: privateKey,
121
+ passphrase: "Biz-A@cli",
122
+ padding: cryptoConstants.RSA_PKCS1_OAEP_PADDING,
123
+ },
124
+ Buffer.from(resData.issuer.key, "base64"),
125
+ ).toString();
126
+ const cliSignature = (signedData) =>
127
+ sign("sha256", signedData, {
128
+ key: privateKey,
129
+ passphrase: "Biz-A@cli",
130
+ padding: cryptoConstants.RSA_PKCS1_PSS_PADDING,
131
+ }).toString("base64");
132
+ const acquirerData = Buffer.from(
133
+ JSON.stringify(resData.acquirer),
134
+ ).toString("base64");
135
+ const signature = cliSignature(acquirerData);
136
+ return {
137
+ encryptKey: decryptedAESKey,
138
+ metadata: { acquirer: { data: acquirerData, signature } },
139
+ };
140
+ } else {
141
+ return null;
142
+ }
143
+ };
144
+
145
+ const compressIt = (fileName, folderPath) => {
146
+ // tar v7.1.0 -> tested 10x times, 2-3x times last compressed file have empty content
147
+ // tar v.7.4.0 -> tested 1000x times, all files have content (used app.test.js)
148
+ let compressSuccess = false;
149
+ const maxRetry = 10;
150
+ let retryCount = 0;
151
+ do {
152
+ tar.c(
153
+ {
154
+ file: fileName,
155
+ cwd: folderPath,
156
+ gzip: { level: 9 },
157
+ strict: true,
158
+ sync: true,
159
+ },
160
+ fs.readdirSync(folderPath),
161
+ );
162
+ compressSuccess = true;
163
+ tar.t({
164
+ file: fileName,
165
+ cwd: folderPath,
166
+ sync: true,
167
+ onentry: (entry) => {
168
+ if (entry.size == 0) {
169
+ compressSuccess = false;
170
+ }
171
+ },
172
+ });
173
+ if (compressSuccess == false) {
174
+ fs.unlinkSync(fileName);
175
+ retryCount++;
176
+ }
177
+ } while (compressSuccess == false && retryCount <= maxRetry);
178
+ };
179
+
180
+ const getNormalizedFileName = (file) => {
181
+ if (typeof file === "string") {
182
+ return file.trim();
183
+ }
184
+ if (file && typeof file === "object") {
185
+ const fileName = file.fileName || file.name || file.path;
186
+ return (fileName || "").toString().trim();
187
+ }
188
+ return "";
189
+ };
190
+
191
+ const normalizeFileList = (files) => {
192
+ if (Array.isArray(files)) {
193
+ return files
194
+ .map((file) => getNormalizedFileName(file))
195
+ .filter((fileName) => fileName.length > 0);
196
+ }
197
+ if (typeof files === "string") {
198
+ return files
199
+ .split(",")
200
+ .map((f) => f.trim())
201
+ .filter((f) => f.length > 0);
202
+ }
203
+ return null;
204
+ };
205
+
206
+ const normalizeAppName = (value) =>
207
+ String(value ?? "")
208
+ .trim()
209
+ .replace(/\s+/g, "")
210
+ .replace(/-/g, "")
211
+ .toLowerCase();
212
+
213
+ const normalizeBodyScripts = (body = null) => {
214
+ const scriptMap = new Map();
215
+ if (!body || typeof body !== "object") {
216
+ return scriptMap;
217
+ }
218
+
219
+ const setScript = (name, content) => {
220
+ const fileName = (name || "").toString().trim().toLowerCase();
221
+ if (fileName.length === 0) {
222
+ return;
223
+ }
224
+ if (typeof content === "string") {
225
+ scriptMap.set(fileName, content);
226
+ } else if (Buffer.isBuffer(content)) {
227
+ scriptMap.set(fileName, content.toString());
228
+ }
229
+ };
230
+
231
+ const fromObjects = (list) => {
232
+ if (!Array.isArray(list)) {
233
+ return;
234
+ }
235
+ list.forEach((item) => {
236
+ if (!item || typeof item !== "object") {
237
+ return;
238
+ }
239
+ const fileName = item.fileName || item.name || item.path;
240
+ const content = item.content ?? item.data ?? item.script;
241
+ setScript(fileName, content);
242
+ });
243
+ };
244
+
245
+ fromObjects(body.files);
246
+ fromObjects(body.fileList);
247
+
248
+ if (body.fileContents && typeof body.fileContents === "object") {
249
+ Object.entries(body.fileContents).forEach(([name, content]) =>
250
+ setScript(name, content),
251
+ );
252
+ }
253
+ if (body.scripts && typeof body.scripts === "object") {
254
+ Object.entries(body.scripts).forEach(([name, content]) =>
255
+ setScript(name, content),
256
+ );
257
+ }
258
+
259
+ return scriptMap;
260
+ };
261
+
262
+ const getFileList = ({ workingDir = process.cwd(), files = null } = {}) => {
263
+ const fileList = normalizeFileList(files);
264
+ const candidateFiles = fileList || fs.readdirSync(workingDir);
265
+
266
+ return candidateFiles.filter((fileName) => {
267
+ const normalizedName = fileName.toString().trim();
268
+ const filePath = path.isAbsolute(normalizedName)
269
+ ? normalizedName
270
+ : path.join(workingDir, normalizedName);
271
+ if (!fs.existsSync(filePath)) {
272
+ return false;
273
+ }
274
+ const stat = fs.statSync(filePath);
275
+ const baseName = path.basename(normalizedName);
276
+ return (
277
+ stat.isFile() &&
278
+ stat.size > 0 &&
279
+ (baseName
280
+ .split(".")
281
+ .pop()
282
+ .toLowerCase()
283
+ .match(/^(js)$/) ||
284
+ baseName.toLowerCase() == "menu.json")
285
+ );
286
+ });
287
+ };
288
+
289
+ const parseApplicationConfigData = (rawConfig) => {
290
+ if (rawConfig == null) {
291
+ return null;
292
+ }
293
+ if (typeof rawConfig === "object") {
294
+ return rawConfig;
295
+ }
296
+
297
+ const rawText = String(rawConfig).trim();
298
+ if (!rawText) {
299
+ return null;
300
+ }
301
+
302
+ try {
303
+ return JSON.parse(rawText);
304
+ } catch {
305
+ try {
306
+ return Function(
307
+ `${rawText}; return (typeof get === 'function') ? get() : null;`,
308
+ )();
309
+ } catch {
310
+ return null;
311
+ }
312
+ }
313
+ };
314
+
315
+ const resolveAppDisplayNameFromApplicationConfig = async ({
316
+ server,
317
+ apiPort,
318
+ dbIndex,
319
+ sub,
320
+ }) => {
321
+ if (typeof axios.request !== "function") {
322
+ return "";
323
+ }
324
+
325
+ const baseUrl = "fina/rest/TOrmMethod/%22list%22";
326
+ const url = sub
327
+ ? `${server}/hub/${baseUrl}?subdomain=${sub}`
328
+ : `${server}:${apiPort}/${baseUrl}`;
329
+
330
+ try {
331
+ const response = await axios.request({
332
+ method: "POST",
333
+ url,
334
+ headers: { "Content-Type": "text/plain" },
335
+ data: {
336
+ dbIndex,
337
+ object: {
338
+ columns: [
339
+ { title: "name", data: "SYS$CONFIG.NAME" },
340
+ { title: "data", data: "SYS$CONFIG.DATA" },
341
+ ],
342
+ },
343
+ },
344
+ });
345
+
346
+ const rawRows = response?.data?.data;
347
+ const rows = Array.isArray(rawRows)
348
+ ? rawRows
349
+ : (() => {
350
+ try {
351
+ return JSON.parse(rawRows || "[]");
352
+ } catch {
353
+ return [];
354
+ }
355
+ })();
356
+
357
+ const applicationConfigRow = rows.find((row) => {
358
+ const name = String(
359
+ row?.["SYS$CONFIG.NAME"] ?? row?.NAME ?? row?.name ?? "",
360
+ )
361
+ .trim()
362
+ .toUpperCase();
363
+ return name === "APPLICATION_CONFIG";
364
+ });
365
+ if (!applicationConfigRow) {
366
+ return "";
367
+ }
368
+
369
+ const applicationConfig = parseApplicationConfigData(
370
+ applicationConfigRow?.["SYS$CONFIG.DATA"] ??
371
+ applicationConfigRow?.DATA ??
372
+ applicationConfigRow?.data,
373
+ );
374
+ const displayName = applicationConfig?.metadata?.name;
375
+ return typeof displayName === "string" ? displayName.trim() : "";
376
+ } catch {
377
+ return "";
378
+ }
379
+ };
380
+
381
+ async function addApp({
382
+ workingDir = process.cwd(),
383
+ verbose = false,
384
+ server = "http://localhost",
385
+ apiPort = 212,
386
+ dbIndex = 2,
387
+ sub,
388
+ files = null,
389
+ body = null,
390
+ } = {}) {
391
+ const oldCwd = process.cwd();
392
+
393
+ /*
394
+ hex => 2 char = 1 bytes => can be encrypted
395
+ base64 => 4 char = 3 bytes => can be encrypted. Smaller size compare to Hex
396
+ utf8 => 1 char = 1 - 4 bytes => can not be encrypted, encryption need precise bytes per Character. Smallest Size compare to Hex and base64
397
+ */
398
+ try {
399
+ process.chdir(path.resolve(workingDir));
400
+
401
+ const bundlingStart = performance.now();
402
+ const rootFolder = "./upload/";
403
+ const bundleName = normalizeAppName(basename(process.cwd()));
404
+ const bundleFolder = rootFolder + bundleName + "/";
405
+ const sourceFiles = getFileList({ workingDir: process.cwd(), files });
406
+ const bodyScripts = normalizeBodyScripts(body);
407
+ const requestedFiles = normalizeFileList(files);
408
+ const bodySourceFiles = Array.from(bodyScripts.keys()).filter(
409
+ (fileName) => {
410
+ const isSupportedFile =
411
+ fileName.match(/\.js$/) || fileName === "menu.json";
412
+ if (!isSupportedFile) {
413
+ return false;
414
+ }
415
+ if (!requestedFiles) {
416
+ return true;
417
+ }
418
+ return requestedFiles.some(
419
+ (requested) =>
420
+ path.basename(requested).toLowerCase() === fileName,
421
+ );
422
+ },
423
+ );
424
+ const mergedSourceFiles = Array.from(
425
+ new Set([...sourceFiles, ...bodySourceFiles]),
426
+ );
427
+
428
+ if (mergedSourceFiles.length > 0) {
429
+ const keys = await prepareKeys();
430
+ if (!keys) {
431
+ const msg = "Can not prepare encryption keys";
432
+ console.error(msg);
433
+ return { success: false, error: msg };
434
+ }
435
+
436
+ let processedFile = 0;
437
+ fs.rmSync(rootFolder, { force: true, recursive: true });
438
+ fs.mkdirSync(bundleFolder, { recursive: true });
439
+
440
+ for (const sourceFile of mergedSourceFiles) {
441
+ const sourceFilePath = path.isAbsolute(sourceFile)
442
+ ? sourceFile
443
+ : path.resolve(process.cwd(), sourceFile);
444
+ const fileName = path.basename(sourceFile).toLowerCase();
445
+ const scriptSource = bodyScripts.has(fileName)
446
+ ? bodyScripts.get(fileName)
447
+ : fs.readFileSync(sourceFilePath).toString();
448
+ const preparedScript = await prepareScript(
449
+ fileName,
450
+ scriptSource,
451
+ verbose,
452
+ );
453
+ const encryptedScript = encryptScript(
454
+ preparedScript,
455
+ keys.encryptKey,
456
+ );
457
+ if (fileName == "menu.json") {
458
+ keys.metadata["menu"] = encryptedScript.toString("base64");
459
+ } else {
460
+ fs.writeFileSync(
461
+ bundleFolder + fileName,
462
+ encryptedScript.toString("base64"),
463
+ );
464
+ }
465
+
466
+ processedFile++;
467
+ }
468
+ const bundleFile = `${rootFolder}${bundleName}.tgz`;
469
+ compressIt(bundleFile, bundleFolder);
470
+ console.log(
471
+ `Finished packing ${processedFile} files into "${bundleFile}" (${((performance.now() - bundlingStart) / 1000).toFixed(2)}s)`,
472
+ );
473
+
474
+ // send to API
475
+ const uploadingStart = performance.now();
476
+ const data = fs.readFileSync(bundleFile).toString("base64"); // *.tgz to base64String
477
+ const baseUrl = "fina/rest/TOrmMethod/%22setApp%22";
478
+
479
+ const url = sub
480
+ ? `${server}/hub/${baseUrl}?subdomain=${sub}`
481
+ : `${server}:${apiPort}/${baseUrl}`;
482
+
483
+ const headers = { "Content-Type": "text/plain" };
484
+ const appDisplayName =
485
+ await resolveAppDisplayNameFromApplicationConfig({
486
+ server,
487
+ apiPort,
488
+ dbIndex,
489
+ sub,
490
+ });
491
+ const useApplicationConfigName = body != null;
492
+ if (useApplicationConfigName && appDisplayName.length > 0) {
493
+ keys.metadata["name"] = appDisplayName;
494
+ }
495
+ const uploadAppName = normalizeAppName(
496
+ (useApplicationConfigName ? appDisplayName : "") || bundleName,
497
+ );
498
+ const param = {
499
+ _parameters: [dbIndex, uploadAppName, data, keys.metadata],
500
+ };
501
+
502
+ const res = await axios.post(url, param, { headers: headers });
503
+ if (res.data.success) {
504
+ console.log(
505
+ `Finished uploading "${bundleFile}" (${((performance.now() - uploadingStart) / 1000).toFixed(2)}s)`,
506
+ );
507
+ fs.rmSync(rootFolder, { force: true, recursive: true });
508
+ return { success: true, data: res.data };
509
+ }
510
+
511
+ console.error(res.data.error);
512
+ return { success: false, error: res.data.error, data: res.data };
513
+ }
514
+
515
+ const msg = "Nothing to upload. Please recheck your app folder.";
516
+ console.error(msg);
517
+ return { success: false, error: msg };
518
+ } catch (e) {
519
+ const errMsg = e.response?.data ? e.response.data : e;
520
+ console.error(errMsg);
521
+ return { success: false, error: errMsg };
522
+ } finally {
523
+ process.chdir(oldCwd);
524
+ }
70
525
  }
71
526
 
72
- Object
73
- .keys(options)
74
- .reduce((app, optKey) => app = app.option(optKey, options[optKey]), yargs(process.argv.slice(2)))
75
- .command('add', 'Add Biz-A Application', addCommandOptions, async (options) => {
76
-
77
- const prepareKeys = async () => {
78
- const data = Buffer.from(JSON.stringify({ issuer: 'CLI', acquirer: 'Client' })).toString('base64')
79
- const privateKey = fs.readFileSync(`${keyFolderPath}\\cliPrivate.pem`)
80
- const signature = sign('sha256', data, { key: privateKey, passphrase: 'Biz-A@cli', padding: cryptoConstants.RSA_PKCS1_PSS_PADDING }).toString('base64')
81
- const res = await axios.get(env.BIZA_SERVER_LINK + '/api/issuerKey', { params: { data, signature } })
82
- if ((res.data.data != null) && verify('sha256', res.data.data, { key: fs.readFileSync(`${keyFolderPath}\\serverPublic.pem`), padding: cryptoConstants.RSA_PKCS1_PSS_PADDING }, Buffer.from(res.data.signature, 'base64'))) {
83
- const resData = JSON.parse(Buffer.from(res.data.data, 'base64').toString())
84
- const decryptedAESKey = privateDecrypt({ key: privateKey, passphrase: 'Biz-A@cli', padding: cryptoConstants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(resData.issuer.key, 'base64')).toString()
85
- const cliSignature = (signedData) => sign('sha256', signedData, { key: privateKey, passphrase: 'Biz-A@cli', padding: cryptoConstants.RSA_PKCS1_PSS_PADDING }).toString('base64')
86
- const acquirerData = Buffer.from(JSON.stringify(resData.acquirer)).toString('base64')
87
- const signature = cliSignature(acquirerData)
88
- return { encryptKey: decryptedAESKey, metadata: { acquirer: { data: acquirerData, signature } } }
89
- }
90
- else {
91
- return null
92
- }
93
- }
94
-
95
- const compressIt = (fileName, folderPath) => {
96
- // tar v7.1.0 -> tested 10x times, 2-3x times last compressed file have empty content
97
- // tar v.7.4.0 -> tested 1000x times, all files have content (used app.test.js)
98
- let compressSuccess = false
99
- const maxRetry = 10
100
- let retryCount = 0
101
- do {
102
- tar.c({ file: fileName, cwd: folderPath, gzip: { level: 9 }, strict: true, sync: true }, fs.readdirSync(folderPath))
103
- compressSuccess = true
104
- tar.t({
105
- file: fileName, cwd: folderPath, sync: true, onentry: (entry) => {
106
- if (entry.size == 0) {
107
- compressSuccess = false
108
- }
109
- }
110
- })
111
- if (compressSuccess == false) {
112
- fs.unlinkSync(fileName)
113
- retryCount++
114
- }
115
- }
116
- while ((compressSuccess == false) && (retryCount <= maxRetry))
117
- }
118
-
119
- const getFileList = () => {
120
- return (fs.readdirSync(process.cwd())).filter((fileName) => {
121
- const stat = fs.statSync(fileName)
122
- return stat.isFile() && (stat.size > 0) && ((fileName.split('.').pop().toLowerCase().match(/^(js)$/) || (fileName.toLowerCase() == 'menu.json')))
123
- })
124
- }
125
-
126
- async function runUnitTests() { //SCY BZ 4331
127
- let jestCommand = ['--no-install', 'jest', '--json'];
128
- let output = '';
129
-
130
- const testDir = path.join(options.workingDir, "test");
131
- const workDir = path.resolve(options.workingDir);
132
- try {
133
- process.chdir(testDir)
134
- } catch (error) {
135
- if (error.code === 'ENOENT') {
136
- jestCommand.push('--passWithNoTests')
137
- }
138
- }
139
- process.chdir(workDir);
140
-
141
- const child = process.platform === 'win32'
142
- ? spawn('cmd.exe', ['/d', '/s', '/c', 'npx', ...jestCommand])
143
- : spawn('npx', jestCommand)
144
- const collectOutput = data => {
145
- output += data?.toString() || ''
146
- }
147
- child.stderr?.on('data', collectOutput)
148
- child.stdout?.on('data', () => { }) // SCY BZ 4363, ref : https://nodejs.org/download/release/v22.19.0/docs/api/child_process.html
149
-
150
- child.on('error', e => {
151
- output += e?.message ? `\n${e.message}` : ''
152
- });
153
- child.on('close', async code => {
154
- console.log('====================');
155
- console.log(output);
156
- console.log('====================');
157
- let noTestsFound = output.includes('No tests found');
158
- let missingJest = (output.includes('missing packages') && output.includes('jest'));
159
- let passWithNoTests = jestCommand.includes('--passWithNoTests');
160
-
161
- if ((code == 0) ||
162
- ((code == 1) && noTestsFound) ||
163
- ((code == 1) && missingJest && passWithNoTests)) {
164
- await addApp()
165
- } else {
166
- console.error('Biz-A Add aborted');
167
- }
168
- })
169
- }
170
-
171
- async function addApp() {
172
- /*
173
- hex => 2 char = 1 bytes => can be encrypted
174
- base64 => 4 char = 3 bytes => can be encrypted. Smaller size compare to Hex
175
- utf8 => 1 char = 1 - 4 bytes => can not be encrypted, encryption need precise bytes per Character. Smallest Size compare to Hex and base64
176
- */
177
- try {
178
- const bundlingStart = performance.now()
179
- const rootFolder = './upload/'
180
- const bundleName = basename(process.cwd()).trim().replace(' ', '').toLowerCase()
181
- const bundleFolder = rootFolder + bundleName + '/'
182
- const files = getFileList()
183
- if (files.length > 0) {
184
- const keys = await prepareKeys()
185
- let processedFile = 0
186
- fs.rmSync(rootFolder, { force: true, recursive: true })
187
- fs.mkdirSync(bundleFolder, { recursive: true })
188
- for (const file of files) {
189
- const fileName = file.toLowerCase();
190
- const encryptedScript = encryptScript(await prepareScript(fileName, fs.readFileSync(fileName).toString(), options.verbose), keys.encryptKey)
191
- if (fileName == 'menu.json') {
192
- keys.metadata['menu'] = encryptedScript.toString('base64')
193
- } else {
194
- fs.writeFileSync(bundleFolder + fileName, encryptedScript.toString('base64'))
195
- }
196
-
197
- processedFile++
198
- }
199
- const bundleFile = `${rootFolder}${bundleName}.tgz`
200
- compressIt(bundleFile, bundleFolder)
201
- console.log(`Finished packing ${processedFile} files into "${bundleFile}" (${((performance.now() - bundlingStart) / 1000).toFixed(2)}s)`)
202
-
203
- // send to API
204
- const uploadingStart = performance.now()
205
- const data = (fs.readFileSync(bundleFile)).toString('base64') // *.tgz to base64String
206
- const baseUrl = 'fina/rest/TOrmMethod/%22setApp%22'
207
- const url = options.sub ?
208
- `${options.server}/hub/${baseUrl}?subdomain=${options.sub}` :
209
- `${options.server}:${options.apiPort}/${baseUrl}`
210
- const headers = { 'Content-Type': 'text/plain' }
211
- const param = { _parameters: [options.dbIndex, bundleName, data, keys.metadata] }
212
- const res = await axios.post(url, param, { headers: headers });
213
- if (res.data.success) {
214
- console.log(`Finished uploading "${bundleFile}" (${((performance.now() - uploadingStart) / 1000).toFixed(2)}s)`)
215
- fs.rmSync(rootFolder, { force: true, recursive: true })
216
- } else {
217
- console.error(res.data.error)
218
- }
219
- } else {
220
- console.error('Nothing to upload. Please recheck your app folder.')
221
- }
222
- } catch (e) {
223
- console.error(e.response?.data ? e.response.data : e)
224
- }
225
- }
226
-
227
- await runUnitTests()
228
- })
229
- .command('remove', 'Remove Biz-A Application', removeCommandOptions, (options) => {
230
- (async () => {
231
- try {
232
- const baseUrl = 'fina/rest/TOrmMethod/%22deleteApp%22'
233
- const url = options.sub ?
234
- `${options.server}/hub/${baseUrl}?subdomain=${options.sub}` :
235
- `${options.server}:${options.apiPort}/${baseUrl}`
236
- const headers = { 'Content-Type': 'text/plain' }
237
- const deleteApps = options.appName.trim().replaceAll(' ', '').toLowerCase()
238
- const param = { _parameters: [options.dbIndex, deleteApps] }
239
- const res = await axios.post(url, param, { headers: headers });
240
- if (res.data?.success) {
241
- if (deleteApps == '') {
242
- console.log('All apps removed')
243
- } else {
244
- const failedList = (res.data._f && (typeof res.data._f == 'string')) ? res.data._f.trim().replaceAll(' ', '').toLowerCase().split(',') : []
245
- const removeList = deleteApps.split(',')
246
- removeList.forEach((app) => {
247
- console.log(`${app} ${failedList.indexOf(app) == -1 ? 'removed' : 'not found'}`)
248
- })
249
- }
250
- } else {
251
- console.error(res.data.error)
252
- }
253
- return res
254
- } catch (e) {
255
- const errMsg = (e.response?.data ? e.response.data : e)
256
- console.error(errMsg)
257
- return errMsg
258
- }
259
- })()
260
- })
261
- .recommendCommands()
262
- .demandCommand(1, 'You need at least one command before moving on')
263
- .strict()
264
- .parse();
265
-
266
- export { options, addCommandOptions, removeCommandOptions }
527
+ async function runUnitTests(options) {
528
+ //SCY BZ 4331
529
+ return new Promise((resolve) => {
530
+ let jestCommand = ["--no-install", "jest", "--json"];
531
+ let output = "";
532
+
533
+ const testDir = path.join(options.workingDir, "test");
534
+ const workDir = path.resolve(options.workingDir);
535
+ try {
536
+ process.chdir(testDir);
537
+ } catch (error) {
538
+ if (error.code === "ENOENT") {
539
+ jestCommand.push("--passWithNoTests");
540
+ }
541
+ }
542
+ process.chdir(workDir);
543
+
544
+ const child =
545
+ process.platform === "win32"
546
+ ? spawn("cmd.exe", ["/d", "/s", "/c", "npx", ...jestCommand])
547
+ : spawn("npx", jestCommand);
548
+ const collectOutput = (data) => {
549
+ output += data?.toString() || "";
550
+ };
551
+ child.stderr?.on("data", collectOutput);
552
+ child.stdout?.on("data", () => {}); // SCY BZ 4363, ref : https://nodejs.org/download/release/v22.19.0/docs/api/child_process.html
553
+
554
+ child.on("error", (e) => {
555
+ output += e?.message ? `\n${e.message}` : "";
556
+ });
267
557
 
558
+ child.on("close", async (code) => {
559
+ console.log("====================");
560
+ console.log(output);
561
+ console.log("====================");
562
+ const noTestsFound = output.includes("No tests found");
563
+ const missingJest =
564
+ output.includes("missing packages") && output.includes("jest");
565
+ const passWithNoTests = jestCommand.includes("--passWithNoTests");
566
+
567
+ if (
568
+ code == 0 ||
569
+ (code == 1 && noTestsFound) ||
570
+ (code == 1 && missingJest && passWithNoTests)
571
+ ) {
572
+ await addApp({
573
+ workingDir: workDir,
574
+ verbose: options.verbose,
575
+ server: options.server,
576
+ apiPort: options.apiPort,
577
+ dbIndex: options.dbIndex,
578
+ sub: options.sub,
579
+ files: options.files,
580
+ });
581
+ } else {
582
+ console.error("Biz-A Add aborted");
583
+ }
584
+ resolve();
585
+ });
586
+ });
587
+ }
588
+
589
+ const buildCli = () =>
590
+ Object.keys(options)
591
+ .reduce(
592
+ (app, optKey) => (app = app.option(optKey, options[optKey])),
593
+ yargs(process.argv.slice(2)),
594
+ )
595
+ .command(
596
+ "add",
597
+ "Add Biz-A Application",
598
+ addCommandOptions,
599
+ async (commandOptions) => {
600
+ await runUnitTests(commandOptions);
601
+ },
602
+ )
603
+ .command(
604
+ "remove",
605
+ "Remove Biz-A Application",
606
+ removeCommandOptions,
607
+ (commandOptions) => {
608
+ (async () => {
609
+ try {
610
+ const baseUrl = "fina/rest/TOrmMethod/%22deleteApp%22";
611
+ const url = commandOptions.sub
612
+ ? `${commandOptions.server}/hub/${baseUrl}?subdomain=${commandOptions.sub}`
613
+ : `${commandOptions.server}:${commandOptions.apiPort}/${baseUrl}`;
614
+ const headers = { "Content-Type": "text/plain" };
615
+ const deleteApps = commandOptions.appName
616
+ .trim()
617
+ .replaceAll(" ", "")
618
+ .toLowerCase();
619
+ const param = {
620
+ _parameters: [commandOptions.dbIndex, deleteApps],
621
+ };
622
+ const res = await axios.post(url, param, {
623
+ headers: headers,
624
+ });
625
+ if (res.data?.success) {
626
+ if (deleteApps == "") {
627
+ console.log("All apps removed");
628
+ } else {
629
+ const failedList =
630
+ res.data._f &&
631
+ typeof res.data._f == "string"
632
+ ? res.data._f
633
+ .trim()
634
+ .replaceAll(" ", "")
635
+ .toLowerCase()
636
+ .split(",")
637
+ : [];
638
+ const removeList = deleteApps.split(",");
639
+ removeList.forEach((app) => {
640
+ console.log(
641
+ `${app} ${failedList.indexOf(app) == -1 ? "removed" : "not found"}`,
642
+ );
643
+ });
644
+ }
645
+ } else {
646
+ console.error(res.data.error);
647
+ }
648
+ return res;
649
+ } catch (e) {
650
+ const errMsg = e.response?.data ? e.response.data : e;
651
+ console.error(errMsg);
652
+ return errMsg;
653
+ }
654
+ })();
655
+ },
656
+ )
657
+ .recommendCommands()
658
+ .demandCommand(1, "You need at least one command before moving on")
659
+ .strict();
660
+
661
+ if (process.env.BIZA_APP_SKIP_PARSE !== "1") {
662
+ buildCli().parse();
663
+ }
268
664
 
665
+ export {
666
+ options,
667
+ addCommandOptions,
668
+ removeCommandOptions,
669
+ getFileList,
670
+ addApp,
671
+ };