Skip to content

Commit eb81eb3

Browse files
committed
Update CLI to support file uploads (when creating a new document)
1 parent 31cf16d commit eb81eb3

File tree

6 files changed

+201
-44
lines changed

6 files changed

+201
-44
lines changed

README.md

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -197,11 +197,13 @@ funcli documents get abc123 --format json
197197
funcli documents create <space-npi> [file]
198198
```
199199

200-
Creates a new document from a markdown file or stdin. Supports frontmatter for metadata.
200+
Creates a new document from a markdown file, Word/OpenOffice file, or stdin. Supports frontmatter for metadata.
201201

202202
**Arguments:**
203203
- `<space-npi>` - Space NPI where the document will be created
204-
- `[file]` - Path to markdown file (optional, reads from stdin if omitted)
204+
- `[file]` - Path to file (optional, reads markdown from stdin if omitted)
205+
- Markdown files: `.md`
206+
- Word/OpenOffice files: `.docx`, `.doc`, `.odt`, `.rtf`, `.txt`
205207

206208
**Options:**
207209
- `-p, --parent <npi>` - Parent document NPI (for nested documents)
@@ -234,16 +236,25 @@ parentNpi: abc123
234236
**Examples:**
235237

236238
```bash
237-
# Create from file (title from frontmatter or filename)
239+
# Create from markdown file (title from frontmatter or filename)
238240
funcli documents create z2zK66AaEF my-document.md
239241

242+
# Create from Word document
243+
funcli documents create z2zK66AaEF report.docx
244+
245+
# Create from OpenOffice document
246+
funcli documents create z2zK66AaEF document.odt
247+
240248
# Create from file with custom title
241249
funcli documents create z2zK66AaEF my-document.md --title "Custom Title"
242250

243251
# Create nested document (under parent)
244252
funcli documents create z2zK66AaEF child.md --parent abc123
245253

246-
# Create from stdin
254+
# Create nested Word document
255+
funcli documents create z2zK66AaEF section.docx --parent abc123 --title "Section 1"
256+
257+
# Create from stdin (markdown only)
247258
echo "# My Document\n\nContent here" | funcli documents create z2zK66AaEF
248259

249260
# Create from stdin with title
@@ -428,6 +439,12 @@ echo "# My Notes\n\nSome content" | funcli documents create z2zK66AaEF --title "
428439
# Import an existing markdown file
429440
funcli documents create z2zK66AaEF README.md
430441

442+
# Import a Word document
443+
funcli documents create z2zK66AaEF report.docx
444+
445+
# Import an OpenOffice document
446+
funcli documents create z2zK66AaEF document.odt --title "Important Document"
447+
431448
# Update an existing document
432449
funcli documents update abc123 updated-content.md
433450

@@ -439,10 +456,15 @@ funcli documents create z2zK66AaEF parent.md
439456
# Note the NPI of the created document, then:
440457
funcli documents create z2zK66AaEF child.md --parent <parent-npi>
441458

442-
# Batch import multiple documents
459+
# Batch import markdown documents
443460
for file in docs/*.md; do
444461
funcli documents create z2zK66AaEF "$file"
445462
done
463+
464+
# Batch import Word documents
465+
for file in reports/*.docx; do
466+
funcli documents create z2zK66AaEF "$file"
467+
done
446468
```
447469

448470
### Search for a space and get its documents

package-lock.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"chalk": "^4.1.2",
3939
"commander": "^12.1.0",
4040
"dotenv": "^16.4.7",
41+
"form-data": "^4.0.5",
4142
"gray-matter": "^4.0.3"
4243
},
4344
"devDependencies": {

src/cli.js

Lines changed: 57 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -139,44 +139,72 @@ documentsCommand
139139

140140
documentsCommand
141141
.command("create <space-npi>")
142-
.description("Create a new document from markdown file or stdin")
143-
.argument("[file]", "Markdown file (omit to read from stdin)")
142+
.description("Create a new document from markdown file, Word/OpenOffice file, or stdin")
143+
.argument("[file]", "File to upload (markdown, .docx, .doc, .odt, etc.) or omit to read markdown from stdin")
144144
.option("-p, --parent <npi>", "Parent document NPI (for nested documents)")
145145
.option("-t, --title <title>", "Document title (overrides frontmatter)")
146146
.action(withClient(async (client, spaceNpi, file, options) => {
147-
// Read content from file or stdin
148-
let content;
149-
if (file) {
150-
content = fs.readFileSync(file, "utf8");
147+
// Determine if this is a file upload (Word/OpenOffice) or markdown content
148+
const fileUploadExtensions = [".docx", ".doc", ".odt", ".rtf", ".txt"];
149+
const isFileUpload = file && fileUploadExtensions.some(ext => file.toLowerCase().endsWith(ext));
150+
151+
if (isFileUpload) {
152+
// File upload path - send file directly
153+
if (!fs.existsSync(file)) {
154+
console.error(chalk.red("Error:"), `File not found: ${file}`);
155+
process.exit(1);
156+
}
157+
158+
// Determine title (priority: CLI arg > filename)
159+
let title = options.title;
160+
if (!title) {
161+
title = path.basename(file, path.extname(file));
162+
}
163+
164+
// Create document from file
165+
const document = await client.createDocument(spaceNpi, {
166+
title,
167+
parentDocumentNpi: options.parent,
168+
file
169+
});
170+
171+
console.log(chalk.green("✓") + " Document created successfully from file!");
172+
console.log(chalk.bold(document.title) + chalk.gray(` (${document.npi})`));
151173
} else {
152-
// Read from stdin
153-
content = await readStdin();
154-
}
174+
// Markdown path - parse frontmatter
175+
let content;
176+
if (file) {
177+
content = fs.readFileSync(file, "utf8");
178+
} else {
179+
// Read from stdin
180+
content = await readStdin();
181+
}
155182

156-
// Parse frontmatter
157-
const { data: frontmatter, content: markdown } = matter(content);
183+
// Parse frontmatter
184+
const { data: frontmatter, content: markdown } = matter(content);
158185

159-
// Determine title (priority: CLI arg > frontmatter > filename > "Untitled")
160-
let title = options.title || frontmatter.title;
161-
if (!title && file) {
162-
title = path.basename(file, path.extname(file));
163-
}
164-
if (!title) {
165-
title = "Untitled";
166-
}
186+
// Determine title (priority: CLI arg > frontmatter > filename > "Untitled")
187+
let title = options.title || frontmatter.title;
188+
if (!title && file) {
189+
title = path.basename(file, path.extname(file));
190+
}
191+
if (!title) {
192+
title = "Untitled";
193+
}
167194

168-
// Determine parent (priority: CLI arg > frontmatter)
169-
const parentDocumentNpi = options.parent || frontmatter.parentNpi;
195+
// Determine parent (priority: CLI arg > frontmatter)
196+
const parentDocumentNpi = options.parent || frontmatter.parentNpi;
170197

171-
// Create document
172-
const document = await client.createDocument(spaceNpi, {
173-
title,
174-
markdown,
175-
parentDocumentNpi
176-
});
198+
// Create document from markdown
199+
const document = await client.createDocument(spaceNpi, {
200+
title,
201+
markdown,
202+
parentDocumentNpi
203+
});
177204

178-
console.log(chalk.green("✓") + " Document created successfully!");
179-
console.log(chalk.bold(document.title) + chalk.gray(` (${document.npi})`));
205+
console.log(chalk.green("✓") + " Document created successfully!");
206+
console.log(chalk.bold(document.title) + chalk.gray(` (${document.npi})`));
207+
}
180208
}));
181209

182210
documentsCommand

src/client.js

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import axios from "axios";
2+
import FormData from "form-data";
3+
import fs from "fs";
24

35
export class FundamentoClient {
46
constructor(config) {
@@ -53,17 +55,39 @@ export class FundamentoClient {
5355
return response.data;
5456
}
5557

56-
async createDocument(spaceNpi, { title, markdown, parentDocumentNpi }) {
57-
const response = await this.axios.post("/api/v1/documents", {
58-
document: {
59-
title,
60-
markdown,
61-
parent_document_npi: parentDocumentNpi
58+
async createDocument(spaceNpi, { title, markdown, parentDocumentNpi, file }) {
59+
if (file) {
60+
// Use multipart form data for file uploads
61+
const formData = new FormData();
62+
formData.append("document[file]", fs.createReadStream(file));
63+
if (title) {
64+
formData.append("document[title]", title);
6265
}
63-
}, {
64-
params: { space_npi: spaceNpi }
65-
});
66-
return response.data;
66+
if (parentDocumentNpi) {
67+
formData.append("document[parent_document_npi]", parentDocumentNpi);
68+
}
69+
70+
const response = await this.axios.post("/api/v1/documents", formData, {
71+
params: { space_npi: spaceNpi },
72+
headers: {
73+
...formData.getHeaders(),
74+
"Authorization": `Bearer ${this.config.apiKey}`
75+
}
76+
});
77+
return response.data;
78+
} else {
79+
// Use JSON for markdown content
80+
const response = await this.axios.post("/api/v1/documents", {
81+
document: {
82+
title,
83+
markdown,
84+
parent_document_npi: parentDocumentNpi
85+
}
86+
}, {
87+
params: { space_npi: spaceNpi }
88+
});
89+
return response.data;
90+
}
6791
}
6892

6993
async updateDocument(npi, { markdown }) {

test/client.test.js

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,3 +129,84 @@ test("FundamentoClient updateDocument should format request correctly", () => {
129129
// Restore original patch
130130
client.axios.patch = originalPatch;
131131
});
132+
133+
test("FundamentoClient createDocument with file should use FormData", async () => {
134+
const config = new Config({ apiKey: "test-key" });
135+
const client = new FundamentoClient(config);
136+
137+
// Mock axios post
138+
const originalPost = client.axios.post;
139+
let capturedUrl, capturedData, capturedConfig;
140+
141+
client.axios.post = async (url, data, config) => {
142+
capturedUrl = url;
143+
capturedData = data;
144+
capturedConfig = config;
145+
return { data: { npi: "test123", title: "Test Document" } };
146+
};
147+
148+
// Call createDocument with file
149+
await client.createDocument("space123", {
150+
title: "Test Document",
151+
parentDocumentNpi: "parent123",
152+
file: "../fundamento-cloud/spec/fixtures/files/pandoc/Volume-2-Terms-of-Reference.docx"
153+
});
154+
155+
// Verify the request format
156+
assert.strictEqual(capturedUrl, "/api/v1/documents");
157+
158+
// Verify FormData was used (it should have getHeaders method)
159+
assert.ok(capturedData);
160+
assert.ok(typeof capturedData.getHeaders === "function");
161+
162+
// Verify params and headers
163+
assert.ok(capturedConfig);
164+
assert.deepStrictEqual(capturedConfig.params, { space_npi: "space123" });
165+
assert.ok(capturedConfig.headers);
166+
assert.strictEqual(capturedConfig.headers.Authorization, "Bearer test-key");
167+
168+
// Restore original post
169+
client.axios.post = originalPost;
170+
});
171+
172+
test("FundamentoClient createDocument with markdown should use JSON", async () => {
173+
const config = new Config({ apiKey: "test-key" });
174+
const client = new FundamentoClient(config);
175+
176+
// Mock axios post
177+
const originalPost = client.axios.post;
178+
let capturedUrl, capturedData, capturedConfig;
179+
180+
client.axios.post = async (url, data, config) => {
181+
capturedUrl = url;
182+
capturedData = data;
183+
capturedConfig = config;
184+
return { data: { npi: "test123", title: "Test Document" } };
185+
};
186+
187+
// Call createDocument with markdown
188+
await client.createDocument("space123", {
189+
title: "Test Document",
190+
markdown: "# Hello",
191+
parentDocumentNpi: "parent123"
192+
});
193+
194+
// Verify the request format
195+
assert.strictEqual(capturedUrl, "/api/v1/documents");
196+
197+
// Verify JSON was used (not FormData)
198+
assert.deepStrictEqual(capturedData, {
199+
document: {
200+
title: "Test Document",
201+
markdown: "# Hello",
202+
parent_document_npi: "parent123"
203+
}
204+
});
205+
206+
// Verify params
207+
assert.ok(capturedConfig);
208+
assert.deepStrictEqual(capturedConfig.params, { space_npi: "space123" });
209+
210+
// Restore original post
211+
client.axios.post = originalPost;
212+
});

0 commit comments

Comments
 (0)