@uipath/data-fabric-tool 0.9.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/dist/index.js +57923 -0
- package/dist/tool.js +57914 -0
- package/package.json +34 -0
- package/src/commands/entities.spec.ts +970 -0
- package/src/commands/entities.ts +564 -0
- package/src/commands/files.spec.ts +344 -0
- package/src/commands/files.ts +339 -0
- package/src/commands/records.spec.ts +2106 -0
- package/src/commands/records.ts +875 -0
- package/src/index.ts +17 -0
- package/src/tool.ts +19 -0
- package/src/utils/input.ts +32 -0
- package/src/utils/sdk-client.ts +23 -0
- package/tsconfig.json +9 -0
- package/vitest.config.ts +1 -0
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import { Command, CommanderError } from "commander";
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
|
|
4
|
+
vi.mock("../utils/sdk-client", () => ({
|
|
5
|
+
createDataFabricClient: vi.fn(),
|
|
6
|
+
}));
|
|
7
|
+
|
|
8
|
+
vi.mock("@uipath/common", async (importOriginal) => {
|
|
9
|
+
const actual = await importOriginal<typeof import("@uipath/common")>();
|
|
10
|
+
return {
|
|
11
|
+
...actual,
|
|
12
|
+
OutputFormatter: {
|
|
13
|
+
success: vi.fn(),
|
|
14
|
+
error: vi.fn(),
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const mockFs = {
|
|
20
|
+
readFile: vi.fn(),
|
|
21
|
+
writeFile: vi.fn(),
|
|
22
|
+
path: { basename: vi.fn((p: string) => p.split("/").pop() ?? "") },
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
vi.mock("@uipath/filesystem", () => ({
|
|
26
|
+
getFileSystem: vi.fn(() => mockFs),
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
import { OutputFormatter } from "@uipath/common";
|
|
30
|
+
import { createDataFabricClient } from "../utils/sdk-client";
|
|
31
|
+
import { registerFilesCommand } from "./files";
|
|
32
|
+
|
|
33
|
+
function mockSdk(overrides: Record<string, unknown> = {}) {
|
|
34
|
+
const sdk = {
|
|
35
|
+
entities: {
|
|
36
|
+
uploadAttachment: vi.fn().mockResolvedValue({}),
|
|
37
|
+
downloadAttachment: vi.fn().mockResolvedValue(new Blob(["data"])),
|
|
38
|
+
deleteAttachment: vi.fn().mockResolvedValue({}),
|
|
39
|
+
...overrides,
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
vi.mocked(createDataFabricClient).mockResolvedValue(sdk as never);
|
|
43
|
+
return sdk;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function buildProgram(): Command {
|
|
47
|
+
const program = new Command();
|
|
48
|
+
program.name("test").exitOverride();
|
|
49
|
+
registerFilesCommand(program);
|
|
50
|
+
return program;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function runCommand(program: Command, args: string): Promise<void> {
|
|
54
|
+
try {
|
|
55
|
+
await program.parseAsync(args.split(" "), { from: "user" });
|
|
56
|
+
} catch (err) {
|
|
57
|
+
if (!(err instanceof CommanderError)) {
|
|
58
|
+
throw err;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
describe("files upload", () => {
|
|
64
|
+
beforeEach(() => {
|
|
65
|
+
vi.clearAllMocks();
|
|
66
|
+
vi.mocked(OutputFormatter.success).mockReset();
|
|
67
|
+
vi.mocked(OutputFormatter.error).mockReset();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("should error when --file is not provided", async () => {
|
|
71
|
+
mockSdk();
|
|
72
|
+
const program = buildProgram();
|
|
73
|
+
await runCommand(program, "files upload entity-1 record-1 attachment");
|
|
74
|
+
expect(OutputFormatter.error).toHaveBeenCalledWith(
|
|
75
|
+
expect.objectContaining({
|
|
76
|
+
Result: "Failure",
|
|
77
|
+
Message: "A file path is required",
|
|
78
|
+
}),
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("should error when file cannot be read", async () => {
|
|
83
|
+
mockSdk();
|
|
84
|
+
mockFs.readFile.mockResolvedValue(null);
|
|
85
|
+
const program = buildProgram();
|
|
86
|
+
await runCommand(
|
|
87
|
+
program,
|
|
88
|
+
"files upload entity-1 record-1 attachment --file /tmp/missing.pdf",
|
|
89
|
+
);
|
|
90
|
+
expect(OutputFormatter.error).toHaveBeenCalledWith(
|
|
91
|
+
expect.objectContaining({
|
|
92
|
+
Result: "Failure",
|
|
93
|
+
Message: "Error reading file",
|
|
94
|
+
}),
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("should upload and return FileUploaded on success", async () => {
|
|
99
|
+
const sdk = mockSdk();
|
|
100
|
+
mockFs.readFile.mockResolvedValue(new Uint8Array([1, 2, 3]));
|
|
101
|
+
const program = buildProgram();
|
|
102
|
+
await runCommand(
|
|
103
|
+
program,
|
|
104
|
+
"files upload entity-1 record-1 attachment --file /tmp/report.pdf",
|
|
105
|
+
);
|
|
106
|
+
expect(sdk.entities.uploadAttachment).toHaveBeenCalledWith(
|
|
107
|
+
"entity-1",
|
|
108
|
+
"record-1",
|
|
109
|
+
"attachment",
|
|
110
|
+
expect.any(File),
|
|
111
|
+
);
|
|
112
|
+
expect(OutputFormatter.success).toHaveBeenCalledWith(
|
|
113
|
+
expect.objectContaining({
|
|
114
|
+
Result: "Success",
|
|
115
|
+
Code: "FileUploaded",
|
|
116
|
+
}),
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("should use 'upload' as filename when basename returns empty string", async () => {
|
|
121
|
+
const sdk = mockSdk();
|
|
122
|
+
mockFs.readFile.mockResolvedValue(new Uint8Array([1, 2, 3]));
|
|
123
|
+
mockFs.path.basename.mockReturnValue("");
|
|
124
|
+
const program = buildProgram();
|
|
125
|
+
await runCommand(
|
|
126
|
+
program,
|
|
127
|
+
"files upload entity-1 record-1 attachment --file /tmp/report.pdf",
|
|
128
|
+
);
|
|
129
|
+
expect(sdk.entities.uploadAttachment).toHaveBeenCalledWith(
|
|
130
|
+
"entity-1",
|
|
131
|
+
"record-1",
|
|
132
|
+
"attachment",
|
|
133
|
+
expect.objectContaining({ name: "upload" }),
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("should error when SDK connection fails on upload", async () => {
|
|
138
|
+
vi.mocked(createDataFabricClient).mockRejectedValue(
|
|
139
|
+
new Error("Not logged in"),
|
|
140
|
+
);
|
|
141
|
+
mockFs.readFile.mockResolvedValue(new Uint8Array([1, 2, 3]));
|
|
142
|
+
const program = buildProgram();
|
|
143
|
+
await runCommand(
|
|
144
|
+
program,
|
|
145
|
+
"files upload entity-1 record-1 attachment --file /tmp/report.pdf",
|
|
146
|
+
);
|
|
147
|
+
expect(OutputFormatter.error).toHaveBeenCalledWith(
|
|
148
|
+
expect.objectContaining({
|
|
149
|
+
Result: "Failure",
|
|
150
|
+
Message: "Error connecting to Data Fabric",
|
|
151
|
+
}),
|
|
152
|
+
);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("should error when upload API fails", async () => {
|
|
156
|
+
const sdk = mockSdk();
|
|
157
|
+
mockFs.readFile.mockResolvedValue(new Uint8Array([1, 2, 3]));
|
|
158
|
+
sdk.entities.uploadAttachment.mockRejectedValue(
|
|
159
|
+
new Error("Upload failed"),
|
|
160
|
+
);
|
|
161
|
+
const program = buildProgram();
|
|
162
|
+
await runCommand(
|
|
163
|
+
program,
|
|
164
|
+
"files upload entity-1 record-1 attachment --file /tmp/report.pdf",
|
|
165
|
+
);
|
|
166
|
+
expect(OutputFormatter.error).toHaveBeenCalledWith(
|
|
167
|
+
expect.objectContaining({
|
|
168
|
+
Result: "Failure",
|
|
169
|
+
Message: "Error uploading file to field 'attachment'",
|
|
170
|
+
}),
|
|
171
|
+
);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe("files download", () => {
|
|
176
|
+
beforeEach(() => {
|
|
177
|
+
vi.clearAllMocks();
|
|
178
|
+
vi.mocked(OutputFormatter.success).mockReset();
|
|
179
|
+
vi.mocked(OutputFormatter.error).mockReset();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("should download and write file to --destination path", async () => {
|
|
183
|
+
const blob = new Blob(["pdfdata"]);
|
|
184
|
+
mockSdk({ downloadAttachment: vi.fn().mockResolvedValue(blob) });
|
|
185
|
+
mockFs.writeFile.mockResolvedValue(undefined);
|
|
186
|
+
const program = buildProgram();
|
|
187
|
+
await runCommand(
|
|
188
|
+
program,
|
|
189
|
+
"files download entity-1 record-1 attachment --destination /tmp/out.pdf",
|
|
190
|
+
);
|
|
191
|
+
expect(mockFs.writeFile).toHaveBeenCalledWith(
|
|
192
|
+
"/tmp/out.pdf",
|
|
193
|
+
expect.any(Uint8Array),
|
|
194
|
+
);
|
|
195
|
+
expect(OutputFormatter.success).toHaveBeenCalledWith(
|
|
196
|
+
expect.objectContaining({
|
|
197
|
+
Result: "Success",
|
|
198
|
+
Code: "FileDownloaded",
|
|
199
|
+
}),
|
|
200
|
+
);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("should error when download API fails", async () => {
|
|
204
|
+
const sdk = mockSdk();
|
|
205
|
+
sdk.entities.downloadAttachment.mockRejectedValue(
|
|
206
|
+
new Error("Server error"),
|
|
207
|
+
);
|
|
208
|
+
const program = buildProgram();
|
|
209
|
+
await runCommand(
|
|
210
|
+
program,
|
|
211
|
+
"files download entity-1 record-1 attachment",
|
|
212
|
+
);
|
|
213
|
+
expect(OutputFormatter.error).toHaveBeenCalledWith(
|
|
214
|
+
expect.objectContaining({
|
|
215
|
+
Result: "Failure",
|
|
216
|
+
Message: "Error downloading file from field 'attachment'",
|
|
217
|
+
}),
|
|
218
|
+
);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("should error when SDK connection fails", async () => {
|
|
222
|
+
vi.mocked(createDataFabricClient).mockRejectedValue(
|
|
223
|
+
new Error("Not logged in"),
|
|
224
|
+
);
|
|
225
|
+
const program = buildProgram();
|
|
226
|
+
await runCommand(
|
|
227
|
+
program,
|
|
228
|
+
"files download entity-1 record-1 attachment",
|
|
229
|
+
);
|
|
230
|
+
expect(OutputFormatter.error).toHaveBeenCalledWith(
|
|
231
|
+
expect.objectContaining({
|
|
232
|
+
Result: "Failure",
|
|
233
|
+
Message: "Error connecting to Data Fabric",
|
|
234
|
+
}),
|
|
235
|
+
);
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
describe("files delete", () => {
|
|
240
|
+
beforeEach(() => {
|
|
241
|
+
vi.clearAllMocks();
|
|
242
|
+
vi.mocked(OutputFormatter.success).mockReset();
|
|
243
|
+
vi.mocked(OutputFormatter.error).mockReset();
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("should delete and return FileDeleted on success", async () => {
|
|
247
|
+
const sdk = mockSdk();
|
|
248
|
+
const program = buildProgram();
|
|
249
|
+
await runCommand(program, "files delete entity-1 record-1 attachment");
|
|
250
|
+
expect(sdk.entities.deleteAttachment).toHaveBeenCalledWith(
|
|
251
|
+
"entity-1",
|
|
252
|
+
"record-1",
|
|
253
|
+
"attachment",
|
|
254
|
+
);
|
|
255
|
+
expect(OutputFormatter.success).toHaveBeenCalledWith(
|
|
256
|
+
expect.objectContaining({ Result: "Success", Code: "FileDeleted" }),
|
|
257
|
+
);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it("should error when delete API fails", async () => {
|
|
261
|
+
const sdk = mockSdk();
|
|
262
|
+
sdk.entities.deleteAttachment.mockRejectedValue(new Error("Forbidden"));
|
|
263
|
+
const program = buildProgram();
|
|
264
|
+
await runCommand(program, "files delete entity-1 record-1 attachment");
|
|
265
|
+
expect(OutputFormatter.error).toHaveBeenCalledWith(
|
|
266
|
+
expect.objectContaining({
|
|
267
|
+
Result: "Failure",
|
|
268
|
+
Message: "Error deleting file from field 'attachment'",
|
|
269
|
+
}),
|
|
270
|
+
);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it("should error when SDK connection fails on delete", async () => {
|
|
274
|
+
vi.mocked(createDataFabricClient).mockRejectedValue(
|
|
275
|
+
new Error("Not logged in"),
|
|
276
|
+
);
|
|
277
|
+
const program = buildProgram();
|
|
278
|
+
await runCommand(program, "files delete entity-1 record-1 attachment");
|
|
279
|
+
expect(OutputFormatter.error).toHaveBeenCalledWith(
|
|
280
|
+
expect.objectContaining({
|
|
281
|
+
Result: "Failure",
|
|
282
|
+
Message: "Error connecting to Data Fabric",
|
|
283
|
+
}),
|
|
284
|
+
);
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
describe("files download edge cases", () => {
|
|
289
|
+
beforeEach(() => {
|
|
290
|
+
vi.clearAllMocks();
|
|
291
|
+
vi.mocked(OutputFormatter.success).mockReset();
|
|
292
|
+
vi.mocked(OutputFormatter.error).mockReset();
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it("should error when writing downloaded file fails", async () => {
|
|
296
|
+
const blob = new Blob(["data"]);
|
|
297
|
+
mockSdk({ downloadAttachment: vi.fn().mockResolvedValue(blob) });
|
|
298
|
+
mockFs.writeFile.mockRejectedValue(new Error("Disk full"));
|
|
299
|
+
const program = buildProgram();
|
|
300
|
+
await runCommand(
|
|
301
|
+
program,
|
|
302
|
+
"files download entity-1 record-1 attachment --destination /tmp/out.bin",
|
|
303
|
+
);
|
|
304
|
+
expect(OutputFormatter.error).toHaveBeenCalledWith(
|
|
305
|
+
expect.objectContaining({
|
|
306
|
+
Result: "Failure",
|
|
307
|
+
Message: "Error writing downloaded file",
|
|
308
|
+
}),
|
|
309
|
+
);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it("should error when arrayBuffer() fails on blob", async () => {
|
|
313
|
+
const badBlob = {
|
|
314
|
+
arrayBuffer: () => Promise.reject(new Error("Stream error")),
|
|
315
|
+
};
|
|
316
|
+
mockSdk({ downloadAttachment: vi.fn().mockResolvedValue(badBlob) });
|
|
317
|
+
const program = buildProgram();
|
|
318
|
+
await runCommand(
|
|
319
|
+
program,
|
|
320
|
+
"files download entity-1 record-1 attachment --destination /tmp/out.bin",
|
|
321
|
+
);
|
|
322
|
+
expect(OutputFormatter.error).toHaveBeenCalledWith(
|
|
323
|
+
expect.objectContaining({
|
|
324
|
+
Result: "Failure",
|
|
325
|
+
Message: "Error reading downloaded file content",
|
|
326
|
+
}),
|
|
327
|
+
);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it("should use recordId_fieldName.bin as default destination", async () => {
|
|
331
|
+
const blob = new Blob(["data"]);
|
|
332
|
+
mockSdk({ downloadAttachment: vi.fn().mockResolvedValue(blob) });
|
|
333
|
+
mockFs.writeFile.mockResolvedValue(undefined);
|
|
334
|
+
const program = buildProgram();
|
|
335
|
+
await runCommand(
|
|
336
|
+
program,
|
|
337
|
+
"files download entity-1 record-1 attachment",
|
|
338
|
+
);
|
|
339
|
+
expect(mockFs.writeFile).toHaveBeenCalledWith(
|
|
340
|
+
"record-1_attachment.bin",
|
|
341
|
+
expect.any(Uint8Array),
|
|
342
|
+
);
|
|
343
|
+
});
|
|
344
|
+
});
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type CommandExample,
|
|
3
|
+
catchError,
|
|
4
|
+
extractErrorMessage,
|
|
5
|
+
OutputFormatter,
|
|
6
|
+
processContext,
|
|
7
|
+
RESULTS,
|
|
8
|
+
} from "@uipath/common";
|
|
9
|
+
import { getFileSystem } from "@uipath/filesystem";
|
|
10
|
+
import type { Command } from "commander";
|
|
11
|
+
import { readFileBinary } from "../utils/input";
|
|
12
|
+
import { createDataFabricClient } from "../utils/sdk-client";
|
|
13
|
+
|
|
14
|
+
interface UploadOptions {
|
|
15
|
+
tenant?: string;
|
|
16
|
+
file?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface DownloadOptions {
|
|
20
|
+
tenant?: string;
|
|
21
|
+
destination?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface DeleteOptions {
|
|
25
|
+
tenant?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const FILES_UPLOAD_EXAMPLES: CommandExample[] = [
|
|
29
|
+
{
|
|
30
|
+
Description: "Upload a file attachment to a record field",
|
|
31
|
+
Command:
|
|
32
|
+
"uip df files upload a1b2c3d4-0000-0000-0000-000000000001 b2c3d4e5-0000-0000-0000-000000000001 invoice --file ./invoice.pdf",
|
|
33
|
+
Output: {
|
|
34
|
+
Code: "FileUploaded",
|
|
35
|
+
Data: {
|
|
36
|
+
EntityId: "a1b2c3d4-0000-0000-0000-000000000001",
|
|
37
|
+
RecordId: "b2c3d4e5-0000-0000-0000-000000000001",
|
|
38
|
+
FieldName: "invoice",
|
|
39
|
+
FileName: "invoice.pdf",
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
const FILES_DOWNLOAD_EXAMPLES: CommandExample[] = [
|
|
46
|
+
{
|
|
47
|
+
Description: "Download a file attachment from a record field",
|
|
48
|
+
Command:
|
|
49
|
+
"uip df files download a1b2c3d4-0000-0000-0000-000000000001 b2c3d4e5-0000-0000-0000-000000000001 invoice --destination ./invoice.pdf",
|
|
50
|
+
Output: {
|
|
51
|
+
Code: "FileDownloaded",
|
|
52
|
+
Data: {
|
|
53
|
+
EntityId: "a1b2c3d4-0000-0000-0000-000000000001",
|
|
54
|
+
RecordId: "b2c3d4e5-0000-0000-0000-000000000001",
|
|
55
|
+
FieldName: "invoice",
|
|
56
|
+
OutputPath: "./invoice.pdf",
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
const FILES_DELETE_EXAMPLES: CommandExample[] = [
|
|
63
|
+
{
|
|
64
|
+
Description: "Delete a file attachment from a record field",
|
|
65
|
+
Command:
|
|
66
|
+
"uip df files delete a1b2c3d4-0000-0000-0000-000000000001 b2c3d4e5-0000-0000-0000-000000000001 invoice",
|
|
67
|
+
Output: {
|
|
68
|
+
Code: "FileDeleted",
|
|
69
|
+
Data: {
|
|
70
|
+
EntityId: "a1b2c3d4-0000-0000-0000-000000000001",
|
|
71
|
+
RecordId: "b2c3d4e5-0000-0000-0000-000000000001",
|
|
72
|
+
FieldName: "invoice",
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
export const registerFilesCommand = (program: Command) => {
|
|
79
|
+
const files = program
|
|
80
|
+
.command("files")
|
|
81
|
+
.description("Manage file attachments on Data Fabric entity records");
|
|
82
|
+
|
|
83
|
+
files
|
|
84
|
+
.command("upload")
|
|
85
|
+
.description("Upload a file to a field on an entity record")
|
|
86
|
+
.argument("<entity-id>", "Entity ID")
|
|
87
|
+
.argument("<record-id>", "Record ID")
|
|
88
|
+
.argument("<field-name>", "Name of the file field (case-sensitive)")
|
|
89
|
+
.option("-t, --tenant <tenant-name>", "Tenant name")
|
|
90
|
+
.option("-f, --file <path>", "Path to the file to upload")
|
|
91
|
+
.examples(FILES_UPLOAD_EXAMPLES)
|
|
92
|
+
.trackedAction(
|
|
93
|
+
processContext,
|
|
94
|
+
async (
|
|
95
|
+
entityId: string,
|
|
96
|
+
recordId: string,
|
|
97
|
+
fieldName: string,
|
|
98
|
+
options: UploadOptions,
|
|
99
|
+
) => {
|
|
100
|
+
if (!options.file) {
|
|
101
|
+
OutputFormatter.error({
|
|
102
|
+
Result: RESULTS.Failure,
|
|
103
|
+
Message: "A file path is required",
|
|
104
|
+
Instructions: "Provide a file path via --file.",
|
|
105
|
+
});
|
|
106
|
+
processContext.exit(1);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const [clientError, sdk] = await catchError(
|
|
111
|
+
createDataFabricClient(options.tenant),
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
if (clientError) {
|
|
115
|
+
OutputFormatter.error({
|
|
116
|
+
Result: RESULTS.Failure,
|
|
117
|
+
Message: "Error connecting to Data Fabric",
|
|
118
|
+
Instructions: await extractErrorMessage(clientError),
|
|
119
|
+
});
|
|
120
|
+
processContext.exit(1);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const [readError, fileContent] = await catchError(
|
|
125
|
+
readFileBinary(options.file),
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
if (readError) {
|
|
129
|
+
OutputFormatter.error({
|
|
130
|
+
Result: RESULTS.Failure,
|
|
131
|
+
Message: "Error reading file",
|
|
132
|
+
Instructions: readError.message,
|
|
133
|
+
});
|
|
134
|
+
processContext.exit(1);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const fsInst = getFileSystem();
|
|
139
|
+
const fileName = fsInst.path.basename(options.file) || "upload";
|
|
140
|
+
|
|
141
|
+
const [uploadError] = await catchError(
|
|
142
|
+
sdk.entities.uploadAttachment(
|
|
143
|
+
entityId,
|
|
144
|
+
recordId,
|
|
145
|
+
fieldName,
|
|
146
|
+
new File(
|
|
147
|
+
[fileContent as Uint8Array<ArrayBuffer>],
|
|
148
|
+
fileName,
|
|
149
|
+
),
|
|
150
|
+
),
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
if (uploadError) {
|
|
154
|
+
OutputFormatter.error({
|
|
155
|
+
Result: RESULTS.Failure,
|
|
156
|
+
Message: `Error uploading file to field '${fieldName}'`,
|
|
157
|
+
Instructions: await extractErrorMessage(uploadError),
|
|
158
|
+
});
|
|
159
|
+
processContext.exit(1);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
OutputFormatter.success({
|
|
164
|
+
Result: RESULTS.Success,
|
|
165
|
+
Code: "FileUploaded",
|
|
166
|
+
Data: {
|
|
167
|
+
EntityId: entityId,
|
|
168
|
+
RecordId: recordId,
|
|
169
|
+
FieldName: fieldName,
|
|
170
|
+
FileName: fileName,
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
},
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
files
|
|
177
|
+
.command("download")
|
|
178
|
+
.description("Download a file from a field on an entity record")
|
|
179
|
+
.argument("<entity-id>", "Entity ID")
|
|
180
|
+
.argument("<record-id>", "Record ID")
|
|
181
|
+
.argument("<field-name>", "Name of the file field (case-sensitive)")
|
|
182
|
+
.option("-t, --tenant <tenant-name>", "Tenant name")
|
|
183
|
+
.option(
|
|
184
|
+
"--destination <path>",
|
|
185
|
+
"Output file path (defaults to <record-id>_<field-name>.bin)",
|
|
186
|
+
)
|
|
187
|
+
.examples(FILES_DOWNLOAD_EXAMPLES)
|
|
188
|
+
.trackedAction(
|
|
189
|
+
processContext,
|
|
190
|
+
async (
|
|
191
|
+
entityId: string,
|
|
192
|
+
recordId: string,
|
|
193
|
+
fieldName: string,
|
|
194
|
+
options: DownloadOptions,
|
|
195
|
+
) => {
|
|
196
|
+
const [clientError, sdk] = await catchError(
|
|
197
|
+
createDataFabricClient(options.tenant),
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
if (clientError) {
|
|
201
|
+
OutputFormatter.error({
|
|
202
|
+
Result: RESULTS.Failure,
|
|
203
|
+
Message: "Error connecting to Data Fabric",
|
|
204
|
+
Instructions: await extractErrorMessage(clientError),
|
|
205
|
+
});
|
|
206
|
+
processContext.exit(1);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const [downloadError, blob] = await catchError(
|
|
211
|
+
sdk.entities.downloadAttachment(
|
|
212
|
+
entityId,
|
|
213
|
+
recordId,
|
|
214
|
+
fieldName,
|
|
215
|
+
),
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
if (downloadError) {
|
|
219
|
+
OutputFormatter.error({
|
|
220
|
+
Result: RESULTS.Failure,
|
|
221
|
+
Message: `Error downloading file from field '${fieldName}'`,
|
|
222
|
+
Instructions: await extractErrorMessage(downloadError),
|
|
223
|
+
});
|
|
224
|
+
processContext.exit(1);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const outputPath =
|
|
229
|
+
options.destination ?? `${recordId}_${fieldName}.bin`;
|
|
230
|
+
|
|
231
|
+
const [bufferError, arrayBuffer] = await catchError(
|
|
232
|
+
(blob as Blob).arrayBuffer(),
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
if (bufferError) {
|
|
236
|
+
OutputFormatter.error({
|
|
237
|
+
Result: RESULTS.Failure,
|
|
238
|
+
Message: "Error reading downloaded file content",
|
|
239
|
+
Instructions: bufferError.message,
|
|
240
|
+
});
|
|
241
|
+
processContext.exit(1);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const [writeError] = await catchError(
|
|
246
|
+
writeFileBinary(outputPath, new Uint8Array(arrayBuffer)),
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
if (writeError) {
|
|
250
|
+
OutputFormatter.error({
|
|
251
|
+
Result: RESULTS.Failure,
|
|
252
|
+
Message: "Error writing downloaded file",
|
|
253
|
+
Instructions: writeError.message,
|
|
254
|
+
});
|
|
255
|
+
processContext.exit(1);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
OutputFormatter.success({
|
|
260
|
+
Result: RESULTS.Success,
|
|
261
|
+
Code: "FileDownloaded",
|
|
262
|
+
Data: {
|
|
263
|
+
EntityId: entityId,
|
|
264
|
+
RecordId: recordId,
|
|
265
|
+
FieldName: fieldName,
|
|
266
|
+
OutputPath: outputPath,
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
},
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
files
|
|
273
|
+
.command("delete")
|
|
274
|
+
.description("Delete a file from a field on an entity record")
|
|
275
|
+
.argument("<entity-id>", "Entity ID")
|
|
276
|
+
.argument("<record-id>", "Record ID")
|
|
277
|
+
.argument("<field-name>", "Name of the file field (case-sensitive)")
|
|
278
|
+
.option("-t, --tenant <tenant-name>", "Tenant name")
|
|
279
|
+
.examples(FILES_DELETE_EXAMPLES)
|
|
280
|
+
.trackedAction(
|
|
281
|
+
processContext,
|
|
282
|
+
async (
|
|
283
|
+
entityId: string,
|
|
284
|
+
recordId: string,
|
|
285
|
+
fieldName: string,
|
|
286
|
+
options: DeleteOptions,
|
|
287
|
+
) => {
|
|
288
|
+
const [clientError, sdk] = await catchError(
|
|
289
|
+
createDataFabricClient(options.tenant),
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
if (clientError) {
|
|
293
|
+
OutputFormatter.error({
|
|
294
|
+
Result: RESULTS.Failure,
|
|
295
|
+
Message: "Error connecting to Data Fabric",
|
|
296
|
+
Instructions: await extractErrorMessage(clientError),
|
|
297
|
+
});
|
|
298
|
+
processContext.exit(1);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const [deleteError] = await catchError(
|
|
303
|
+
sdk.entities.deleteAttachment(
|
|
304
|
+
entityId,
|
|
305
|
+
recordId,
|
|
306
|
+
fieldName,
|
|
307
|
+
),
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
if (deleteError) {
|
|
311
|
+
OutputFormatter.error({
|
|
312
|
+
Result: RESULTS.Failure,
|
|
313
|
+
Message: `Error deleting file from field '${fieldName}'`,
|
|
314
|
+
Instructions: await extractErrorMessage(deleteError),
|
|
315
|
+
});
|
|
316
|
+
processContext.exit(1);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
OutputFormatter.success({
|
|
321
|
+
Result: RESULTS.Success,
|
|
322
|
+
Code: "FileDeleted",
|
|
323
|
+
Data: {
|
|
324
|
+
EntityId: entityId,
|
|
325
|
+
RecordId: recordId,
|
|
326
|
+
FieldName: fieldName,
|
|
327
|
+
},
|
|
328
|
+
});
|
|
329
|
+
},
|
|
330
|
+
);
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
async function writeFileBinary(
|
|
334
|
+
filePath: string,
|
|
335
|
+
data: Uint8Array,
|
|
336
|
+
): Promise<void> {
|
|
337
|
+
const fs = getFileSystem();
|
|
338
|
+
await fs.writeFile(filePath, data);
|
|
339
|
+
}
|