view-api 1.1.1 → 3.0.1
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/LICENSE +7 -0
- package/README.md +63 -28
- package/bin/mock-runner.js +28 -10
- package/package.json +11 -5
- package/src/api/handler.js +42 -0
- package/src/api/random.js +7 -0
- package/src/api/server.js +20 -0
- package/src/editor/public/editor.js +78 -0
- package/src/editor/public/index.html +14 -0
- package/src/editor/public/style.css +40 -0
- package/src/editor/server.js +30 -0
- package/src/mocks/mock.json +39 -42
- package/src/shared/config.js +23 -0
- package/src/shared/defaultConfig.js +54 -0
- package/src/app.js +0 -29
- package/src/controllers/mock.controller.js +0 -21
- package/src/routes/mock.routes.js +0 -8
- package/src/server.js +0 -10
- package/src/services/mock.service.js +0 -15
- package/src/utils/loadConfig.js +0 -6
- package/src/utils/responsePicker.js +0 -19
package/LICENSE
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright 2026 Ridlo Achmad Ghifary
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
CHANGED
|
@@ -6,10 +6,11 @@ Perfect for frontend development, testing, and prototyping without a real backen
|
|
|
6
6
|
|
|
7
7
|
## ✨ Features
|
|
8
8
|
|
|
9
|
-
- Run
|
|
9
|
+
- Run a command and get editor and the api fetchable
|
|
10
10
|
- Auto Refresh JSON file
|
|
11
11
|
- Customizable port
|
|
12
12
|
- Randomized success / error responses
|
|
13
|
+
- Delay response
|
|
13
14
|
- Zero setup for frontend teams
|
|
14
15
|
- Works fully offline
|
|
15
16
|
|
|
@@ -20,62 +21,96 @@ You don’t need to install it globally.
|
|
|
20
21
|
Run directly with `npx`:
|
|
21
22
|
|
|
22
23
|
```bash
|
|
23
|
-
|
|
24
|
+
# simpler way
|
|
25
|
+
npx view-api dev
|
|
26
|
+
|
|
27
|
+
or
|
|
28
|
+
|
|
29
|
+
# use your own file
|
|
30
|
+
npx view-api dev <file-path>
|
|
24
31
|
```
|
|
25
32
|
|
|
26
33
|
Or install globally:
|
|
27
34
|
|
|
28
35
|
```bash
|
|
29
36
|
npm install -g view-api
|
|
30
|
-
view-api
|
|
37
|
+
view-api dev
|
|
31
38
|
```
|
|
32
39
|
|
|
33
40
|
## 🚀 Usage
|
|
34
41
|
|
|
35
42
|
```bash
|
|
36
|
-
view-api
|
|
43
|
+
view-api dev <config-path> [options]
|
|
37
44
|
```
|
|
38
45
|
|
|
39
46
|
### Options
|
|
40
47
|
|
|
41
|
-
| Option
|
|
42
|
-
|
|
|
43
|
-
| `--port
|
|
48
|
+
| Option | Description | Default |
|
|
49
|
+
| ------------ | ---------------------- | ------- |
|
|
50
|
+
| `--api-port` | Port to run the api | `8723` |
|
|
51
|
+
| `--ui-port` | Port to run the editor | `8724` |
|
|
44
52
|
|
|
45
53
|
Example:
|
|
46
54
|
|
|
47
55
|
```bash
|
|
48
|
-
view-api
|
|
56
|
+
view-api dev src/mocks/mock.json --api-port 4000 --ui-port 4001
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Then you will have running editor and the API endpoint:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
➜ API running at http://localhost:4000
|
|
63
|
+
➜ EDITOR running at http://localhost:4001
|
|
49
64
|
```
|
|
50
65
|
|
|
51
66
|
## 📄 Mock Config Format
|
|
52
67
|
|
|
53
68
|
```json
|
|
54
69
|
{
|
|
55
|
-
"
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
70
|
+
"GET /products": {
|
|
71
|
+
"behavior": {
|
|
72
|
+
"successRate": 50
|
|
73
|
+
},
|
|
74
|
+
"responses": {
|
|
75
|
+
"success": {
|
|
76
|
+
"statusCode": 200,
|
|
77
|
+
"body": {
|
|
78
|
+
"status": "success",
|
|
79
|
+
"message": "Products fetched wkwk",
|
|
80
|
+
"data": [
|
|
81
|
+
{
|
|
82
|
+
"id": 1,
|
|
83
|
+
"name": "Product A",
|
|
84
|
+
"price": 10000,
|
|
85
|
+
"stock": 50
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"id": 2,
|
|
89
|
+
"name": "Product B",
|
|
90
|
+
"price": 15000,
|
|
91
|
+
"stock": 30
|
|
92
|
+
}
|
|
93
|
+
]
|
|
94
|
+
}
|
|
60
95
|
},
|
|
61
|
-
"
|
|
62
|
-
|
|
63
|
-
"statusCode":
|
|
96
|
+
"errors": [
|
|
97
|
+
{
|
|
98
|
+
"statusCode": 500,
|
|
64
99
|
"body": {
|
|
65
|
-
"status": "
|
|
66
|
-
"
|
|
100
|
+
"status": "failed",
|
|
101
|
+
"message": "Server error",
|
|
102
|
+
"error_code": "SERVER_ERROR"
|
|
67
103
|
}
|
|
68
104
|
},
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
"
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
105
|
+
{
|
|
106
|
+
"statusCode": 400,
|
|
107
|
+
"body": {
|
|
108
|
+
"status": "failed",
|
|
109
|
+
"message": "Bad request, invalid parameters",
|
|
110
|
+
"error_code": "INVALID_PARAMETERS"
|
|
76
111
|
}
|
|
77
|
-
|
|
78
|
-
|
|
112
|
+
}
|
|
113
|
+
]
|
|
79
114
|
}
|
|
80
115
|
}
|
|
81
116
|
}
|
|
@@ -83,4 +118,4 @@ view-api start src/mocks/mock.json --port 4000
|
|
|
83
118
|
|
|
84
119
|
## 📜 License
|
|
85
120
|
|
|
86
|
-
MIT
|
|
121
|
+
Licensed under the MIT License.
|
package/bin/mock-runner.js
CHANGED
|
@@ -1,20 +1,38 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import path from "path";
|
|
4
3
|
import { program } from "commander";
|
|
5
|
-
import
|
|
4
|
+
import { startApiServer } from "../src/api/server.js";
|
|
5
|
+
import { startEditorServer } from "../src/editor/server.js";
|
|
6
|
+
import { loadConfig } from "../src/shared/config.js";
|
|
6
7
|
|
|
7
8
|
program
|
|
8
|
-
.name("
|
|
9
|
-
.description("Run mock APIs from a JSON
|
|
10
|
-
.version("
|
|
9
|
+
.name("view-api")
|
|
10
|
+
.description("Run mock APIs locally from a JSON configuration file")
|
|
11
|
+
.version("2.0.0");
|
|
11
12
|
|
|
13
|
+
/**
|
|
14
|
+
* DEV — API + Editor
|
|
15
|
+
*/
|
|
12
16
|
program
|
|
13
|
-
.command("
|
|
14
|
-
.
|
|
15
|
-
.
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
.command("dev [configPath]")
|
|
18
|
+
.description("Start mock API with live editor UI")
|
|
19
|
+
.option("--api-port <port>", "API port", 8723)
|
|
20
|
+
.option("--ui-port <port>", "Editor UI port", 8724)
|
|
21
|
+
.action((configPath, options) => {
|
|
22
|
+
if (configPath && !configPath.endsWith(".json")) {
|
|
23
|
+
console.error("➜ Config file must be a .json file");
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (options.apiPort === options.uiPort) {
|
|
28
|
+
console.error("➜ API port and UI port must be different");
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
loadConfig(configPath);
|
|
33
|
+
|
|
34
|
+
startApiServer({ port: Number(options.apiPort) });
|
|
35
|
+
startEditorServer({ port: Number(options.uiPort) });
|
|
18
36
|
});
|
|
19
37
|
|
|
20
38
|
program.parse();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "view-api",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.1",
|
|
4
4
|
"description": "Run mock APIs locally from a JSON configuration file",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"type": "git",
|
|
11
11
|
"url": "https://github.com/RidloGhifary/view-api"
|
|
12
12
|
},
|
|
13
|
-
"homepage": "https://github.
|
|
13
|
+
"homepage": "https://ridloghifary.github.io/view-api",
|
|
14
14
|
"bugs": {
|
|
15
15
|
"url": "https://github.com/RidloGhifary/view-api/issues"
|
|
16
16
|
},
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"README.md"
|
|
21
21
|
],
|
|
22
22
|
"scripts": {
|
|
23
|
-
"dev": "nodemon bin/mock-runner.js
|
|
23
|
+
"dev": "nodemon bin/mock-runner.js dev src/mocks/mock.json",
|
|
24
24
|
"start": "node bin/mock-runner.js",
|
|
25
25
|
"prepublishOnly": "npm run lint || true"
|
|
26
26
|
},
|
|
@@ -31,16 +31,22 @@
|
|
|
31
31
|
"cli",
|
|
32
32
|
"frontend",
|
|
33
33
|
"testing",
|
|
34
|
-
"json"
|
|
34
|
+
"json",
|
|
35
|
+
"view-api"
|
|
35
36
|
],
|
|
36
|
-
"author": "Ridlo",
|
|
37
|
+
"author": "Ridlo Achmad Ghifary <ridloghfry@gmail.com>",
|
|
38
|
+
"private": false,
|
|
37
39
|
"license": "MIT",
|
|
38
40
|
"engines": {
|
|
39
41
|
"node": ">=18"
|
|
40
42
|
},
|
|
41
43
|
"dependencies": {
|
|
44
|
+
"@codemirror/lang-json": "^6.0.2",
|
|
45
|
+
"@codemirror/theme-one-dark": "^6.1.3",
|
|
46
|
+
"codemirror": "^6.0.2",
|
|
42
47
|
"commander": "^14.0.2",
|
|
43
48
|
"cors": "^2.8.5",
|
|
49
|
+
"dotenv": "^17.2.3",
|
|
44
50
|
"express": "^5.2.1",
|
|
45
51
|
"helmet": "^8.1.0",
|
|
46
52
|
"morgan": "^1.10.1"
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { getConfig } from "../shared/config.js";
|
|
2
|
+
import { chance, pickRandom } from "./random.js";
|
|
3
|
+
|
|
4
|
+
export const handleMockRequest = (req, res) => {
|
|
5
|
+
const key = `${req.method.toUpperCase()} ${req.path}`;
|
|
6
|
+
const config = getConfig()[key];
|
|
7
|
+
|
|
8
|
+
if (!config) {
|
|
9
|
+
return res.status(404).json({
|
|
10
|
+
status: "failed",
|
|
11
|
+
message: "Mock not found",
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const isSuccess = chance(config?.behavior?.successRate ?? 100);
|
|
16
|
+
const errors = config?.responses?.errors ?? [];
|
|
17
|
+
|
|
18
|
+
if (isSuccess || !errors?.length) {
|
|
19
|
+
const { statusCode = 200, body } = config.responses.success;
|
|
20
|
+
const delay = config?.behavior?.delay ?? 0;
|
|
21
|
+
|
|
22
|
+
if (!body) {
|
|
23
|
+
return res.status(statusCode).json("No response body defined");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (delay > 0) {
|
|
27
|
+
return setTimeout(() => {
|
|
28
|
+
res.status(statusCode).json(body);
|
|
29
|
+
}, delay);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return res.status(statusCode).json(body);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const error = pickRandom(errors);
|
|
36
|
+
|
|
37
|
+
if (!error.body) {
|
|
38
|
+
return res.status(error.statusCode ?? 500).json("No error body defined");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return res.status(error.statusCode ?? 500).json(error.body);
|
|
42
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
import morgan from "morgan";
|
|
3
|
+
import cors from "cors";
|
|
4
|
+
import helmet from "helmet";
|
|
5
|
+
import { handleMockRequest } from "./handler.js";
|
|
6
|
+
|
|
7
|
+
export const startApiServer = ({ port }) => {
|
|
8
|
+
const app = express();
|
|
9
|
+
|
|
10
|
+
app.use(express.json());
|
|
11
|
+
app.use(cors());
|
|
12
|
+
app.use(helmet());
|
|
13
|
+
app.use(morgan("dev"));
|
|
14
|
+
|
|
15
|
+
app.use(handleMockRequest);
|
|
16
|
+
|
|
17
|
+
app.listen(port, () =>
|
|
18
|
+
console.log(`➜ API running at http://localhost:${port}`),
|
|
19
|
+
);
|
|
20
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { json } from "https://esm.sh/@codemirror/lang-json";
|
|
2
|
+
import { oneDark } from "https://esm.sh/@codemirror/theme-one-dark";
|
|
3
|
+
import { EditorView, basicSetup } from "https://esm.sh/codemirror";
|
|
4
|
+
|
|
5
|
+
let editor, saveTimer, statusTimer;
|
|
6
|
+
let lastValid = true;
|
|
7
|
+
|
|
8
|
+
const statusEl =
|
|
9
|
+
document.getElementById("status") || document.createElement("div");
|
|
10
|
+
statusEl.id = "status";
|
|
11
|
+
document.body.appendChild(statusEl);
|
|
12
|
+
|
|
13
|
+
// load config
|
|
14
|
+
const res = await fetch("/__config");
|
|
15
|
+
const data = await res.json();
|
|
16
|
+
|
|
17
|
+
const customTheme = EditorView.theme({
|
|
18
|
+
"&": {
|
|
19
|
+
fontSize: "14px",
|
|
20
|
+
fontFamily: `"JetBrains Mono", "Fira Code", monospace`,
|
|
21
|
+
},
|
|
22
|
+
".cm-content": {
|
|
23
|
+
padding: "16px",
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
editor = new EditorView({
|
|
28
|
+
doc: JSON.stringify(data, null, 2),
|
|
29
|
+
extensions: [
|
|
30
|
+
basicSetup,
|
|
31
|
+
json(),
|
|
32
|
+
oneDark,
|
|
33
|
+
customTheme,
|
|
34
|
+
EditorView.updateListener.of((update) => {
|
|
35
|
+
if (!update.docChanged) return;
|
|
36
|
+
debounceSave();
|
|
37
|
+
}),
|
|
38
|
+
],
|
|
39
|
+
parent: document.getElementById("editor"),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
function debounceSave() {
|
|
43
|
+
clearTimeout(saveTimer);
|
|
44
|
+
saveTimer = setTimeout(saveConfig, 700);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function saveConfig() {
|
|
48
|
+
try {
|
|
49
|
+
const value = editor.state.doc.toString();
|
|
50
|
+
if (!value.trim().endsWith("}")) return;
|
|
51
|
+
|
|
52
|
+
const parsed = JSON.parse(value);
|
|
53
|
+
|
|
54
|
+
await fetch("/__config", {
|
|
55
|
+
method: "POST",
|
|
56
|
+
headers: { "Content-Type": "application/json" },
|
|
57
|
+
body: JSON.stringify(parsed),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
showStatus("Saved ✓", false);
|
|
61
|
+
lastValid = true;
|
|
62
|
+
} catch (err) {
|
|
63
|
+
showStatus("Invalid JSON ✗", true);
|
|
64
|
+
lastValid = false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function showStatus(text, isError) {
|
|
69
|
+
clearTimeout(statusTimer);
|
|
70
|
+
|
|
71
|
+
statusEl.textContent = text;
|
|
72
|
+
statusEl.className = isError ? "error" : "ok";
|
|
73
|
+
statusEl.style.opacity = "1";
|
|
74
|
+
|
|
75
|
+
statusTimer = setTimeout(() => {
|
|
76
|
+
statusEl.style.opacity = "0";
|
|
77
|
+
}, 2500);
|
|
78
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<title>View API Editor</title>
|
|
6
|
+
<link rel="stylesheet" href="./style.css" />
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="editor"></div>
|
|
10
|
+
<div id="status"></div>
|
|
11
|
+
|
|
12
|
+
<script type="module" src="./editor.js"></script>
|
|
13
|
+
</body>
|
|
14
|
+
</html>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
html,
|
|
2
|
+
body {
|
|
3
|
+
margin: 0;
|
|
4
|
+
padding: 0;
|
|
5
|
+
height: 100%;
|
|
6
|
+
overflow: hidden;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
#editor {
|
|
10
|
+
height: 100vh;
|
|
11
|
+
width: 100vw;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/* CodeMirror full height fix */
|
|
15
|
+
.cm-editor {
|
|
16
|
+
height: 100%;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
#status {
|
|
20
|
+
position: fixed;
|
|
21
|
+
bottom: 0;
|
|
22
|
+
left: 0;
|
|
23
|
+
right: 0;
|
|
24
|
+
padding: 6px 12px;
|
|
25
|
+
font-size: 12px;
|
|
26
|
+
text-align: center;
|
|
27
|
+
transition: opacity 0.3s ease;
|
|
28
|
+
opacity: 0;
|
|
29
|
+
pointer-events: none;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
#status.ok {
|
|
33
|
+
background: #064e3b;
|
|
34
|
+
color: #a7f3d0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
#status.error {
|
|
38
|
+
background: #7f1d1d;
|
|
39
|
+
color: #fecaca;
|
|
40
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
import { getConfig, setConfig } from "../shared/config.js";
|
|
5
|
+
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = path.dirname(__filename);
|
|
8
|
+
|
|
9
|
+
export const startEditorServer = ({ port }) => {
|
|
10
|
+
const app = express();
|
|
11
|
+
app.use(express.json());
|
|
12
|
+
|
|
13
|
+
// serve UI
|
|
14
|
+
app.use(express.static(path.join(__dirname, "public")));
|
|
15
|
+
|
|
16
|
+
// get current config
|
|
17
|
+
app.get("/__config", (_req, res) => {
|
|
18
|
+
res.json(getConfig());
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// save config
|
|
22
|
+
app.post("/__config", (req, res) => {
|
|
23
|
+
setConfig(req.body);
|
|
24
|
+
res.json({ ok: true });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
app.listen(port, () =>
|
|
28
|
+
console.log(`➜ EDITOR running at http://localhost:${port}`),
|
|
29
|
+
);
|
|
30
|
+
};
|
package/src/mocks/mock.json
CHANGED
|
@@ -1,51 +1,48 @@
|
|
|
1
1
|
{
|
|
2
|
-
"
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
"GET /products": {
|
|
3
|
+
"behavior": {
|
|
4
|
+
"successRate": 50
|
|
5
|
+
},
|
|
6
|
+
"responses": {
|
|
7
|
+
"success": {
|
|
8
|
+
"statusCode": 200,
|
|
9
|
+
"body": {
|
|
10
|
+
"status": "success",
|
|
11
|
+
"message": "Products fetched wkwk",
|
|
12
|
+
"data": [
|
|
13
|
+
{
|
|
14
|
+
"id": 1,
|
|
15
|
+
"name": "Product A",
|
|
16
|
+
"price": 10000,
|
|
17
|
+
"stock": 50
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"id": 2,
|
|
21
|
+
"name": "Product B",
|
|
22
|
+
"price": 15000,
|
|
23
|
+
"stock": 30
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
}
|
|
7
27
|
},
|
|
8
|
-
"
|
|
9
|
-
|
|
10
|
-
"statusCode":
|
|
28
|
+
"errors": [
|
|
29
|
+
{
|
|
30
|
+
"statusCode": 500,
|
|
11
31
|
"body": {
|
|
12
|
-
"status": "
|
|
13
|
-
"message": "
|
|
14
|
-
"
|
|
15
|
-
{
|
|
16
|
-
"id": 1,
|
|
17
|
-
"name": "Product A",
|
|
18
|
-
"price": 10000,
|
|
19
|
-
"stock": 50
|
|
20
|
-
},
|
|
21
|
-
{
|
|
22
|
-
"id": 2,
|
|
23
|
-
"name": "Product B",
|
|
24
|
-
"price": 15000,
|
|
25
|
-
"stock": 30
|
|
26
|
-
}
|
|
27
|
-
]
|
|
32
|
+
"status": "failed",
|
|
33
|
+
"message": "Server error",
|
|
34
|
+
"error_code": "SERVER_ERROR"
|
|
28
35
|
}
|
|
29
36
|
},
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
"
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
"error_code": "SERVER_ERROR"
|
|
37
|
-
}
|
|
38
|
-
},
|
|
39
|
-
{
|
|
40
|
-
"statusCode": 400,
|
|
41
|
-
"body": {
|
|
42
|
-
"status": "failed",
|
|
43
|
-
"message": "Bad request, invalid parameters",
|
|
44
|
-
"error_code": "INVALID_PARAMETERS"
|
|
45
|
-
}
|
|
37
|
+
{
|
|
38
|
+
"statusCode": 400,
|
|
39
|
+
"body": {
|
|
40
|
+
"status": "failed",
|
|
41
|
+
"message": "Bad request, invalid parameters",
|
|
42
|
+
"error_code": "INVALID_PARAMETERS"
|
|
46
43
|
}
|
|
47
|
-
|
|
48
|
-
|
|
44
|
+
}
|
|
45
|
+
]
|
|
49
46
|
}
|
|
50
47
|
}
|
|
51
48
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import { defaultMockConfig } from "./defaultConfig.js";
|
|
3
|
+
|
|
4
|
+
let config = {};
|
|
5
|
+
|
|
6
|
+
export const loadConfig = (path) => {
|
|
7
|
+
if (!path) {
|
|
8
|
+
config = structuredClone(defaultMockConfig);
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (!fs.existsSync(path)) {
|
|
13
|
+
throw new Error(`Mock config file not found: ${path}`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
config = JSON.parse(fs.readFileSync(path, "utf-8"));
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const getConfig = () => config;
|
|
20
|
+
|
|
21
|
+
export const setConfig = (next) => {
|
|
22
|
+
config = next;
|
|
23
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export const defaultMockConfig = {
|
|
2
|
+
"GET /health": {
|
|
3
|
+
behavior: {
|
|
4
|
+
successRate: 100,
|
|
5
|
+
delay: 3000, // milliseconds
|
|
6
|
+
},
|
|
7
|
+
responses: {
|
|
8
|
+
success: {
|
|
9
|
+
statusCode: 200,
|
|
10
|
+
body: {
|
|
11
|
+
status: "success",
|
|
12
|
+
message: "Life is good!",
|
|
13
|
+
data: { healthy: true },
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
errors: [
|
|
17
|
+
{
|
|
18
|
+
statusCode: 500,
|
|
19
|
+
body: {
|
|
20
|
+
status: "error",
|
|
21
|
+
message: "Internal server error",
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
"GET /products": {
|
|
29
|
+
behavior: {
|
|
30
|
+
successRate: 50,
|
|
31
|
+
},
|
|
32
|
+
responses: {
|
|
33
|
+
success: {
|
|
34
|
+
statusCode: 200,
|
|
35
|
+
body: {
|
|
36
|
+
status: "success",
|
|
37
|
+
data: [
|
|
38
|
+
{ id: 1, name: "Product A", price: 120000 },
|
|
39
|
+
{ id: 2, name: "Product B", price: 90000 },
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
errors: [
|
|
44
|
+
{
|
|
45
|
+
statusCode: 500,
|
|
46
|
+
body: {
|
|
47
|
+
status: "error",
|
|
48
|
+
message: "Internal server error",
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
};
|
package/src/app.js
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import express from "express";
|
|
2
|
-
import morgan from "morgan";
|
|
3
|
-
import helmet from "helmet";
|
|
4
|
-
import cors from "cors";
|
|
5
|
-
|
|
6
|
-
// define routes here
|
|
7
|
-
import mockRoutes from "./routes/mock.routes.js";
|
|
8
|
-
|
|
9
|
-
import "dotenv/config";
|
|
10
|
-
|
|
11
|
-
export default function createApp(configPath) {
|
|
12
|
-
const app = express();
|
|
13
|
-
|
|
14
|
-
// ==== MIDDLEWARES ====
|
|
15
|
-
app.use(express.json());
|
|
16
|
-
app.use(express.urlencoded({ extended: true }));
|
|
17
|
-
app.use(morgan("dev"));
|
|
18
|
-
app.use(cors());
|
|
19
|
-
app.use(helmet());
|
|
20
|
-
|
|
21
|
-
app.use((req, _res, next) => {
|
|
22
|
-
req.mockConfigPath = configPath;
|
|
23
|
-
next();
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
app.use(mockRoutes);
|
|
27
|
-
|
|
28
|
-
return app;
|
|
29
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { handle } from "../services/mock.service.js";
|
|
2
|
-
|
|
3
|
-
export const handleRequest = async (req, res, next) => {
|
|
4
|
-
try {
|
|
5
|
-
// call service to handle request
|
|
6
|
-
const data = handle(req.mockConfigPath, req.method, req.path);
|
|
7
|
-
console.log("🚀 ~ handleRequest ~ data:", data);
|
|
8
|
-
|
|
9
|
-
if (!data) {
|
|
10
|
-
return res.status(404).json({
|
|
11
|
-
status: "failed",
|
|
12
|
-
message: `No mock defined for ${req.method} ${req.path}`,
|
|
13
|
-
});
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
// return response
|
|
17
|
-
return res.status(data.statusCode).json(data.body);
|
|
18
|
-
} catch (err) {
|
|
19
|
-
next(err);
|
|
20
|
-
}
|
|
21
|
-
};
|
package/src/server.js
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import createApp from "./app.js";
|
|
2
|
-
|
|
3
|
-
export default ({ configPath, port }) => {
|
|
4
|
-
const server = createApp(configPath);
|
|
5
|
-
|
|
6
|
-
server.listen(port, () => {
|
|
7
|
-
console.log(` ➜ [API] Server running on: http://localhost:${port}`);
|
|
8
|
-
console.log(` ➜ [API] Using config file: ${configPath}`);
|
|
9
|
-
});
|
|
10
|
-
};
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { loadConfig } from "../utils/loadConfig.js";
|
|
2
|
-
import { pickResponse } from "../utils/responsePicker.js";
|
|
3
|
-
|
|
4
|
-
export const handle = (mockConfigPath, method, path) => {
|
|
5
|
-
const config = loadConfig(mockConfigPath);
|
|
6
|
-
const key = `${method} ${path}`;
|
|
7
|
-
|
|
8
|
-
const route = config.routes[key];
|
|
9
|
-
|
|
10
|
-
if (!route) {
|
|
11
|
-
return null;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
return pickResponse(route);
|
|
15
|
-
};
|
package/src/utils/loadConfig.js
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
export const pickResponse = (routeConfig) => {
|
|
2
|
-
const { behavior, responses } = routeConfig;
|
|
3
|
-
const roll = Math.random() * 100;
|
|
4
|
-
|
|
5
|
-
if (roll <= behavior.successRate) {
|
|
6
|
-
return {
|
|
7
|
-
statusCode: responses.success.statusCode,
|
|
8
|
-
body: responses.success.body,
|
|
9
|
-
};
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const errors = responses.errors;
|
|
13
|
-
const error = errors[Math.floor(Math.random() * errors.length)];
|
|
14
|
-
|
|
15
|
-
return {
|
|
16
|
-
statusCode: error.statusCode,
|
|
17
|
-
body: error.body,
|
|
18
|
-
};
|
|
19
|
-
};
|