biz-a-cli 2.3.32 → 2.3.34

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/mailController.js CHANGED
@@ -6,19 +6,12 @@ function createEmailTransport(req, config) {
6
6
  );
7
7
  }
8
8
 
9
- export function sendMailWatcher(req) {
9
+ export async function sendMailWatcher(req) {
10
10
  try {
11
- let config = req.body.config;
12
- const transporter = createEmailTransport(req, config);
13
- transporter.sendMail(req.body.mailOptions, (err, info) => {
14
- if (err) {
15
- console.log(err.message);
16
- return err.message;
17
- } else {
18
- console.log('Info:' + info);
19
- return info;
20
- }
21
- })
11
+ const transporter = createEmailTransport(req, req.body.config);
12
+ const sendResult = await transporter.sendMail(req.body.mailOptions);
13
+
14
+ return sendResult;
22
15
  } catch (err) {
23
16
  console.log('Error: ' + err.message);
24
17
  return err.message;
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "biz-a-cli",
3
3
  "nameDev": "biz-a-cli-dev",
4
- "version": "2.3.32",
4
+ "version": "2.3.34",
5
5
  "versionDev": "0.0.30",
6
6
  "description": "",
7
7
  "main": "bin/index.js",
8
8
  "type": "module",
9
9
  "engines": {
10
- "node": "20.12.1",
11
- "npm": "10.5.0"
10
+ "node": "20.16.0",
11
+ "npm": "10.8.1"
12
12
  },
13
13
  "scripts": {
14
14
  "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch a",
@@ -36,29 +36,26 @@ if (process.env.NODE_ENV !== 'production') {
36
36
  // BIZA_SERVER_LINK = env.BIZA_SERVER_LINK;
37
37
  // };
38
38
 
39
- function mapData2Key(res, cols) {
39
+ export function mapData2Key(res, cols) {
40
40
  return res.map(e => {
41
41
  return cols.reduce((o, k) => {
42
+ // o[k.key ?? k.data] = e[k.data]
42
43
  o[k.key ? k.key : k.data] = e[k.data]
43
44
  return o;
44
45
  }, {})
45
46
  })
46
47
  }
47
48
 
48
- function getUrlApi(config, methodName, isPost = true) {
49
+ export function getUrlApi(config, methodName, isPost = true) {
49
50
  return `${config.url}/fina/rest/TOrmMethod/` +
50
51
  `${isPost ? '%22' : ''}${methodName}${isPost ? '%22' : ''}`
51
52
  }
52
53
 
53
- function json2Parameters(obj) {
54
- return JSON.stringify(
55
- {
56
- _parameters: obj.map((el) => JSON.stringify(el))
57
- }
58
- );
54
+ export function json2Parameters(obj) {
55
+ return JSON.stringify({ _parameters: obj.map((el) => JSON.stringify(el)) });
59
56
  }
60
57
 
61
- function getUrlAndParam(config, method, obj, path) {
58
+ export function getUrlAndParam(config, method, obj, path) {
62
59
  return {
63
60
  url: getUrlApi(config, path),
64
61
  params: json2Parameters([
@@ -71,12 +68,38 @@ function getUrlAndParam(config, method, obj, path) {
71
68
  }
72
69
  }
73
70
 
74
- function getTableObj(model, tableName) {
71
+ export function getTableObj(model, tableName) {
75
72
  let obj = {};
76
73
  obj[tableName] = model;
77
74
  return obj;
78
75
  }
79
76
 
77
+ export function options(apiConfig) {
78
+ return {
79
+ headers: { 'content-type': 'text/plain' },
80
+ params: { subdomain: apiConfig.subdomain }
81
+ };
82
+ }
83
+
84
+ export async function json2fina(obj, apiConfig, raiseError = false) {
85
+ const url = getUrlApi(
86
+ {
87
+ url: apiConfig.url,
88
+ dbIndex: obj.dbIndex,
89
+ methodClass: 'TFinaMethod'
90
+ },
91
+ 'json2fina'
92
+ );
93
+
94
+ const params = json2Parameters([obj]);
95
+ const res = await axios.post(url, params, options(apiConfig));
96
+
97
+ if (res.data?.error && raiseError) {
98
+ throw new Error(res.data.error);
99
+ }
100
+ return res;
101
+ }
102
+
80
103
  export function sendModel(model, apiConfig, tableName) {
81
104
  const { url, params } = getUrlAndParam(
82
105
  apiConfig,
@@ -85,12 +108,7 @@ export function sendModel(model, apiConfig, tableName) {
85
108
  'orm'
86
109
  );
87
110
 
88
- const options = {
89
- headers: { 'content-type': 'text/plain' },
90
- params: { subdomain: apiConfig.subdomain }
91
- }
92
-
93
- return axios.post(url, params, options)
111
+ return axios.post(url, params, options(apiConfig))
94
112
  }
95
113
 
96
114
  export async function queryData(param, apiConfig, mapField) {
@@ -105,12 +123,7 @@ export async function queryData(param, apiConfig, mapField) {
105
123
  'list'
106
124
  );
107
125
 
108
- const options = {
109
- headers: { 'content-type': 'text/plain' },
110
- params: { subdomain: apiConfig.subdomain }
111
- }
112
-
113
- let res = await axios.post(url, params, options);
126
+ let res = await axios.post(url, params, options(apiConfig));
114
127
  res = res.data?.data ? JSON.parse(res.data.data) : res.data;
115
128
  return (!mapField || res.error) ? res : mapData2Key(res, param.columns);
116
129
  }
@@ -123,65 +136,80 @@ export async function crudData(param, apiConfig, method, path) {
123
136
  path
124
137
  );
125
138
 
126
- const options = {
127
- headers: { 'content-type': 'text/plain' },
128
- params: { subdomain: apiConfig.subdomain }
129
- }
130
-
131
- const res = await axios.post(url, params, options);
139
+ const res = await axios.post(url, params, options(apiConfig));
132
140
  return res.data?.data ? JSON.parse(res.data.data) : res.data;
133
141
  }
134
142
 
135
143
  export async function genId(apiConfig, genName) {
136
144
  const url = `${getUrlApi(apiConfig, 'genId', false)}/${genName}/${apiConfig.dbindex}`;
137
- const options = {
138
- headers: { 'content-type': 'text/plain' },
139
- params: { subdomain: apiConfig.subdomain }
140
- }
141
- const res = await axios.get(url, options);
145
+
146
+ const res = await axios.get(url, options(apiConfig));
142
147
  return res.data?.data ? JSON.parse(res.data.data) : res.data;
143
148
  }
144
149
 
145
- export function extractFunctionScript(data) {
150
+ function runScriptInThisContext(script) {
151
+ return runInThisContext(script, { importModuleDynamically: constants.USE_MAIN_CONTEXT_DEFAULT_LOADER });
152
+ }
153
+
154
+ export async function setLibrary(config, libraries) {
155
+ let libs = {};
156
+ for (const lib of libraries) {
157
+ const data = await loadCliScript(config, 'SCRIPT_NAME', lib);
158
+ const libFn = runScriptInThisContext(data[0].script);
159
+ Object.assign(libs, { lib: libFn().functions });
160
+ }
161
+
162
+ return libs;
163
+ }
164
+
165
+ export async function extractFunctionScript(selectedConfig, data) {
166
+ const config = getConfig(selectedConfig);
167
+
146
168
  if (data.error) {
147
169
  console.log(data.error, 'error')
148
170
  return
149
171
  }
150
172
  if (data.length == 0) return
151
173
 
152
- const scriptFn = runInThisContext(data[0].script, { importModuleDynamically: constants.USE_MAIN_CONTEXT_DEFAULT_LOADER });
174
+ const scriptFn = runScriptInThisContext(data[0].script);
153
175
  if (!scriptFn().functions) return
176
+
154
177
  let lib = {
155
178
  axios: axios.default,
156
179
  dayjs: dayjs.default,
157
180
  crypto: crypto,
158
- log: logger,
181
+ log: logger
182
+ }
183
+
184
+ if (scriptFn().functions.useLibrary) {
185
+ Object.assign(lib, await setLibrary(config, scriptFn().functions.useLibrary()));
159
186
  }
160
187
  return scriptFn(lib).functions;
161
188
  }
162
189
 
163
- export function getQueryDataObsParameters(trigger) {
190
+ export function getQueryDataPrmsParameters(column, value) {
164
191
  return {
165
192
  length: 1,
166
- filter: [{ junction: '', column: 'ID', operator: '=', value1: `${trigger.scriptid}` }],
193
+ filter: [{ junction: '', column: column, operator: '=', value1: `'${value}'` }],
167
194
  columns: [{ data: "SYS$CLI_SCRIPT.SCRIPT", key: 'script' }]
168
195
  }
169
196
  }
170
197
 
171
- export function queryDataPromise(config, trigger) {
172
- let param = getQueryDataObsParameters(trigger);
198
+ export async function queryDataPromise(config, column, value) {
199
+ let param = getQueryDataPrmsParameters(column, value);
200
+ // let param = getQueryDataPrmsParameters(d);
173
201
 
174
- return queryData(param, config, true);
202
+ return await queryData(param, config, true);
175
203
  }
176
204
 
177
205
  export function getConfig(config) {
178
206
  return (Array.isArray(config)) ? config[0] : config;
179
207
  }
180
208
 
181
- export function loadCliScript(selectedConfig, trigger) {
209
+ export async function loadCliScript(selectedConfig, column, value) {
182
210
  const config = getConfig(selectedConfig);
183
211
 
184
- return queryDataPromise(config, trigger);
212
+ return await queryDataPromise(config, column, value);
185
213
  }
186
214
 
187
215
  export function getInputData(config, trigger) {
@@ -195,7 +223,7 @@ export function getInputData(config, trigger) {
195
223
  subdomain: config.subdomain,
196
224
  smtp: {
197
225
  user: config.smtp.auth.user,
198
- pass:config.smtp.auth.pass,
226
+ pass: config.smtp.auth.pass,
199
227
  host: config.smtp.host,
200
228
  port: config.smtp.port,
201
229
  secure: config.smtp.secure
@@ -207,7 +235,8 @@ export function getInputData(config, trigger) {
207
235
 
208
236
  export async function scheduleSubscription(config, data, trigger, needSetHistory, isTest = false) {
209
237
  try {
210
- let functions = extractFunctionScript(data);
238
+ let functions = extractFunctionScript(config, data);
239
+
211
240
  if (functions) {
212
241
  if (!isTest) { // next, change isTest to test better
213
242
  functions.onInit(getInputData(config, trigger));
@@ -230,7 +259,7 @@ export async function scheduleSubscription(config, data, trigger, needSetHistory
230
259
 
231
260
  export async function checkSchedule(selectedConfig, trigger, needSetHistory) {
232
261
  try {
233
- const data = await loadCliScript(selectedConfig, trigger);
262
+ const data = await loadCliScript(selectedConfig, 'ID', trigger.scriptid);
234
263
  await scheduleSubscription(selectedConfig, data, trigger, needSetHistory);
235
264
  } catch (error) {
236
265
  console.error(error);
package/tests/app.test.js CHANGED
@@ -294,7 +294,7 @@ describe('Biz-A Apps CLI', ()=>{
294
294
  expect(logSpy.mock.calls[0][0]).toBe('===================\nA.JS\n===================')
295
295
 
296
296
  expect(errorSpy.mock.calls.length).toBe(1)
297
- expect(errorSpy.mock.calls[0][0]).toBe('a.js : SyntaxError: Unexpected token: eof, expected: punc «}»')
297
+ expect(errorSpy.mock.calls[0][0]).toStrictEqual({e: 'a.js:1:38: SyntaxError: Unexpected token: eof, expected: punc «}»'})
298
298
  }
299
299
  ],
300
300
  [
@@ -305,11 +305,11 @@ describe('Biz-A Apps CLI', ()=>{
305
305
  expect(logSpy.mock.calls[0][0]).toBe('===================\nA.JS\n===================')
306
306
  expect(logSpy.mock.calls[1][0]).toBe('Minify : \nget=function(){return{modelA:{}}};modul.expor=get;')
307
307
  expect(logSpy.mock.calls[2][0]).toBe('Running script with VM') // node sandbox (VN) error as console.log
308
- expect(logSpy.mock.calls[3][0]).toBe('a.js : ReferenceError: modul is not defined')
308
+ expect(logSpy.mock.calls[3][0]).toBe("a.js : ReferenceError: modul is not defined")
309
309
  expect(logSpy.mock.calls[4][0]).toBe('Running script with Import function')
310
310
 
311
311
  expect(errorSpy.mock.calls.length).toBe(1)
312
- expect(errorSpy.mock.calls[0][0]).toBe("a.js : ReferenceError: modul is not defined") // ES6 import error
312
+ expect(errorSpy.mock.calls[0][0]).toStrictEqual({e: 'a.js : ReferenceError: modul is not defined'}) // ES6 import error
313
313
  }
314
314
  ],
315
315
  [
@@ -323,7 +323,7 @@ describe('Biz-A Apps CLI', ()=>{
323
323
  expect(logSpy.mock.calls[3][0]).toBe('Running script with Import function')
324
324
 
325
325
  expect(errorSpy.mock.calls.length).toBe(1)
326
- expect(errorSpy.mock.calls[0][0]).toBe('a.js : Failed to compile template script.\nPlease make sure the script is correct and not returning empty result')
326
+ expect(errorSpy.mock.calls[0][0]).toStrictEqual({e: 'a.js : Failed to compile template script.\nPlease make sure the script is correct and not returning empty result'})
327
327
  }
328
328
  ],
329
329
  ]
@@ -2,14 +2,27 @@ import { jest } from '@jest/globals'
2
2
 
3
3
  const mockInsertHistory = jest.fn();
4
4
  jest.unstable_mockModule("../scheduler/watcherController.js", () => ({
5
- insertHistory: mockInsertHistory.mockResolvedValue('OK')
5
+ insertHistory: mockInsertHistory.mockResolvedValue('OK'),
6
6
  }))
7
7
 
8
+ jest.unstable_mockModule('axios', () => { return { default: jest.fn() } })
9
+ let axios = (await import('axios')).default
10
+
11
+ axios.get = jest.fn()
12
+ axios.post = jest.fn()
13
+
8
14
  const {
9
15
  scheduleSubscription,
10
16
  getConfig,
11
17
  getInputData,
12
- getQueryDataObsParameters,
18
+ getQueryDataPrmsParameters,
19
+ mapData2Key,
20
+ getUrlApi,
21
+ json2Parameters,
22
+ getUrlAndParam,
23
+ getTableObj,
24
+ options,
25
+ setLibrary
13
26
  } = await import('../scheduler/datalib.js');
14
27
 
15
28
  describe('data test', () => {
@@ -135,16 +148,201 @@ describe('data test', () => {
135
148
  });
136
149
 
137
150
  test('get query data parameter', () => {
138
- const trigger = {
139
- scriptid: 12
140
- }
141
-
142
- expect(getQueryDataObsParameters(trigger)).toStrictEqual({
151
+ expect(getQueryDataPrmsParameters('ID', 12)).toStrictEqual({
143
152
  length: 1,
144
- filter: [{ junction: '', column: 'ID', operator: '=', value1: '12' }],
153
+ filter: [{ junction: '', column: 'ID', operator: '=', value1: "'12'" }],
145
154
  columns: [{ data: "SYS$CLI_SCRIPT.SCRIPT", key: 'script' }]
146
155
  });
147
156
  });
148
157
 
158
+ test('should map data correctly when key is provided', () => {
159
+ const res = [
160
+ { id: 1, name: 'Alice' },
161
+ { id: 2, name: 'Bob' },
162
+ ];
163
+ const cols = [
164
+ { key: 'identifier', data: 'id' },
165
+ { key: 'fullName', data: 'name' },
166
+ ];
167
+
168
+ const result = mapData2Key(res, cols);
169
+ expect(result).toEqual([
170
+ { identifier: 1, fullName: 'Alice' },
171
+ { identifier: 2, fullName: 'Bob' },
172
+ ]);
173
+ });
174
+
175
+ test('should map data correctly when key is not provided', () => {
176
+ const res = [
177
+ { id: 1, name: 'Alice' },
178
+ { id: 2, name: 'Bob' },
179
+ ];
180
+ const cols = [
181
+ { data: 'id' },
182
+ { data: 'name' },
183
+ ];
184
+
185
+ const result = mapData2Key(res, cols);
186
+ expect(result).toEqual([
187
+ { id: 1, name: 'Alice' },
188
+ { id: 2, name: 'Bob' },
189
+ ]);
190
+ });
191
+
192
+ test('should handle an empty result set', () => {
193
+ const res = [];
194
+ const cols = [
195
+ { key: 'identifier', data: 'id' },
196
+ { key: 'fullName', data: 'name' },
197
+ ];
198
+
199
+ const result = mapData2Key(res, cols);
200
+ expect(result).toEqual([]);
201
+ });
202
+
203
+ test('should handle an empty column set', () => {
204
+ const res = [
205
+ { id: 1, name: 'Alice' },
206
+ { id: 2, name: 'Bob' },
207
+ ];
208
+ const cols = [];
209
+
210
+ const result = mapData2Key(res, cols);
211
+ expect(result).toEqual([{}, {}]);
212
+ });
213
+
214
+ test('should handle null and undefined values in the data', () => {
215
+ const res = [
216
+ { id: 1, name: null },
217
+ { id: null, name: 'Bob' },
218
+ ];
219
+ const cols = [
220
+ { key: 'identifier', data: 'id' },
221
+ { key: 'fullName', data: 'name' },
222
+ ];
223
+
224
+ const result = mapData2Key(res, cols);
225
+ expect(result).toEqual([
226
+ { identifier: 1, fullName: null },
227
+ { identifier: null, fullName: 'Bob' },
228
+ ]);
229
+ });
230
+
231
+ test('should return the correct URL for POST requests', () => {
232
+ const config = { url: 'http://example.com' };
233
+ const methodName = 'testMethod';
234
+ const isPost = true;
235
+ const expectedUrl = 'http://example.com/fina/rest/TOrmMethod/%22testMethod%22';
236
+
237
+ expect(getUrlApi(config, methodName, isPost)).toBe(expectedUrl);
238
+ });
239
+
240
+ test('should return the correct URL for GET requests', () => {
241
+ const config = { url: 'http://example.com' };
242
+ const methodName = 'testMethod';
243
+ const isPost = false;
244
+ const expectedUrl = 'http://example.com/fina/rest/TOrmMethod/testMethod';
245
+
246
+ expect(getUrlApi(config, methodName, isPost)).toBe(expectedUrl);
247
+ });
248
+
249
+ test('should convert an array of objects to a JSON string with a _parameters key', () => {
250
+ const input = [{ a: 1 }, { b: 2 }];
251
+ const expectedOutput = JSON.stringify({ _parameters: [JSON.stringify({ a: 1 }), JSON.stringify({ b: 2 })] });
252
+
253
+ expect(json2Parameters(input)).toBe(expectedOutput);
254
+ });
255
+
256
+ test('should handle an empty array', () => {
257
+ const input = [];
258
+ const expectedOutput = JSON.stringify({ _parameters: [] });
259
+
260
+ expect(json2Parameters(input)).toBe(expectedOutput);
261
+ });
262
+
263
+ test('should return correct url and params', () => {
264
+ const config = {
265
+ url: 'https://example.com',
266
+ dbindex: '1'
267
+ };
268
+ const method = 'POST';
269
+ const obj = { id: 1 };
270
+ const path = 'testMethod';
271
+ const result = getUrlAndParam(config, method, obj, path);
272
+
273
+ expect(result).toEqual({
274
+ url: 'https://example.com/fina/rest/TOrmMethod/%22testMethod%22',
275
+ params: "{\"_parameters\":[\"{\\\"dbIndex\\\":\\\"1\\\",\\\"method\\\":\\\"POST\\\",\\\"object\\\":{\\\"id\\\":1}}\"]}"
276
+ });
277
+ });
278
+
279
+ test('should get table object', () => {
280
+ const model = { id: 1, name: 'testModel' };
281
+ const tableName = 'testTable';
282
+
283
+ const result = getTableObj(model, tableName);
284
+
285
+ let expected = {};
286
+ expected[tableName] = model;
287
+ expect(result).toEqual(expected);
288
+ });
289
+
290
+ test('should return the correct headers and params', () => {
291
+ const apiConfig = { subdomain: 'example' };
292
+ const expectedResult = {
293
+ headers: { 'content-type': 'text/plain' },
294
+ params: { subdomain: 'example' }
295
+ };
296
+
297
+ const result = options(apiConfig);
298
+ expect(result).toEqual(expectedResult);
299
+ });
300
+
301
+ test('should handle missing subdomain in apiConfig', () => {
302
+ const apiConfig = {};
303
+ const expectedResult = {
304
+ headers: { 'content-type': 'text/plain' },
305
+ params: { subdomain: undefined }
306
+ };
307
+
308
+ const result = options(apiConfig);
309
+ expect(result).toEqual(expectedResult);
310
+ });
311
+
312
+ test('should set library if any', async () => {
313
+ axios.post.mockResolvedValueOnce(axios.post.mockResolvedValueOnce({
314
+ data: [
315
+ {
316
+ 'SYS$CLI_SCRIPT.SCRIPT': 'get = function () {\n' +
317
+ ' return {\n' +
318
+ ' functions: {\n' +
319
+ ' yyy: function (data) {\n' +
320
+ ' function doit() {\n' +
321
+ ' return {\n' +
322
+ " abc: function () { return 'abc'; console.log('abc') },\n" +
323
+ ' }\n' +
324
+ ' }\n' +
325
+ ' return doit()\n' +
326
+ ' },\n' +
327
+ ' zzz: function () {\n' +
328
+ " return 'aaabbbccc';\n" +
329
+ ' }\n' +
330
+ ' }\n' +
331
+ ' }\n' +
332
+ '}'
333
+ }
334
+ ]
335
+ }))
336
+
337
+ const config = {};
338
+ const libraries = ['lib'];
339
+
340
+ const result = await setLibrary(config, libraries);
341
+
342
+ expect(result.lib.yyy().abc()).toStrictEqual('abc');
343
+ expect(result.lib.zzz()).toStrictEqual('aaabbbccc');
344
+ });
345
+
346
+
149
347
  })
150
348
 
@@ -1,7 +1,7 @@
1
1
 
2
2
  import { jest } from '@jest/globals'
3
3
 
4
- const mockSendMail = jest.fn();
4
+ let mockSendMail = jest.fn();
5
5
  const mockCreateTransport = jest.fn();
6
6
  mockCreateTransport.mockImplementation(() => ({
7
7
  sendMail: mockSendMail
@@ -16,7 +16,7 @@ const { sendMailWatcher } = await import('../mailController.js');
16
16
  describe('Mail Controller', () => {
17
17
  let req;
18
18
 
19
- test('transporter.sendmailWatcher is called', () => {
19
+ test('transporter.sendmailWatcher is called', async () => {
20
20
  req = {
21
21
  body: {
22
22
  companyname: 'abc',
@@ -26,9 +26,14 @@ describe('Mail Controller', () => {
26
26
  },
27
27
  }
28
28
 
29
- sendMailWatcher(req);
29
+ mockSendMail.mockResolvedValue('OK');
30
+ expect(await sendMailWatcher(req)).toEqual('OK');
30
31
  expect(mockSendMail).toBeCalledTimes(1);
31
32
  expect(mockCreateTransport).toHaveBeenCalledWith({ smtp: 'test' });
32
33
 
34
+ mockSendMail.mockRejectedValue({ message: 'error' });
35
+ expect(await sendMailWatcher(req)).toEqual('error');
36
+
37
+ expect(mockSendMail).toBeCalledTimes(2);
33
38
  })
34
39
  })
@@ -0,0 +1 @@
1
+ get = function () {return {modelA: {}}}
@@ -0,0 +1,2 @@
1
+ const get = function () {
2
+ return {"modelB": {id: null}}};
@@ -0,0 +1,4 @@
1
+ let get = function () {return {modelC: {}, function: {func: () => {
2
+ return ["lib"];
3
+ }
4
+ }}};
@@ -0,0 +1,2 @@
1
+ var get = function () {return {model: {}, tableName: "", fields: [], function: {}}};
2
+ module.exports = get;
@@ -0,0 +1,49 @@
1
+ [
2
+ {"menuName": "", "caption": "Home", "link": ["./main"],"subMenu": []},
3
+ {"menuName": "", "caption": "Personalia","link": [], "subMenu": [
4
+ {"menuName": "","caption": "Data Master","link": [], "subMenu": []},
5
+ {
6
+ "menuName": "",
7
+ "caption": "Gaji",
8
+ "link": [],
9
+ "subMenu": [
10
+ {
11
+ "menuName": "",
12
+ "caption": "Parameter",
13
+ "link": [
14
+ "./form",
15
+ "gajiparam",
16
+ null
17
+ ],
18
+ "subMenu": []
19
+ },
20
+ {
21
+ "menuName": "",
22
+ "caption": "Perubahan",
23
+ "link": [],
24
+ "subMenu": [
25
+ {
26
+ "menuName": "",
27
+ "caption": "Form Perubahan Gaji",
28
+ "link": [
29
+ "./form",
30
+ "gajiubah",
31
+ null
32
+ ],
33
+ "subMenu": []
34
+ },
35
+ {
36
+ "menuName": "",
37
+ "caption": "Daftar Perubahan Gaji",
38
+ "link": [
39
+ "./list",
40
+ "gajiubahdaftar"
41
+ ],
42
+ "subMenu": []
43
+ }
44
+ ]
45
+ }
46
+ ]
47
+ }
48
+ ]}
49
+ ]