@ketrics/ketrics-cli 0.2.3 → 0.4.0
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/README.md +623 -607
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/package.json +1 -1
- package/templates/HelloWorld/README.md +83 -106
- package/templates/HelloWorld/backend/package.json +1 -1
- package/templates/HelloWorld/backend/src/database.ts +108 -0
- package/templates/HelloWorld/backend/src/excel.ts +118 -0
- package/templates/HelloWorld/backend/src/http.ts +22 -0
- package/templates/HelloWorld/backend/src/index.ts +105 -29
- package/templates/HelloWorld/backend/src/jobs.ts +47 -0
- package/templates/HelloWorld/backend/src/messages.ts +59 -0
- package/templates/HelloWorld/backend/src/pdf.ts +212 -0
- package/templates/HelloWorld/backend/src/secrets.ts +21 -14
- package/templates/HelloWorld/backend/src/volumes.ts +107 -0
- package/templates/HelloWorld/frontend/package.json +1 -3
- package/templates/HelloWorld/frontend/src/App.css +62 -37
- package/templates/HelloWorld/frontend/src/App.tsx +131 -111
- package/templates/HelloWorld/frontend/src/mocks/handlers.ts +149 -0
- package/templates/HelloWorld/frontend/src/mocks/mock-client.ts +45 -0
- package/templates/HelloWorld/frontend/src/services/index.ts +38 -20
- package/templates/HelloWorld/frontend/src/vite-env.d.ts +1 -0
- package/templates/HelloWorld/tests/test.createInvoicePdf.json +18 -0
- package/templates/HelloWorld/tests/{test.getSecretWithoutGrant.json → test.createSimplePdf.json} +4 -2
- package/templates/HelloWorld/tests/test.createSpreadsheet.json +11 -0
- package/templates/HelloWorld/tests/test.echo.json +2 -2
- package/templates/HelloWorld/tests/test.fetchExternalApi.json +13 -0
- package/templates/HelloWorld/tests/test.getSecret.json +5 -1
- package/templates/HelloWorld/tests/test.info.json +3 -1
- package/templates/HelloWorld/tests/test.listFiles.json +13 -0
- package/templates/HelloWorld/tests/{test.echo2.json → test.queryUsers.json} +3 -3
- package/templates/HelloWorld/tests/{test.greet.json → test.readFile.json} +2 -2
- package/templates/HelloWorld/tests/{test.testWriteFileWithoutVolumeGrant.json → test.saveFile.json} +4 -2
- package/templates/HelloWorld/tests/test.sendNotification.json +14 -0
- package/templates/HelloWorld/backend/src/volume.ts +0 -55
|
@@ -1,141 +1,161 @@
|
|
|
1
1
|
import { useState } from "react";
|
|
2
|
-
import { z } from "zod";
|
|
3
2
|
import { apiClient } from "./services";
|
|
4
3
|
|
|
5
|
-
const inputSchema = z.string().min(1, "Input is required").max(100, "Max 100 characters");
|
|
6
|
-
|
|
7
4
|
function App() {
|
|
8
|
-
const [input, setInput] = useState("");
|
|
9
5
|
const [result, setResult] = useState<string | null>(null);
|
|
10
6
|
const [error, setError] = useState<string | null>(null);
|
|
7
|
+
const [loading, setLoading] = useState(false);
|
|
11
8
|
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
if (!validation.success) {
|
|
15
|
-
setError(validation.error.errors[0].message);
|
|
16
|
-
setResult(null);
|
|
17
|
-
return;
|
|
18
|
-
}
|
|
9
|
+
const runFunction = async (fnName: string, payload?: unknown) => {
|
|
10
|
+
setLoading(true);
|
|
19
11
|
setError(null);
|
|
20
|
-
setResult(
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
const handleRunEcho = async () => {
|
|
24
|
-
try {
|
|
25
|
-
const data = await apiClient.run("echo");
|
|
26
|
-
console.log("API Data:", data);
|
|
27
|
-
} catch (err) {
|
|
28
|
-
console.error("API Error:", err);
|
|
29
|
-
}
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
const handleRunSaveFile = async () => {
|
|
33
|
-
try {
|
|
34
|
-
const data = await apiClient.run("saveFile");
|
|
35
|
-
console.log("API Data:", data);
|
|
36
|
-
} catch (err) {
|
|
37
|
-
console.error("API Error:", err);
|
|
38
|
-
}
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
const handleRunReadFile = async () => {
|
|
42
|
-
try {
|
|
43
|
-
const data = await apiClient.run("readFile");
|
|
44
|
-
console.log("API Data:", data);
|
|
45
|
-
} catch (err) {
|
|
46
|
-
console.error("API Error:", err);
|
|
47
|
-
}
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
const handleRunGenerateDownloadUrl = async () => {
|
|
51
|
-
try {
|
|
52
|
-
const data = await apiClient.run("generateDownloadUrl");
|
|
53
|
-
// Trigger download of the file in the url data.result.url
|
|
54
|
-
const downloadUrl = data.result.url;
|
|
55
|
-
const link = document.createElement("a");
|
|
56
|
-
link.href = downloadUrl;
|
|
57
|
-
link.target = "_blank";
|
|
58
|
-
link.download = "data.json";
|
|
59
|
-
document.body.appendChild(link);
|
|
60
|
-
link.click();
|
|
61
|
-
document.body.removeChild(link);
|
|
62
|
-
console.log("downloadUrl:", downloadUrl);
|
|
63
|
-
} catch (err) {
|
|
64
|
-
console.error("API Error:", err);
|
|
65
|
-
}
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
const handleRunTestWriteFileWithoutVolumeGrant = async () => {
|
|
12
|
+
setResult(null);
|
|
69
13
|
try {
|
|
70
|
-
const data = await apiClient.run(
|
|
71
|
-
|
|
14
|
+
const data = await apiClient.run(fnName, payload);
|
|
15
|
+
setResult(JSON.stringify(data, null, 2));
|
|
72
16
|
} catch (err) {
|
|
73
|
-
|
|
17
|
+
setError(err instanceof Error ? err.message : "An error occurred");
|
|
18
|
+
} finally {
|
|
19
|
+
setLoading(false);
|
|
74
20
|
}
|
|
75
21
|
};
|
|
76
22
|
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
} catch (err) {
|
|
82
|
-
console.error("Secret Error:", err);
|
|
83
|
-
}
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
const handleGetSecretWithoutGrant = async () => {
|
|
23
|
+
const handleDownload = async (fnName: string, payload?: unknown) => {
|
|
24
|
+
setLoading(true);
|
|
25
|
+
setError(null);
|
|
26
|
+
setResult(null);
|
|
87
27
|
try {
|
|
88
|
-
const data = await apiClient.run(
|
|
89
|
-
|
|
28
|
+
const data = await apiClient.run(fnName, payload);
|
|
29
|
+
const downloadUrl = data?.result?.downloadUrl;
|
|
30
|
+
if (downloadUrl) {
|
|
31
|
+
const link = document.createElement("a");
|
|
32
|
+
link.href = downloadUrl;
|
|
33
|
+
link.target = "_blank";
|
|
34
|
+
document.body.appendChild(link);
|
|
35
|
+
link.click();
|
|
36
|
+
document.body.removeChild(link);
|
|
37
|
+
setResult(`Download started. URL: ${downloadUrl}`);
|
|
38
|
+
} else {
|
|
39
|
+
setResult(JSON.stringify(data, null, 2));
|
|
40
|
+
}
|
|
90
41
|
} catch (err) {
|
|
91
|
-
|
|
42
|
+
setError(err instanceof Error ? err.message : "An error occurred");
|
|
43
|
+
} finally {
|
|
44
|
+
setLoading(false);
|
|
92
45
|
}
|
|
93
46
|
};
|
|
94
47
|
|
|
95
48
|
return (
|
|
96
49
|
<div className="app">
|
|
97
|
-
<h1>Ketrics
|
|
98
|
-
<p className="subtitle">
|
|
99
|
-
|
|
100
|
-
<div className="
|
|
101
|
-
<
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
</
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
50
|
+
<h1>Ketrics Application</h1>
|
|
51
|
+
<p className="subtitle">SDK Feature Examples</p>
|
|
52
|
+
|
|
53
|
+
<div className="sections">
|
|
54
|
+
<div className="section">
|
|
55
|
+
<h2>General</h2>
|
|
56
|
+
<div className="button-group">
|
|
57
|
+
<button onClick={() => runFunction("echo", { message: "Hello!" })} className="button" disabled={loading}>
|
|
58
|
+
Echo
|
|
59
|
+
</button>
|
|
60
|
+
<button onClick={() => runFunction("info")} className="button" disabled={loading}>
|
|
61
|
+
Info
|
|
62
|
+
</button>
|
|
63
|
+
<button onClick={() => runFunction("fetchExternalApi")} className="button" disabled={loading}>
|
|
64
|
+
HTTP Request
|
|
65
|
+
</button>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
<div className="section">
|
|
70
|
+
<h2>Volumes</h2>
|
|
71
|
+
<div className="button-group">
|
|
72
|
+
<button onClick={() => runFunction("saveFile")} className="button" disabled={loading}>
|
|
73
|
+
Save File
|
|
74
|
+
</button>
|
|
75
|
+
<button onClick={() => runFunction("readFile")} className="button" disabled={loading}>
|
|
76
|
+
Read File
|
|
77
|
+
</button>
|
|
78
|
+
<button onClick={() => runFunction("listFiles", { prefix: "output/" })} className="button" disabled={loading}>
|
|
79
|
+
List Files
|
|
80
|
+
</button>
|
|
81
|
+
<button onClick={() => handleDownload("generateDownloadUrl")} className="button" disabled={loading}>
|
|
82
|
+
Download File
|
|
83
|
+
</button>
|
|
84
|
+
<button onClick={() => runFunction("copyFile")} className="button" disabled={loading}>
|
|
85
|
+
Copy File
|
|
86
|
+
</button>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
<div className="section">
|
|
91
|
+
<h2>Database</h2>
|
|
92
|
+
<div className="button-group">
|
|
93
|
+
<button onClick={() => runFunction("queryUsers", { limit: 5 })} className="button" disabled={loading}>
|
|
94
|
+
Query Users
|
|
95
|
+
</button>
|
|
96
|
+
<button onClick={() => runFunction("insertRecord", { name: "Test User", email: "test@example.com" })} className="button" disabled={loading}>
|
|
97
|
+
Insert Record
|
|
98
|
+
</button>
|
|
99
|
+
<button onClick={() => runFunction("transferFunds", { fromAccountId: 1, toAccountId: 2, amount: 100 })} className="button" disabled={loading}>
|
|
100
|
+
Transfer Funds
|
|
101
|
+
</button>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<div className="section">
|
|
106
|
+
<h2>Documents</h2>
|
|
107
|
+
<div className="button-group">
|
|
108
|
+
<button onClick={() => handleDownload("createSimplePdf")} className="button" disabled={loading}>
|
|
109
|
+
Create PDF
|
|
110
|
+
</button>
|
|
111
|
+
<button onClick={() => handleDownload("createInvoicePdf")} className="button" disabled={loading}>
|
|
112
|
+
Create Invoice PDF
|
|
113
|
+
</button>
|
|
114
|
+
<button onClick={() => handleDownload("createSpreadsheet")} className="button" disabled={loading}>
|
|
115
|
+
Create Spreadsheet
|
|
116
|
+
</button>
|
|
117
|
+
<button onClick={() => handleDownload("exportDataToExcel")} className="button" disabled={loading}>
|
|
118
|
+
Export to Excel
|
|
119
|
+
</button>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<div className="section">
|
|
124
|
+
<h2>Messaging & Jobs</h2>
|
|
125
|
+
<div className="button-group">
|
|
126
|
+
<button onClick={() => runFunction("sendNotification", { subject: "Test", body: "Hello from the app!" })} className="button" disabled={loading}>
|
|
127
|
+
Send Notification
|
|
128
|
+
</button>
|
|
129
|
+
<button onClick={() => runFunction("scheduleBackgroundJob")} className="button" disabled={loading}>
|
|
130
|
+
Schedule Job
|
|
131
|
+
</button>
|
|
132
|
+
<button onClick={() => runFunction("getSecret")} className="button" disabled={loading}>
|
|
133
|
+
Get Secret
|
|
134
|
+
</button>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
127
137
|
</div>
|
|
128
138
|
|
|
139
|
+
{loading && <p className="loading">Running...</p>}
|
|
129
140
|
{error && <p className="error">{error}</p>}
|
|
130
|
-
{result &&
|
|
141
|
+
{result && (
|
|
142
|
+
<div className="result">
|
|
143
|
+
<pre>{result}</pre>
|
|
144
|
+
</div>
|
|
145
|
+
)}
|
|
131
146
|
|
|
132
147
|
<div className="info">
|
|
133
148
|
<h2>About</h2>
|
|
134
|
-
<p>This
|
|
149
|
+
<p>This application demonstrates the Ketrics SDK features:</p>
|
|
135
150
|
<ul>
|
|
136
|
-
<li>
|
|
137
|
-
<li>
|
|
138
|
-
<li>
|
|
151
|
+
<li>Volume storage (save, read, list, download, copy files)</li>
|
|
152
|
+
<li>Database connections (query, insert, transactions)</li>
|
|
153
|
+
<li>PDF document generation (simple and invoice-style)</li>
|
|
154
|
+
<li>Excel workbook creation (single and multi-sheet)</li>
|
|
155
|
+
<li>User messaging and notifications</li>
|
|
156
|
+
<li>Background job scheduling</li>
|
|
157
|
+
<li>Secret management</li>
|
|
158
|
+
<li>External HTTP requests</li>
|
|
139
159
|
</ul>
|
|
140
160
|
</div>
|
|
141
161
|
</div>
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mock Backend Handlers
|
|
3
|
+
*
|
|
4
|
+
* Define mock responses for your backend functions here.
|
|
5
|
+
* These are used automatically when running `npm run dev` (local development)
|
|
6
|
+
* and are completely excluded from production builds.
|
|
7
|
+
*
|
|
8
|
+
* Each key corresponds to a backend function name exported from backend/src/index.ts.
|
|
9
|
+
* The handler receives the same payload your backend function would receive
|
|
10
|
+
* and should return the same response shape.
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* 1. Add a handler for each backend function you want to mock
|
|
14
|
+
* 2. Run `npm run dev` - the frontend will use these handlers instead of the runtime API
|
|
15
|
+
* 3. Run `npm run build` / `ketrics deploy` - mock code is stripped out entirely
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
type MockHandler = (payload?: unknown) => unknown | Promise<unknown>;
|
|
19
|
+
|
|
20
|
+
const handlers: Record<string, MockHandler> = {
|
|
21
|
+
echo: (payload) => ({
|
|
22
|
+
payload,
|
|
23
|
+
context: {
|
|
24
|
+
tenant: { id: "mock-tenant-id", code: "mock-tenant", name: "Mock Tenant" },
|
|
25
|
+
application: { id: "mock-app-id", code: "mock-app", name: "Mock App", version: "1.0.0", deploymentId: "mock-deploy" },
|
|
26
|
+
requestor: { type: "user", userId: "mock-user-id", email: "dev@localhost", name: "Local Developer", applicationPermissions: [] },
|
|
27
|
+
runtime: { nodeVersion: "18.x", runtime: "mock", region: "local" },
|
|
28
|
+
environment: {},
|
|
29
|
+
},
|
|
30
|
+
}),
|
|
31
|
+
|
|
32
|
+
info: () => ({
|
|
33
|
+
tenant: { id: "mock-tenant-id", code: "mock-tenant", name: "Mock Tenant" },
|
|
34
|
+
application: { id: "mock-app-id", code: "mock-app", name: "Mock App", version: "1.0.0" },
|
|
35
|
+
runtime: { nodeVersion: "18.x", runtime: "mock", region: "local" },
|
|
36
|
+
}),
|
|
37
|
+
|
|
38
|
+
fetchExternalApi: () => ({
|
|
39
|
+
data: { id: 1, title: "Mock Post", body: "This is a mock response from the local dev server." },
|
|
40
|
+
fetchedAt: new Date().toISOString(),
|
|
41
|
+
}),
|
|
42
|
+
|
|
43
|
+
// --- Volumes ---
|
|
44
|
+
|
|
45
|
+
saveFile: () => ({
|
|
46
|
+
jsonFile: { key: "output/data.json", etag: "mock-etag-1", size: 128 },
|
|
47
|
+
textFile: { key: "output/hello.txt", etag: "mock-etag-2", size: 19 },
|
|
48
|
+
}),
|
|
49
|
+
|
|
50
|
+
readFile: () => ({
|
|
51
|
+
contentType: "application/json",
|
|
52
|
+
contentLength: 128,
|
|
53
|
+
lastModified: new Date().toISOString(),
|
|
54
|
+
parsed: { generatedBy: "mock-app", generatedAt: new Date().toISOString(), tenant: "mock-tenant" },
|
|
55
|
+
}),
|
|
56
|
+
|
|
57
|
+
listFiles: (payload: unknown) => {
|
|
58
|
+
const { prefix } = (payload as { prefix?: string }) || {};
|
|
59
|
+
return {
|
|
60
|
+
files: [
|
|
61
|
+
{ key: `${prefix || "output/"}data.json`, size: 128, lastModified: new Date().toISOString(), contentType: "application/json" },
|
|
62
|
+
{ key: `${prefix || "output/"}hello.txt`, size: 19, lastModified: new Date().toISOString(), contentType: "text/plain" },
|
|
63
|
+
],
|
|
64
|
+
count: 2,
|
|
65
|
+
isTruncated: false,
|
|
66
|
+
};
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
generateDownloadUrl: () => ({
|
|
70
|
+
url: "https://example.com/mock-download-url",
|
|
71
|
+
expiresAt: new Date(Date.now() + 3600_000).toISOString(),
|
|
72
|
+
}),
|
|
73
|
+
|
|
74
|
+
copyFile: () => ({
|
|
75
|
+
sourceKey: "output/data.json",
|
|
76
|
+
destinationKey: "backup/data-backup.json",
|
|
77
|
+
etag: "mock-etag-copy",
|
|
78
|
+
}),
|
|
79
|
+
|
|
80
|
+
// --- Database ---
|
|
81
|
+
|
|
82
|
+
queryUsers: (payload: unknown) => {
|
|
83
|
+
const { limit } = (payload as { limit?: number }) || {};
|
|
84
|
+
const users = [
|
|
85
|
+
{ id: 1, name: "Alice Johnson", email: "alice@example.com" },
|
|
86
|
+
{ id: 2, name: "Bob Smith", email: "bob@example.com" },
|
|
87
|
+
{ id: 3, name: "Carol Davis", email: "carol@example.com" },
|
|
88
|
+
{ id: 4, name: "Dan Wilson", email: "dan@example.com" },
|
|
89
|
+
{ id: 5, name: "Eve Martinez", email: "eve@example.com" },
|
|
90
|
+
];
|
|
91
|
+
const sliced = users.slice(0, limit || 10);
|
|
92
|
+
return { users: sliced, rowCount: sliced.length };
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
insertRecord: (payload: unknown) => {
|
|
96
|
+
const { name, email } = (payload as { name?: string; email?: string }) || {};
|
|
97
|
+
if (!name || !email) throw new Error("name and email are required");
|
|
98
|
+
return { affectedRows: 1, insertId: Math.floor(Math.random() * 1000) + 100 };
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
transferFunds: (payload: unknown) => {
|
|
102
|
+
const { amount } = (payload as { amount?: number }) || {};
|
|
103
|
+
if (!amount || amount <= 0) throw new Error("Amount must be positive");
|
|
104
|
+
return { transferred: amount };
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
// --- Documents ---
|
|
108
|
+
|
|
109
|
+
createSimplePdf: () => ({
|
|
110
|
+
downloadUrl: "https://example.com/mock-simple.pdf",
|
|
111
|
+
message: "[Mock] PDF would be generated by the backend",
|
|
112
|
+
}),
|
|
113
|
+
|
|
114
|
+
createInvoicePdf: () => ({
|
|
115
|
+
downloadUrl: "https://example.com/mock-invoice.pdf",
|
|
116
|
+
message: "[Mock] Invoice PDF would be generated by the backend",
|
|
117
|
+
}),
|
|
118
|
+
|
|
119
|
+
createSpreadsheet: () => ({
|
|
120
|
+
downloadUrl: "https://example.com/mock-spreadsheet.xlsx",
|
|
121
|
+
message: "[Mock] Spreadsheet would be generated by the backend",
|
|
122
|
+
}),
|
|
123
|
+
|
|
124
|
+
exportDataToExcel: () => ({
|
|
125
|
+
downloadUrl: "https://example.com/mock-export.xlsx",
|
|
126
|
+
message: "[Mock] Excel export would be generated by the backend",
|
|
127
|
+
}),
|
|
128
|
+
|
|
129
|
+
// --- Messaging & Jobs ---
|
|
130
|
+
|
|
131
|
+
sendNotification: (payload: unknown) => {
|
|
132
|
+
const { subject, body } = (payload as { subject?: string; body?: string }) || {};
|
|
133
|
+
return { sent: true, subject, body, message: "[Mock] Notification would be sent by the backend" };
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
scheduleBackgroundJob: () => ({
|
|
137
|
+
jobId: `mock-job-${Date.now()}`,
|
|
138
|
+
status: "scheduled",
|
|
139
|
+
message: "[Mock] Job would be scheduled by the backend",
|
|
140
|
+
}),
|
|
141
|
+
|
|
142
|
+
getSecret: () => ({
|
|
143
|
+
value: "mock-secret-value",
|
|
144
|
+
message: "[Mock] Secret would be retrieved from the backend",
|
|
145
|
+
}),
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
export { handlers };
|
|
149
|
+
export type { MockHandler };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mock API Client
|
|
3
|
+
*
|
|
4
|
+
* Replaces the real API client during local development (`npm run dev`).
|
|
5
|
+
* Routes function calls to the mock handlers defined in ./handlers.ts
|
|
6
|
+
* instead of making HTTP requests to the Ketrics Runtime API.
|
|
7
|
+
*
|
|
8
|
+
* This file is tree-shaken out of production builds by Vite.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { handlers } from "./handlers";
|
|
12
|
+
|
|
13
|
+
class MockAPIClient {
|
|
14
|
+
async run(fnName: string, payload?: unknown) {
|
|
15
|
+
const handler = handlers[fnName];
|
|
16
|
+
|
|
17
|
+
if (!handler) {
|
|
18
|
+
throw new Error(
|
|
19
|
+
`[Mock] No handler defined for "${fnName}". ` +
|
|
20
|
+
`Add it to src/mocks/handlers.ts to use it in local development.`
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Simulate network latency
|
|
25
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const result = await handler(payload);
|
|
29
|
+
console.log(`[Mock] ${fnName}`, { payload, result });
|
|
30
|
+
return { success: true, result };
|
|
31
|
+
} catch (err) {
|
|
32
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
33
|
+
console.error(`[Mock] ${fnName} error:`, message);
|
|
34
|
+
throw new Error(message);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function createMockClient() {
|
|
40
|
+
console.log(
|
|
41
|
+
"%c[Ketrics] Running with mock backend — edit src/mocks/handlers.ts to customize responses",
|
|
42
|
+
"color: #f59e0b; font-weight: bold;"
|
|
43
|
+
);
|
|
44
|
+
return new MockAPIClient();
|
|
45
|
+
}
|
|
@@ -1,25 +1,20 @@
|
|
|
1
1
|
import { createAuthManager } from "@ketrics/sdk-frontend";
|
|
2
|
-
import axios from "axios";
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
auth.initAutoRefresh({
|
|
8
|
-
refreshBuffer: 60,
|
|
9
|
-
onTokenUpdated: (token) => {
|
|
10
|
-
// Update your HTTP client headers
|
|
11
|
-
axios.defaults.headers.common["Authorization"] = `Bearer ${token}`;
|
|
12
|
-
},
|
|
13
|
-
});
|
|
3
|
+
interface APIClientInterface {
|
|
4
|
+
run(fnName: string, payload?: unknown): Promise<unknown>;
|
|
5
|
+
}
|
|
14
6
|
|
|
15
|
-
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Real API Client — used when deployed (production builds)
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
class APIClient implements APIClientInterface {
|
|
16
11
|
private auth;
|
|
17
12
|
|
|
18
|
-
constructor(authManager: typeof
|
|
13
|
+
constructor(authManager: ReturnType<typeof createAuthManager>) {
|
|
19
14
|
this.auth = authManager;
|
|
20
15
|
}
|
|
21
16
|
|
|
22
|
-
private async makeRequest(fnName: string) {
|
|
17
|
+
private async makeRequest(fnName: string, payload?: unknown) {
|
|
23
18
|
const runtimeApiUrl = this.auth.getRuntimeApiUrl();
|
|
24
19
|
const tenantId = this.auth.getTenantId();
|
|
25
20
|
const applicationId = this.auth.getApplicationId();
|
|
@@ -32,24 +27,47 @@ class APIClient {
|
|
|
32
27
|
const response = await fetch(`${runtimeApiUrl}/tenants/${tenantId}/applications/${applicationId}/functions/${fnName}`, {
|
|
33
28
|
method: "POST",
|
|
34
29
|
headers: {
|
|
35
|
-
Authorization: `Bearer ${accessToken}`,
|
|
30
|
+
"Authorization": `Bearer ${accessToken}`,
|
|
31
|
+
"Content-Type": "application/json",
|
|
36
32
|
},
|
|
33
|
+
body: JSON.stringify({ payload: payload ?? null }),
|
|
37
34
|
});
|
|
38
35
|
|
|
39
36
|
if (!response.ok) {
|
|
40
|
-
|
|
37
|
+
const errorBody = await response.json().catch(() => null);
|
|
38
|
+
const message = errorBody?.error?.message || `API request failed with status ${response.status}`;
|
|
39
|
+
throw new Error(message);
|
|
41
40
|
}
|
|
42
41
|
|
|
43
42
|
return response.json();
|
|
44
43
|
}
|
|
45
44
|
|
|
46
|
-
async run(fnName: string) {
|
|
47
|
-
|
|
45
|
+
async run(fnName: string, payload?: unknown) {
|
|
46
|
+
return this.makeRequest(fnName, payload);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
48
49
|
|
|
49
|
-
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// Client initialization — mock in dev, real in production
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
async function createClient(): Promise<APIClientInterface> {
|
|
54
|
+
if (import.meta.env.DEV) {
|
|
55
|
+
// Dynamic import keeps mock code in a separate chunk that Vite
|
|
56
|
+
// completely excludes from production builds.
|
|
57
|
+
const { createMockClient } = await import("../mocks/mock-client");
|
|
58
|
+
return createMockClient();
|
|
50
59
|
}
|
|
60
|
+
|
|
61
|
+
const auth = createAuthManager();
|
|
62
|
+
auth.initAutoRefresh({
|
|
63
|
+
refreshBuffer: 60,
|
|
64
|
+
onTokenUpdated: () => {
|
|
65
|
+
// Token is refreshed automatically and retrieved via getAccessToken()
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
return new APIClient(auth);
|
|
51
69
|
}
|
|
52
70
|
|
|
53
|
-
const apiClient =
|
|
71
|
+
const apiClient = await createClient();
|
|
54
72
|
|
|
55
73
|
export { apiClient };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"endpoint": "/tenants/{{tenantId}}/applications/{{applicationId}}/functions/createInvoicePdf",
|
|
3
|
+
"method": "POST",
|
|
4
|
+
"headers": {
|
|
5
|
+
"Authorization": "Bearer {{token}}",
|
|
6
|
+
"Content-Type": "application/json"
|
|
7
|
+
},
|
|
8
|
+
"body": {
|
|
9
|
+
"payload": {
|
|
10
|
+
"invoiceNumber": "INV-100",
|
|
11
|
+
"customerName": "Acme Corp",
|
|
12
|
+
"items": [
|
|
13
|
+
{ "description": "Consulting", "quantity": 10, "price": 200 },
|
|
14
|
+
{ "description": "Development", "quantity": 40, "price": 150 }
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
package/templates/HelloWorld/tests/{test.getSecretWithoutGrant.json → test.createSimplePdf.json}
RENAMED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
{
|
|
2
|
-
"endpoint": "/tenants/{{tenantId}}/applications/{{applicationId}}/functions/
|
|
2
|
+
"endpoint": "/tenants/{{tenantId}}/applications/{{applicationId}}/functions/createSimplePdf",
|
|
3
3
|
"method": "POST",
|
|
4
4
|
"headers": {
|
|
5
5
|
"Authorization": "Bearer {{token}}",
|
|
6
6
|
"Content-Type": "application/json"
|
|
7
7
|
},
|
|
8
|
-
"body": {
|
|
8
|
+
"body": {
|
|
9
|
+
"payload": null
|
|
10
|
+
}
|
|
9
11
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"endpoint": "/tenants/{{tenantId}}/applications/{{applicationId}}/functions/fetchExternalApi",
|
|
3
|
+
"method": "POST",
|
|
4
|
+
"headers": {
|
|
5
|
+
"Authorization": "Bearer {{token}}",
|
|
6
|
+
"Content-Type": "application/json"
|
|
7
|
+
},
|
|
8
|
+
"body": {
|
|
9
|
+
"payload": {
|
|
10
|
+
"url": "https://jsonplaceholder.typicode.com/posts/1"
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"endpoint": "/tenants/{{tenantId}}/applications/{{applicationId}}/functions/listFiles",
|
|
3
|
+
"method": "POST",
|
|
4
|
+
"headers": {
|
|
5
|
+
"Authorization": "Bearer {{token}}",
|
|
6
|
+
"Content-Type": "application/json"
|
|
7
|
+
},
|
|
8
|
+
"body": {
|
|
9
|
+
"payload": {
|
|
10
|
+
"prefix": "output/"
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|