@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
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Background Job Examples
|
|
3
|
+
*
|
|
4
|
+
* Demonstrates scheduling and monitoring background jobs using ketrics.Job.
|
|
5
|
+
* Background jobs run asynchronously and can have longer timeouts (up to 15 minutes).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Schedule a function to run in the background.
|
|
10
|
+
*/
|
|
11
|
+
const scheduleBackgroundJob = async (payload: {
|
|
12
|
+
functionName?: string;
|
|
13
|
+
data?: Record<string, unknown>;
|
|
14
|
+
}) => {
|
|
15
|
+
const jobId = await ketrics.Job.runInBackground({
|
|
16
|
+
function: payload?.functionName || "echo",
|
|
17
|
+
payload: payload?.data || { source: "background-job" },
|
|
18
|
+
options: {
|
|
19
|
+
timeout: 60000, // 1 minute timeout
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
return { jobId, status: "scheduled" };
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Check the status of a background job.
|
|
28
|
+
*/
|
|
29
|
+
const getJobStatus = async (payload: { jobId: string }) => {
|
|
30
|
+
if (!payload?.jobId) {
|
|
31
|
+
throw new Error("jobId is required");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const status = await ketrics.Job.getStatus(payload.jobId);
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
jobId: status.jobId,
|
|
38
|
+
functionName: status.functionName,
|
|
39
|
+
status: status.status,
|
|
40
|
+
createdAt: status.createdAt,
|
|
41
|
+
startedAt: status.startedAt,
|
|
42
|
+
completedAt: status.completedAt,
|
|
43
|
+
error: status.error,
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export { scheduleBackgroundJob, getJobStatus };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Messages Examples
|
|
3
|
+
*
|
|
4
|
+
* Demonstrates sending notifications to users via ketrics.Messages.
|
|
5
|
+
* Messages appear in the user's inbox within the Ketrics portal.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Send a notification to the current user.
|
|
10
|
+
*/
|
|
11
|
+
const sendNotification = async (payload: { subject?: string; body?: string }) => {
|
|
12
|
+
if (ketrics.requestor.type !== "USER") {
|
|
13
|
+
throw new Error("This function can only be called by a user");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const result = await ketrics.Messages.send({
|
|
17
|
+
userId: ketrics.requestor.userId!,
|
|
18
|
+
type: "notification",
|
|
19
|
+
subject: payload?.subject || "Hello from your app!",
|
|
20
|
+
body: payload?.body || `This notification was sent by **${ketrics.application.name}**.`,
|
|
21
|
+
priority: "MEDIUM",
|
|
22
|
+
channels: { inbox: true, push: false },
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
messageId: result.messageId,
|
|
27
|
+
status: result.status,
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Send a notification to multiple users.
|
|
33
|
+
*/
|
|
34
|
+
const sendBulkNotification = async (payload: {
|
|
35
|
+
userIds: string[];
|
|
36
|
+
subject: string;
|
|
37
|
+
body: string;
|
|
38
|
+
}) => {
|
|
39
|
+
if (!payload?.userIds?.length) {
|
|
40
|
+
throw new Error("userIds array is required");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const result = await ketrics.Messages.sendBulk({
|
|
44
|
+
userIds: payload.userIds,
|
|
45
|
+
type: "announcement",
|
|
46
|
+
subject: payload.subject,
|
|
47
|
+
body: payload.body,
|
|
48
|
+
priority: "HIGH",
|
|
49
|
+
channels: { inbox: true, push: true },
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
total: result.total,
|
|
54
|
+
sent: result.sent,
|
|
55
|
+
failed: result.failed,
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export { sendNotification, sendBulkNotification };
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PDF Examples
|
|
3
|
+
*
|
|
4
|
+
* Demonstrates PDF document creation using ketrics.Pdf.
|
|
5
|
+
* Generated PDFs can be saved to a volume for download.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Create a simple PDF document and save it to a volume.
|
|
10
|
+
*/
|
|
11
|
+
const createSimplePdf = async () => {
|
|
12
|
+
const doc = await ketrics.Pdf.create();
|
|
13
|
+
|
|
14
|
+
// Set document metadata
|
|
15
|
+
doc.setTitle("Sample Document");
|
|
16
|
+
doc.setAuthor(ketrics.tenant.name);
|
|
17
|
+
doc.setCreator(ketrics.application.name);
|
|
18
|
+
|
|
19
|
+
// Add a page
|
|
20
|
+
const page = doc.addPage({ size: "A4" });
|
|
21
|
+
|
|
22
|
+
// Embed a standard font
|
|
23
|
+
const font = await doc.embedStandardFont("Helvetica");
|
|
24
|
+
const boldFont = await doc.embedStandardFont("Helvetica-Bold");
|
|
25
|
+
|
|
26
|
+
// Draw title
|
|
27
|
+
page.drawText("Hello from Ketrics!", {
|
|
28
|
+
x: 50,
|
|
29
|
+
y: 780,
|
|
30
|
+
size: 28,
|
|
31
|
+
font: boldFont,
|
|
32
|
+
color: ketrics.Pdf.rgb(0.2, 0.2, 0.6),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Draw subtitle
|
|
36
|
+
page.drawText(`Generated by ${ketrics.application.name} for ${ketrics.tenant.name}`, {
|
|
37
|
+
x: 50,
|
|
38
|
+
y: 750,
|
|
39
|
+
size: 12,
|
|
40
|
+
font,
|
|
41
|
+
color: ketrics.Pdf.rgb(0.4, 0.4, 0.4),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Draw a decorative line
|
|
45
|
+
page.drawLine({
|
|
46
|
+
start: { x: 50, y: 740 },
|
|
47
|
+
end: { x: 545, y: 740 },
|
|
48
|
+
thickness: 2,
|
|
49
|
+
color: ketrics.Pdf.rgb(0.2, 0.2, 0.6),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Draw body text
|
|
53
|
+
page.drawText("This PDF was generated dynamically using the Ketrics PDF SDK.", {
|
|
54
|
+
x: 50,
|
|
55
|
+
y: 710,
|
|
56
|
+
size: 14,
|
|
57
|
+
font,
|
|
58
|
+
color: ketrics.Pdf.rgb(0, 0, 0),
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
page.drawText(`Date: ${new Date().toISOString()}`, {
|
|
62
|
+
x: 50,
|
|
63
|
+
y: 685,
|
|
64
|
+
size: 11,
|
|
65
|
+
font,
|
|
66
|
+
color: ketrics.Pdf.rgb(0.3, 0.3, 0.3),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Save to volume
|
|
70
|
+
const buffer = await doc.toBuffer();
|
|
71
|
+
const volume = await ketrics.Volume.connect("test-volume");
|
|
72
|
+
const result = await volume.put("documents/sample.pdf", buffer, {
|
|
73
|
+
contentType: "application/pdf",
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Generate download URL
|
|
77
|
+
const downloadUrl = await volume.generateDownloadUrl("documents/sample.pdf", {
|
|
78
|
+
expiresIn: 3600,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
file: { key: result.key, size: result.size },
|
|
83
|
+
downloadUrl: downloadUrl.url,
|
|
84
|
+
pageCount: doc.getPageCount(),
|
|
85
|
+
};
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Create an invoice-style PDF with structured data.
|
|
90
|
+
*/
|
|
91
|
+
const createInvoicePdf = async (payload: {
|
|
92
|
+
invoiceNumber?: string;
|
|
93
|
+
customerName?: string;
|
|
94
|
+
items?: Array<{ description: string; quantity: number; price: number }>;
|
|
95
|
+
}) => {
|
|
96
|
+
const invoiceNumber = payload?.invoiceNumber || "INV-001";
|
|
97
|
+
const customerName = payload?.customerName || "Sample Customer";
|
|
98
|
+
const items = payload?.items || [
|
|
99
|
+
{ description: "Web Development", quantity: 40, price: 150 },
|
|
100
|
+
{ description: "Design Services", quantity: 20, price: 120 },
|
|
101
|
+
{ description: "Consulting", quantity: 10, price: 200 },
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
const doc = await ketrics.Pdf.create();
|
|
105
|
+
doc.setTitle(`Invoice ${invoiceNumber}`);
|
|
106
|
+
doc.setAuthor(ketrics.tenant.name);
|
|
107
|
+
|
|
108
|
+
const page = doc.addPage({ size: "A4" });
|
|
109
|
+
const font = await doc.embedStandardFont("Helvetica");
|
|
110
|
+
const boldFont = await doc.embedStandardFont("Helvetica-Bold");
|
|
111
|
+
|
|
112
|
+
let y = 780;
|
|
113
|
+
|
|
114
|
+
// Header
|
|
115
|
+
page.drawText("INVOICE", {
|
|
116
|
+
x: 50, y, size: 32, font: boldFont,
|
|
117
|
+
color: ketrics.Pdf.rgb(0.2, 0.2, 0.6),
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
y -= 35;
|
|
121
|
+
page.drawText(`#${invoiceNumber}`, {
|
|
122
|
+
x: 50, y, size: 14, font,
|
|
123
|
+
color: ketrics.Pdf.rgb(0.4, 0.4, 0.4),
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// Company info (right side)
|
|
127
|
+
page.drawText(ketrics.tenant.name, {
|
|
128
|
+
x: 400, y: 780, size: 14, font: boldFont,
|
|
129
|
+
color: ketrics.Pdf.rgb(0, 0, 0),
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
page.drawText(new Date().toLocaleDateString(), {
|
|
133
|
+
x: 400, y: 762, size: 11, font,
|
|
134
|
+
color: ketrics.Pdf.rgb(0.4, 0.4, 0.4),
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Divider
|
|
138
|
+
y -= 20;
|
|
139
|
+
page.drawLine({
|
|
140
|
+
start: { x: 50, y },
|
|
141
|
+
end: { x: 545, y },
|
|
142
|
+
thickness: 1,
|
|
143
|
+
color: ketrics.Pdf.rgb(0.8, 0.8, 0.8),
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Bill To
|
|
147
|
+
y -= 30;
|
|
148
|
+
page.drawText("Bill To:", { x: 50, y, size: 10, font, color: ketrics.Pdf.rgb(0.5, 0.5, 0.5) });
|
|
149
|
+
y -= 18;
|
|
150
|
+
page.drawText(customerName, { x: 50, y, size: 14, font: boldFont, color: ketrics.Pdf.rgb(0, 0, 0) });
|
|
151
|
+
|
|
152
|
+
// Table header
|
|
153
|
+
y -= 40;
|
|
154
|
+
page.drawRectangle({
|
|
155
|
+
x: 50, y: y - 5, width: 495, height: 25,
|
|
156
|
+
color: ketrics.Pdf.rgb(0.93, 0.93, 0.97),
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
page.drawText("Description", { x: 60, y, size: 10, font: boldFont, color: ketrics.Pdf.rgb(0.3, 0.3, 0.3) });
|
|
160
|
+
page.drawText("Qty", { x: 330, y, size: 10, font: boldFont, color: ketrics.Pdf.rgb(0.3, 0.3, 0.3) });
|
|
161
|
+
page.drawText("Price", { x: 400, y, size: 10, font: boldFont, color: ketrics.Pdf.rgb(0.3, 0.3, 0.3) });
|
|
162
|
+
page.drawText("Total", { x: 480, y, size: 10, font: boldFont, color: ketrics.Pdf.rgb(0.3, 0.3, 0.3) });
|
|
163
|
+
|
|
164
|
+
// Table rows
|
|
165
|
+
let grandTotal = 0;
|
|
166
|
+
for (const item of items) {
|
|
167
|
+
y -= 25;
|
|
168
|
+
const lineTotal = item.quantity * item.price;
|
|
169
|
+
grandTotal += lineTotal;
|
|
170
|
+
|
|
171
|
+
page.drawText(item.description, { x: 60, y, size: 11, font, color: ketrics.Pdf.rgb(0, 0, 0) });
|
|
172
|
+
page.drawText(String(item.quantity), { x: 335, y, size: 11, font, color: ketrics.Pdf.rgb(0, 0, 0) });
|
|
173
|
+
page.drawText(`$${item.price.toFixed(2)}`, { x: 395, y, size: 11, font, color: ketrics.Pdf.rgb(0, 0, 0) });
|
|
174
|
+
page.drawText(`$${lineTotal.toFixed(2)}`, { x: 475, y, size: 11, font, color: ketrics.Pdf.rgb(0, 0, 0) });
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Total
|
|
178
|
+
y -= 15;
|
|
179
|
+
page.drawLine({
|
|
180
|
+
start: { x: 380, y },
|
|
181
|
+
end: { x: 545, y },
|
|
182
|
+
thickness: 1,
|
|
183
|
+
color: ketrics.Pdf.rgb(0.8, 0.8, 0.8),
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
y -= 20;
|
|
187
|
+
page.drawText("Total:", { x: 400, y, size: 14, font: boldFont, color: ketrics.Pdf.rgb(0, 0, 0) });
|
|
188
|
+
page.drawText(`$${grandTotal.toFixed(2)}`, {
|
|
189
|
+
x: 470, y, size: 14, font: boldFont,
|
|
190
|
+
color: ketrics.Pdf.rgb(0.2, 0.2, 0.6),
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// Save to volume
|
|
194
|
+
const buffer = await doc.toBuffer();
|
|
195
|
+
const volume = await ketrics.Volume.connect("test-volume");
|
|
196
|
+
const fileName = `invoices/${invoiceNumber}.pdf`;
|
|
197
|
+
const result = await volume.put(fileName, buffer, {
|
|
198
|
+
contentType: "application/pdf",
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const downloadUrl = await volume.generateDownloadUrl(fileName, { expiresIn: 3600 });
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
file: { key: result.key, size: result.size },
|
|
205
|
+
downloadUrl: downloadUrl.url,
|
|
206
|
+
invoiceNumber,
|
|
207
|
+
total: grandTotal,
|
|
208
|
+
itemCount: items.length,
|
|
209
|
+
};
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
export { createSimplePdf, createInvoicePdf };
|
|
@@ -1,17 +1,24 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Secret Examples
|
|
3
|
+
*
|
|
4
|
+
* Demonstrates encrypted secret retrieval using ketrics.Secret.
|
|
5
|
+
* Secrets must be created and granted to the application in the Ketrics portal.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Retrieve an encrypted secret by code.
|
|
10
|
+
*/
|
|
11
|
+
const getSecret = async (payload: { code?: string }) => {
|
|
12
|
+
const secretCode = payload?.code || "apikey";
|
|
13
|
+
const value = await ketrics.Secret.get(secretCode);
|
|
8
14
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
// In production, never return the secret value directly.
|
|
16
|
+
// This is only for demonstration purposes.
|
|
17
|
+
return {
|
|
18
|
+
code: secretCode,
|
|
19
|
+
retrieved: true,
|
|
20
|
+
valueLength: value.length,
|
|
21
|
+
};
|
|
15
22
|
};
|
|
16
23
|
|
|
17
|
-
export { getSecret
|
|
24
|
+
export { getSecret };
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Volume Examples
|
|
3
|
+
*
|
|
4
|
+
* Demonstrates S3-backed file storage operations using ketrics.Volume.
|
|
5
|
+
* Volumes must be granted to the application in the Ketrics portal before use.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Save files to a volume - demonstrates writing JSON and binary data.
|
|
10
|
+
*/
|
|
11
|
+
const saveFile = async () => {
|
|
12
|
+
const volume = await ketrics.Volume.connect("test-volume");
|
|
13
|
+
|
|
14
|
+
// Write JSON content
|
|
15
|
+
const jsonData = {
|
|
16
|
+
generatedBy: ketrics.application.code,
|
|
17
|
+
generatedAt: new Date().toISOString(),
|
|
18
|
+
tenant: ketrics.tenant.code,
|
|
19
|
+
};
|
|
20
|
+
const jsonResult = await volume.put("output/data.json", JSON.stringify(jsonData), {
|
|
21
|
+
contentType: "application/json",
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// Write plain text content
|
|
25
|
+
const textResult = await volume.put("output/hello.txt", Buffer.from("Hello from Ketrics!"), {
|
|
26
|
+
contentType: "text/plain",
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
jsonFile: { key: jsonResult.key, etag: jsonResult.etag, size: jsonResult.size },
|
|
31
|
+
textFile: { key: textResult.key, etag: textResult.etag, size: textResult.size },
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Read a file from a volume - demonstrates reading and parsing stored data.
|
|
37
|
+
*/
|
|
38
|
+
const readFile = async () => {
|
|
39
|
+
const volume = await ketrics.Volume.connect("test-volume");
|
|
40
|
+
|
|
41
|
+
const exists = await volume.exists("output/data.json");
|
|
42
|
+
if (!exists) {
|
|
43
|
+
return { error: "File not found. Run saveFile first." };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const file = await volume.get("output/data.json");
|
|
47
|
+
const content = file.content.toString();
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
contentType: file.contentType,
|
|
51
|
+
contentLength: file.contentLength,
|
|
52
|
+
lastModified: file.lastModified,
|
|
53
|
+
parsed: JSON.parse(content),
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* List files in a volume - demonstrates pagination and prefix filtering.
|
|
59
|
+
*/
|
|
60
|
+
const listFiles = async (payload: { prefix?: string }) => {
|
|
61
|
+
const volume = await ketrics.Volume.connect("test-volume");
|
|
62
|
+
|
|
63
|
+
const result = await volume.list({
|
|
64
|
+
prefix: payload?.prefix || "output/",
|
|
65
|
+
maxResults: 50,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
files: result.files.map((f) => ({
|
|
70
|
+
key: f.key,
|
|
71
|
+
size: f.size,
|
|
72
|
+
lastModified: f.lastModified,
|
|
73
|
+
contentType: f.contentType,
|
|
74
|
+
})),
|
|
75
|
+
count: result.count,
|
|
76
|
+
isTruncated: result.isTruncated,
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Generate a temporary download URL for a file.
|
|
82
|
+
*/
|
|
83
|
+
const generateDownloadUrl = async () => {
|
|
84
|
+
const volume = await ketrics.Volume.connect("test-volume");
|
|
85
|
+
const result = await volume.generateDownloadUrl("output/data.json", {
|
|
86
|
+
expiresIn: 3600, // 1 hour
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return { url: result.url, expiresAt: result.expiresAt };
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Copy a file within a volume.
|
|
94
|
+
*/
|
|
95
|
+
const copyFile = async () => {
|
|
96
|
+
const volume = await ketrics.Volume.connect("test-volume");
|
|
97
|
+
|
|
98
|
+
const result = await volume.copy("output/data.json", "backup/data-backup.json");
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
sourceKey: result.sourceKey,
|
|
102
|
+
destinationKey: result.destinationKey,
|
|
103
|
+
etag: result.etag,
|
|
104
|
+
};
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export { saveFile, readFile, listFiles, generateDownloadUrl, copyFile };
|
|
@@ -11,23 +11,24 @@ body {
|
|
|
11
11
|
min-height: 100vh;
|
|
12
12
|
display: flex;
|
|
13
13
|
justify-content: center;
|
|
14
|
-
align-items:
|
|
14
|
+
align-items: flex-start;
|
|
15
15
|
padding: 20px;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
.app {
|
|
19
19
|
background: white;
|
|
20
20
|
border-radius: 12px;
|
|
21
|
-
padding:
|
|
22
|
-
max-width:
|
|
21
|
+
padding: 32px;
|
|
22
|
+
max-width: 700px;
|
|
23
23
|
width: 100%;
|
|
24
24
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
|
25
|
+
margin-top: 20px;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
h1 {
|
|
28
29
|
color: #333;
|
|
29
|
-
margin-bottom:
|
|
30
|
-
font-size:
|
|
30
|
+
margin-bottom: 4px;
|
|
31
|
+
font-size: 26px;
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
.subtitle {
|
|
@@ -36,86 +37,110 @@ h1 {
|
|
|
36
37
|
font-size: 14px;
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
.
|
|
40
|
+
.sections {
|
|
40
41
|
display: flex;
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
flex-direction: column;
|
|
43
|
+
gap: 20px;
|
|
43
44
|
}
|
|
44
45
|
|
|
45
|
-
.
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
transition: border-color 0.2s;
|
|
46
|
+
.section h2 {
|
|
47
|
+
font-size: 14px;
|
|
48
|
+
color: #555;
|
|
49
|
+
margin-bottom: 8px;
|
|
50
|
+
text-transform: uppercase;
|
|
51
|
+
letter-spacing: 0.5px;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
.
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
.button-group {
|
|
55
|
+
display: flex;
|
|
56
|
+
flex-wrap: wrap;
|
|
57
|
+
gap: 8px;
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
.button {
|
|
60
|
-
padding:
|
|
61
|
+
padding: 8px 16px;
|
|
61
62
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
62
63
|
color: white;
|
|
63
64
|
border: none;
|
|
64
|
-
border-radius:
|
|
65
|
-
font-size:
|
|
66
|
-
font-weight:
|
|
65
|
+
border-radius: 6px;
|
|
66
|
+
font-size: 13px;
|
|
67
|
+
font-weight: 500;
|
|
67
68
|
cursor: pointer;
|
|
68
|
-
transition: transform 0.
|
|
69
|
+
transition: transform 0.15s, box-shadow 0.15s, opacity 0.15s;
|
|
69
70
|
}
|
|
70
71
|
|
|
71
|
-
.button:hover {
|
|
72
|
-
transform: translateY(-
|
|
72
|
+
.button:hover:not(:disabled) {
|
|
73
|
+
transform: translateY(-1px);
|
|
73
74
|
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
|
74
75
|
}
|
|
75
76
|
|
|
76
|
-
.button:active {
|
|
77
|
+
.button:active:not(:disabled) {
|
|
77
78
|
transform: translateY(0);
|
|
78
79
|
}
|
|
79
80
|
|
|
81
|
+
.button:disabled {
|
|
82
|
+
opacity: 0.6;
|
|
83
|
+
cursor: not-allowed;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.loading {
|
|
87
|
+
color: #667eea;
|
|
88
|
+
font-weight: 500;
|
|
89
|
+
padding: 12px;
|
|
90
|
+
margin-top: 16px;
|
|
91
|
+
}
|
|
92
|
+
|
|
80
93
|
.error {
|
|
81
94
|
color: #e53e3e;
|
|
82
95
|
background: #fed7d7;
|
|
83
96
|
padding: 12px;
|
|
84
97
|
border-radius: 8px;
|
|
85
|
-
margin-
|
|
98
|
+
margin-top: 16px;
|
|
99
|
+
font-size: 14px;
|
|
86
100
|
}
|
|
87
101
|
|
|
88
102
|
.result {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
padding: 12px;
|
|
103
|
+
background: #f0fdf4;
|
|
104
|
+
border: 1px solid #bbf7d0;
|
|
92
105
|
border-radius: 8px;
|
|
93
|
-
margin-
|
|
106
|
+
margin-top: 16px;
|
|
107
|
+
padding: 12px;
|
|
108
|
+
overflow-x: auto;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.result pre {
|
|
112
|
+
font-size: 12px;
|
|
113
|
+
color: #166534;
|
|
114
|
+
white-space: pre-wrap;
|
|
115
|
+
word-break: break-word;
|
|
116
|
+
margin: 0;
|
|
94
117
|
}
|
|
95
118
|
|
|
96
119
|
.info {
|
|
97
|
-
margin-top:
|
|
98
|
-
padding-top:
|
|
120
|
+
margin-top: 24px;
|
|
121
|
+
padding-top: 20px;
|
|
99
122
|
border-top: 1px solid #e0e0e0;
|
|
100
123
|
}
|
|
101
124
|
|
|
102
125
|
.info h2 {
|
|
103
|
-
font-size:
|
|
126
|
+
font-size: 16px;
|
|
104
127
|
color: #333;
|
|
105
|
-
margin-bottom:
|
|
128
|
+
margin-bottom: 8px;
|
|
106
129
|
}
|
|
107
130
|
|
|
108
131
|
.info p {
|
|
109
132
|
color: #666;
|
|
110
|
-
margin-bottom:
|
|
133
|
+
margin-bottom: 8px;
|
|
111
134
|
line-height: 1.5;
|
|
135
|
+
font-size: 14px;
|
|
112
136
|
}
|
|
113
137
|
|
|
114
138
|
.info ul {
|
|
115
139
|
color: #666;
|
|
116
140
|
padding-left: 20px;
|
|
141
|
+
font-size: 13px;
|
|
117
142
|
}
|
|
118
143
|
|
|
119
144
|
.info li {
|
|
120
|
-
margin-bottom:
|
|
145
|
+
margin-bottom: 4px;
|
|
121
146
|
}
|