@n42/cli 0.1.42 → 0.1.63

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/src/db.js ADDED
@@ -0,0 +1,157 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+
4
+ const config = require("./config");
5
+ let DATABASE_FILE = config.DATABASE_FILE;
6
+ let artefactIndex = null;
7
+
8
+ function setSource(p) {
9
+ DATABASE_FILE = p;
10
+ }
11
+
12
+ function load() {
13
+ if (!fs.existsSync(DATABASE_FILE)) return {
14
+ user: [],
15
+ artefacts: [],
16
+ usage: [],
17
+ };
18
+ return JSON.parse(fs.readFileSync(DATABASE_FILE, "utf8"));
19
+ }
20
+
21
+ function save(database) {
22
+ invalidateArtefactIndex();
23
+
24
+ const dir = path.dirname(DATABASE_FILE);
25
+ fs.mkdirSync(dir, { recursive: true });
26
+
27
+ const tmp = DATABASE_FILE + ".tmp";
28
+
29
+ // always write tmp
30
+ fs.writeFileSync(tmp, JSON.stringify(database, null, 2), "utf8");
31
+
32
+ try { fs.renameSync(tmp, DATABASE_FILE); }
33
+ catch (e) {
34
+ if (e.code === "ENOENT") {
35
+ // fallback for first write
36
+ fs.writeFileSync(DATABASE_FILE, JSON.stringify(database, null, 2), "utf8");
37
+ } else {
38
+ throw e;
39
+ }
40
+ }
41
+ }
42
+
43
+ function insert(collection, item) {
44
+ const db = load();
45
+ db[collection] ??= [];
46
+ db[collection].push(item);
47
+ save(db);
48
+ }
49
+
50
+ function update(collection, item, key = "id") {
51
+ const db = load();
52
+ const idx = db[collection]?.findIndex(x => x[key] === item[key]);
53
+
54
+ if (idx < 0) return false;
55
+
56
+ db[collection][idx] = { ...db[collection][idx], ...item };
57
+ save(db);
58
+ return true;
59
+ }
60
+
61
+ function upsert(collection, item, key = "id") {
62
+ const db = load();
63
+ db[collection] ??= [];
64
+
65
+ const idx = db[collection].findIndex(x => x[key] === item[key]);
66
+ if (idx >= 0) {
67
+ db[collection][idx] = { ...db[collection][idx], ...item, updatedAt: Date.now() };
68
+ } else {
69
+ db[collection].push({ ...item, createdAt: Date.now() });
70
+ }
71
+
72
+ save(db);
73
+ }
74
+
75
+ function replace(collection, value) {
76
+ const db = load();
77
+ db[collection] = value;
78
+ save(db);
79
+ }
80
+
81
+ function set(collection, key, value) {
82
+ const db = load();
83
+ db[collection] ??= {};
84
+ db[collection][key] = value;
85
+ save(db);
86
+ }
87
+
88
+ function remove(collection, keyValue, key = "id") {
89
+ const db = load();
90
+ db[collection] = (db[collection] ?? []).filter(x => x[key] !== keyValue);
91
+ save(db);
92
+ }
93
+
94
+ function get(collection) {
95
+ const db = load();
96
+ return db[collection] ?? [];
97
+ }
98
+
99
+ function find(collection, predicate) {
100
+ const db = load();
101
+ return (db[collection] ?? []).filter(predicate);
102
+ }
103
+
104
+ function indexBy(list, key) {
105
+ const map = Object.create(null);
106
+
107
+ for (const item of list) {
108
+ const k = item?.[key];
109
+ if (k == null) continue; // skip null/undefined
110
+ (map[k] ??= []).push(item);
111
+ }
112
+
113
+ return map;
114
+ }
115
+
116
+ function indexByFn(list, keyFn) {
117
+ const map = Object.create(null);
118
+
119
+ for (const item of list) {
120
+ const k = keyFn(item);
121
+ if (k == null) continue;
122
+ (map[k] ??= []).push(item);
123
+ }
124
+
125
+ return map;
126
+ }
127
+
128
+ function indexByMap(list, key) {
129
+ const map = new Map();
130
+ for (const item of list) {
131
+ const k = item?.[key];
132
+ if (k == null) continue;
133
+ const arr = map.get(k) ?? [];
134
+ arr.push(item);
135
+ map.set(k, arr);
136
+ }
137
+ return map;
138
+ }
139
+
140
+ function getArtefactIndex() {
141
+ if (!artefactIndex) {
142
+ const list = get("artefacts");
143
+ artefactIndex = indexBy(list, "participantId");
144
+ }
145
+ return artefactIndex;
146
+ }
147
+
148
+ function invalidateArtefactIndex() {
149
+ artefactIndex = null;
150
+ }
151
+
152
+ function artefactsByParticipant(pid) {
153
+ const idx = getArtefactIndex();
154
+ return idx[pid] ?? [];
155
+ }
156
+
157
+ module.exports = { setSource, load, save, insert, update, upsert, replace, set, remove, get, find, indexBy, indexByFn, indexByMap, artefactsByParticipant };
package/src/discover.js CHANGED
@@ -1,10 +1,11 @@
1
1
  const fs = require("fs");
2
2
  const path = require("path");
3
3
  const pkg = require("../package.json");
4
+ const db = require("./db");
4
5
 
5
6
  const { fetchWithAuth } = require("./auth");
6
7
  const { API_URL, EP_DISCOVER, DEFAULT_OUTPUT, DEFAULT_FORMAT, ARTEFACTS_DIR } = require("./config");
7
- const { getUserUsage, updateUserUsage } = require("./user");
8
+ const { getUserUsage } = require("./user");
8
9
  const { clearScreen, startSpinner, buildDocLabel, promptForDocument } = require("./utils");
9
10
  const { handleError } = require("./errors");
10
11
 
@@ -33,7 +34,7 @@ const DEFAULT_DISCOVERY_INPUT = {
33
34
  const discoveryInput = DEFAULT_DISCOVERY_INPUT;
34
35
  let docSelected = false;
35
36
 
36
- async function processSupportedDocuments(encodedDocs, environment, participantId, options) {
37
+ async function processSupportedDocuments(encodedDocs, onDone) {
37
38
  if (encodedDocs && !docSelected) {
38
39
  const docs = JSON.parse(Buffer.from(encodedDocs, "base64").toString("utf8"))
39
40
  .map(d => ({ ...d, label: buildDocLabel(d) }));
@@ -52,13 +53,16 @@ async function processSupportedDocuments(encodedDocs, environment, participantId
52
53
  discoveryInput.document.value = docSelected.value;
53
54
  }
54
55
 
55
- runDiscovery(environment, participantId, options);
56
+ if (typeof onDone === "function") {
57
+ await onDone(); // no args
58
+ }
56
59
  }
57
60
  }
58
61
  }
59
62
 
60
- async function runDiscovery(environment, participantId, options) {
63
+ async function runDiscovery(participantId, options) {
61
64
  const {
65
+ env,
62
66
  output = DEFAULT_OUTPUT,
63
67
  format = DEFAULT_FORMAT,
64
68
  forceHttps,
@@ -72,7 +76,7 @@ async function runDiscovery(environment, participantId, options) {
72
76
 
73
77
  const payload = {
74
78
  ...discoveryInput,
75
- env: environment,
79
+ env,
76
80
  options: {
77
81
  output,
78
82
  format,
@@ -116,7 +120,16 @@ async function runDiscovery(environment, participantId, options) {
116
120
  const currentMonth = new Date().toISOString().slice(0, 7);
117
121
  userUsage.serviceUsage.discovery[currentMonth] = serviceUsage;
118
122
 
119
- updateUserUsage(userUsage);
123
+ db.replace("serviceUsage", userUsage);
124
+
125
+ db.insert("artefacts", {
126
+ id: refId,
127
+ participantId,
128
+ options,
129
+ output,
130
+ format,
131
+ createdAt: Date.now()
132
+ });
120
133
 
121
134
  if (output === "plantuml" && format === "svg") {
122
135
  const svg = await res.text();
@@ -127,7 +140,9 @@ async function runDiscovery(environment, participantId, options) {
127
140
  process.exit(1);
128
141
  }
129
142
 
130
- await processSupportedDocuments(encodedDocs, environment, participantId, options);
143
+ await processSupportedDocuments(encodedDocs, async () => {
144
+ await runDiscovery(participantId, options);
145
+ });
131
146
 
132
147
  const file = path.join(ARTEFACTS_DIR, `${refId}.svg`);
133
148
  fs.writeFileSync(file, svg);
@@ -142,7 +157,9 @@ async function runDiscovery(environment, participantId, options) {
142
157
  const text = await res.text();
143
158
  stopSpinner();
144
159
 
145
- await processSupportedDocuments(encodedDocs, environment, participantId, options);
160
+ await processSupportedDocuments(encodedDocs, async () => {
161
+ await runDiscovery(participantId, options);
162
+ });
146
163
 
147
164
  const file = path.join(ARTEFACTS_DIR, `${refId}.puml`);
148
165
  fs.writeFileSync(file, text);
@@ -157,7 +174,9 @@ async function runDiscovery(environment, participantId, options) {
157
174
  const json = await res.json();
158
175
  stopSpinner();
159
176
 
160
- await processSupportedDocuments(encodedDocs, environment, participantId, options);
177
+ await processSupportedDocuments(encodedDocs, async () => {
178
+ await runDiscovery(participantId, options);
179
+ });
161
180
 
162
181
  const file = path.join(ARTEFACTS_DIR, `${refId}.json`);
163
182
  fs.writeFileSync(file, JSON.stringify(json, null, 2));
package/src/user.js CHANGED
@@ -1,64 +1,27 @@
1
- const fs = require("fs");
2
- const { NODE42_DIR, USER_FILE, USAGE_FILE } = require("./config");
1
+ const db = require("./db");
3
2
 
4
- function updateUserInfo(user) {
5
- fs.mkdirSync(NODE42_DIR, { recursive: true });
6
-
7
- fs.writeFileSync(
8
- USER_FILE,
9
- JSON.stringify(user, null, 2)
10
- );
11
- }
12
-
13
- function getUserInfo() {
14
- try {
15
- if (!fs.existsSync(USER_FILE)) {
16
- return {
17
- userName: "n/a",
18
- userMail: "n/a",
19
- role: "n/a"
20
- };
21
- }
22
- return JSON.parse(fs.readFileSync(USER_FILE, "utf8"));
23
- } catch {
24
- return {
25
- userName: "n/a",
26
- userMail: "n/a",
27
- role: "n/a"
3
+ function getUser() {
4
+ const users = db.get("user");
5
+ return users.length ? users[0] : {
6
+ "id": "n/a",
7
+ "userName": "n/a",
8
+ "userMail": "n/a",
9
+ "role": "n/a"
28
10
  };
29
- }
30
11
  }
31
12
 
32
- function updateUserUsage(usage) {
33
- fs.mkdirSync(NODE42_DIR, { recursive: true });
13
+ function getUserUsage() {
14
+ const usage = db.get("serviceUsage");
34
15
 
35
- fs.writeFileSync(
36
- USAGE_FILE,
37
- JSON.stringify(usage, null, 2)
38
- );
39
- }
16
+ if (usage && usage.serviceUsage) return usage;
40
17
 
41
- function getUserUsage() {
42
- try {
43
- if (!fs.existsSync(USAGE_FILE)) {
44
- return {
45
- serviceUsage: {
46
- discovery: {},
47
- validation: {},
48
- transactions: {}
49
- }
50
- };
18
+ return {
19
+ serviceUsage: {
20
+ discovery: {},
21
+ validation: {},
22
+ transactions: {}
51
23
  }
52
- return JSON.parse(fs.readFileSync(USAGE_FILE, "utf8"));
53
- } catch {
54
- return {
55
- serviceUsage: {
56
- discovery: {},
57
- validation: {},
58
- transactions: {}
59
- }
60
- };
61
- }
24
+ };
62
25
  }
63
26
 
64
- module.exports = { updateUserInfo, getUserInfo, updateUserUsage, getUserUsage };
27
+ module.exports = { getUser, getUserUsage };
package/src/utils.js CHANGED
@@ -1,5 +1,8 @@
1
+ const fs = require("fs");
1
2
  const inquirer = require("inquirer");
2
3
  const readline = require("readline");
4
+ const config = require("./config");
5
+
3
6
 
4
7
  function clearScreen(text) {
5
8
  process.stdout.write("\x1Bc");
@@ -8,7 +11,7 @@ function clearScreen(text) {
8
11
  }
9
12
  }
10
13
 
11
- function ask(question, hidden=false) {
14
+ function ask(question, def, hidden=false) {
12
15
  return new Promise(resolve => {
13
16
  const rl = readline.createInterface({
14
17
  input: process.stdin,
@@ -16,6 +19,8 @@ function ask(question, hidden=false) {
16
19
  terminal: true
17
20
  });
18
21
 
22
+ const q = def ? `${question} (${def}): ` : `${question}: `;
23
+
19
24
  process.stdin.on("data", char => {
20
25
  char = char + "";
21
26
  switch (char) {
@@ -28,16 +33,16 @@ function ask(question, hidden=false) {
28
33
  if (hidden) {
29
34
  process.stdout.clearLine(0);
30
35
  process.stdout.cursorTo(0);
31
- process.stdout.write(question + "*".repeat(rl.line.length));
36
+ process.stdout.write(q + "*".repeat(rl.line.length));
32
37
  }
33
38
  break;
34
39
  }
35
40
  });
36
41
 
37
- rl.question(question, answer => {
42
+ rl.question(q, answer => {
38
43
  rl.history = rl.history.slice(1);
39
44
  rl.close();
40
- resolve(answer);
45
+ resolve(answer || def);
41
46
  });
42
47
  });
43
48
  }
@@ -56,6 +61,59 @@ function startSpinner(text = "Working") {
56
61
  };
57
62
  }
58
63
 
64
+ async function promptForDocument(docs) {
65
+ const { document } = await inquirer.prompt([
66
+ {
67
+ type: "list",
68
+ name: "document",
69
+ message: "Select document type:",
70
+ choices: docs.map(d => ({
71
+ name: d.label,
72
+ value: d
73
+ }))
74
+ }
75
+ ]);
76
+
77
+ return document;
78
+ }
79
+
80
+ function validateEnv(env) {
81
+ const allowedEnvs = ["TEST", "PROD"];
82
+ if (!allowedEnvs.includes(env.toUpperCase())) {
83
+ throw new Error(
84
+ `Invalid environment: ${env}\nAllowed values: ${allowedEnvs.join(", ")}`
85
+ );
86
+ }
87
+ }
88
+
89
+ function validateId(type, id) {
90
+ const value = id.replace(/\s+/g, "");
91
+
92
+ // ISO 6523–safe; participant id like 0000:12345 or 9915:abcde
93
+ if (!/^[0-9]{4}:[a-zA-Z0-9\-\._~]{1,135}$/.test(value)) {
94
+ throw new Error(
95
+ `Invalid ${type}Id: ${id}\nExpected format: 0007:123456789 or 0007:abcd`
96
+ );
97
+ }
98
+ }
99
+
100
+ function createAppDirs(force=false) {
101
+ fs.mkdirSync(config.NODE42_DIR, { recursive: true });
102
+ fs.mkdirSync(config.ARTEFACTS_DIR, { recursive: true });
103
+ fs.mkdirSync(config.TRANSACTIONS_DIR, { recursive: true });
104
+ fs.mkdirSync(config.VALIDATION_DIR, { recursive: true });
105
+
106
+ if (!fs.existsSync(config.CONFIG_FILE) || force) {
107
+ fs.writeFileSync(
108
+ config.CONFIG_FILE,
109
+ JSON.stringify({
110
+ DEFAULT_OUTPUT: config.DEFAULT_OUTPUT,
111
+ DEFAULT_FORMAT: config.DEFAULT_FORMAT
112
+ }, null, 2)
113
+ );
114
+ }
115
+ }
116
+
59
117
  function buildDocLabel({ scheme, value }) {
60
118
  // 1. Document name (after :: before ##)
61
119
  const docMatch = value.match(/::([^#]+)##/);
@@ -95,40 +153,14 @@ function buildDocLabel({ scheme, value }) {
95
153
  return docName;
96
154
  }
97
155
 
98
- async function promptForDocument(docs) {
99
- const { document } = await inquirer.prompt([
100
- {
101
- type: "list",
102
- name: "document",
103
- message: "Select document type:",
104
- choices: docs.map(d => ({
105
- name: d.label,
106
- value: d
107
- }))
108
- }
109
- ]);
110
-
111
- return document;
112
- }
113
-
114
- function validateEnv(env) {
115
- const allowedEnvs = ["TEST", "PROD"];
116
- if (!allowedEnvs.includes(env.toUpperCase())) {
117
- throw new Error(
118
- `Invalid environment: ${env}\nAllowed values: ${allowedEnvs.join(", ")}`
119
- );
120
- }
121
- }
122
-
123
- function validateId(type, id) {
124
- const value = id.replace(/\s+/g, "");
125
-
126
- // ISO 6523–safe; participant id like 0000:12345 or 9915:abcde
127
- if (!/^[0-9]{4}:[a-zA-Z0-9\-\._~]{1,135}$/.test(value)) {
128
- throw new Error(
129
- `Invalid ${type}Id: ${id}\nExpected format: 0007:123456789 or 0007:abcd`
130
- );
156
+ function getArtefactExt(output, format) {
157
+ if (output === "plantuml" && format === "svg") {
158
+ return "svg";
159
+ } else if (output === "plantuml" && format === "text") {
160
+ return "puml"
161
+ } else {
162
+ return "json";
131
163
  }
132
164
  }
133
165
 
134
- module.exports = { clearScreen, startSpinner, ask, buildDocLabel, promptForDocument, validateEnv, validateId };
166
+ module.exports = { clearScreen, startSpinner, ask, buildDocLabel, promptForDocument, validateEnv, validateId, createAppDirs, getArtefactExt };
@@ -0,0 +1,98 @@
1
+ const { expect } = require("chai");
2
+ const sinon = require("sinon");
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const os = require("os");
6
+ const db = require("../src/db");
7
+
8
+ const TEST_DB = path.join(os.tmpdir(), "test-db.json");
9
+
10
+ describe("auth", () => {
11
+ describe("login()", () => {
12
+ let login;
13
+ let utils;
14
+ let auth;
15
+ let user;
16
+
17
+ beforeEach(() => {
18
+ sinon.restore();
19
+
20
+ db.setSource(TEST_DB);
21
+ if (fs.existsSync(TEST_DB)) fs.unlinkSync(TEST_DB);
22
+
23
+ // fresh require each test
24
+ delete require.cache[require.resolve("../src/utils")];
25
+ delete require.cache[require.resolve("../src/user")];
26
+ delete require.cache[require.resolve("../src/auth")];
27
+
28
+ utils = require("../src/utils");
29
+ user = require("../src/user");
30
+
31
+ sinon.stub(utils, "clearScreen");
32
+ sinon.stub(utils, "ask")
33
+ .onFirstCall().resolves("user")
34
+ .onSecondCall().resolves("secret");
35
+
36
+ sinon.stub(utils, "startSpinner").callsFake(() => () => {});
37
+ sinon.stub(user, "getUser").returns({
38
+ userName: "User",
39
+ userMail: "user@test.com",
40
+ role: "user"
41
+ });
42
+
43
+ sinon.stub(fs, "mkdirSync");
44
+ sinon.stub(fs, "writeFileSync");
45
+
46
+ sinon.stub(console, "log");
47
+ sinon.stub(console, "error");
48
+ sinon.stub(process, "exit").callsFake(() => {
49
+ throw new Error("process.exit");
50
+ });
51
+
52
+ auth = require("../src/auth");
53
+ login = auth.login;
54
+
55
+ sinon.stub(auth, "checkAuth").returns(true);
56
+ });
57
+
58
+ afterEach(() => {
59
+ sinon.restore();
60
+ delete global.fetch;
61
+ });
62
+
63
+ it("logs in successfully and writes tokens", async () => {
64
+ // mock fetch
65
+ global.fetch = sinon.stub().resolves({
66
+ ok: true,
67
+ json: async () => ({
68
+ accessToken: "a",
69
+ refreshToken: "r",
70
+ idToken: "i"
71
+ })
72
+ });
73
+
74
+ await login();
75
+
76
+ expect(fs.writeFileSync.called).to.be.true;
77
+ expect(console.log.calledWithMatch("Authenticated as")).to.be.true;
78
+ });
79
+
80
+ it("exits on http error", async () => {
81
+ global.fetch = sinon.stub().resolves({
82
+ ok: false,
83
+ status: 401,
84
+ json: async () => ({})
85
+ });
86
+
87
+ try {
88
+ await login();
89
+ } catch (e) {
90
+ // expected
91
+ }
92
+
93
+ expect(console.error.calledWithMatch("Login failed")).to.be.true;
94
+ expect(process.exit.calledWith(1)).to.be.true;
95
+ expect(fs.writeFileSync.called).to.be.false;
96
+ });
97
+ });
98
+ });