Skip to content

Commit 8a8dd8b

Browse files
Merge branch 'PipedreamHQ:master' into feature/query-logs-action
2 parents 0ca86c5 + 21dddb3 commit 8a8dd8b

File tree

11 files changed

+659
-40
lines changed

11 files changed

+659
-40
lines changed

docs-v2/pages/components/contributing/guidelines.mdx

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,8 @@ scoped components are easier for users to understand and use.
116116
Registry [components](/components/contributing/api/#component-structure) require a unique
117117
`key` and `version`, and a friendly `name` and `description`. Action components
118118
require a `type` field to be set to `action` (sources will require a type to be
119-
set in the future).
119+
set in the future). Action components require the description to include a link to the
120+
relevant documentation in the following format: \[See the documentation\](https://public-api.com)
120121

121122
```javascript
122123
export default {
@@ -160,6 +161,9 @@ to the registry, the version should be `0.0.1`. If the action was at version
160161
`0.1.0` and you've fixed a bug, change it to `0.1.1` when committing your final
161162
code.
162163

164+
If you update a file, you must increment the versions of all components that
165+
import or are affected by the updated file.
166+
163167
### Folder Structure
164168

165169
Registry components are organized by app in the `components` directory of the
@@ -168,26 +172,28 @@ Registry components are organized by app in the `components` directory of the
168172
```text
169173
/components
170174
/[app-name-slug]
171-
/[app-name-slug].app.js
175+
/[app-name-slug].app.mjs
172176
/actions
173177
/[action-name-slug]
174-
/[action-name-slug].js
178+
/[action-name-slug].mjs
175179
/sources
176180
/[source-name-slug]
177-
/[source-name-slug].js
181+
/[source-name-slug].mjs
178182
```
179183

180184
- The name of each app folder corresponds with the name slug for each app
181185
- The app file should be in the root of the app folder (e.g.,
182-
`/components/[app_slug]/[app_slug].app.js`)
186+
`/components/[app_slug]/[app_slug].app.mjs`)
183187
- Components for each app are organized into `/sources` and `/actions`
184188
subfolders
185189
- Each component should be placed in its own subfolder (with the name of the
186190
folder and the name of the `js` file equivalent to the slugified component
187191
name). For example, the path for the "Search Mentions" source for Twitter is
188-
`/components/twitter/sources/search-mentions/search-mentions.js`.
192+
`/components/twitter/sources/search-mentions/search-mentions.mjs`.
189193
- Aside from `app_slug`, words in folder and file names are separated by dashes
190194
(-) (i.e., in kebab case)
195+
- Common files (e.g., `common.mjs`, `utils.mjs`) must be placed within a common
196+
folder: `/common/common.mjs`.
191197

192198
You can explore examples in the [components
193199
directory](https://github.com/PipedreamHQ/pipedream/tree/master/components).
@@ -329,7 +335,7 @@ If users are required to enter sensitive data, always use
329335

330336
App files contain components that declare the app and include prop definitions
331337
and methods that may be reused across components. App files should adhere to the
332-
following naming convention: `[app_name_slug].app.js`. If an app file does not
338+
following naming convention: `[app_name_slug].app.mjs`. If an app file does not
333339
exist for your app, please [reach
334340
out](https://pipedream.com/community/c/dev/11).
335341

@@ -402,18 +408,20 @@ with this approach is that it increases complexity for end-users who have the
402408
option of customizing the code for components within Pipedream. When using this
403409
approach, the general pattern is:
404410

405-
- The `.app.js` module contains the logic related to making the actual API calls
411+
- The `.app.mjs` module contains the logic related to making the actual API calls
406412
(e.g. calling `axios.get`, encapsulate the API URL and token, etc).
407-
- The `common.js` module contains logic and structure that is not specific to
413+
- The `common.mjs` module contains logic and structure that is not specific to
408414
any single component. Its structure is equivalent to a component, except that
409415
it doesn't define attributes such as `version`, `dedupe`, `key`, `name`, etc
410416
(those are specific to each component). It defines the main logic/flow and
411417
relies on calling its methods (which might not be implemented by this
412418
component) to get any necessary data that it needs. In OOP terms, it would be
413419
the equivalent of a base abstract class.
414-
- The component module of each action would inherit/extend the `common.js`
420+
- The component module of each action would inherit/extend the `common.mjs`
415421
component by setting additional attributes (e.g. `name`, `description`, `key`,
416422
etc) and potentially redefining any inherited methods.
423+
- Common files (e.g., `common.mjs`, `utils.mjs`) must be placed within a common
424+
folder: `/common/common.mjs`.
417425

418426
See [Google
419427
Drive](https://github.com/PipedreamHQ/pipedream/tree/master/components/google_drive)
@@ -424,15 +432,13 @@ Please note that the name `common` is just a convention and depending on each
424432
case it might make sense to name any common module differently. For example, the
425433
[AWS
426434
sources](https://github.com/PipedreamHQ/pipedream/tree/master/components/aws)
427-
contains a `common` directory instead of a `common.js` file, and the directory
435+
contains a `common` directory instead of a `common.mjs` file, and the directory
428436
contains several modules that are shared between different event sources.
429437

430438
## Props
431439

432-
As a general rule of thumb, we should strive to only incorporate the 3-4 most
433-
relevant options from a given API as props. This is not a hard limit, but the
434-
goal is to optimize for usability. We should aim to solve specific use cases as
435-
simply as possible.
440+
As a general rule of thumb, we should strive to incorporate all
441+
relevant options from a given API as props.
436442

437443
### Labels
438444

@@ -445,7 +451,7 @@ but its label is set to “Search Term”.
445451

446452
### Descriptions
447453

448-
Include a description for [props](/components/contributing/api/#user-input-props) if it helps
454+
Include a description for [props](/components/contributing/api/#user-input-props) to help
449455
the user understand what they need to do. Use Markdown as appropriate to improve
450456
the clarity of the description or instructions. When using Markdown:
451457

@@ -781,6 +787,10 @@ know the content being returned is likely to be large – e.g. files — don't
781787
export the full content. Consider writing the data to the `/tmp` directory and
782788
exporting a reference to the file.
783789

790+
## Miscellaneous
791+
792+
- Use camelCase for all props, method names, and variables.
793+
784794
## Database Components
785795

786796
Pipedream supports a special category of apps called ["databases"](/workflows/data-management/databases/),

platform/__tests__/file-stream.js

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
const {
2+
getFileStream, getFileStreamAndMetadata,
3+
} = require("../dist");
4+
const fs = require("fs");
5+
const path = require("path");
6+
const http = require("http");
7+
const os = require("os");
8+
9+
// Helper function to read content from a readable stream
10+
async function readStreamContent(stream) {
11+
let content = "";
12+
stream.on("data", (chunk) => {
13+
content += chunk.toString();
14+
});
15+
16+
await new Promise((resolve) => {
17+
stream.on("end", resolve);
18+
});
19+
20+
return content;
21+
}
22+
23+
// Helper function to wait for stream cleanup by listening to close event
24+
async function waitForStreamCleanup(stream) {
25+
return new Promise((resolve) => {
26+
stream.on("close", resolve);
27+
});
28+
}
29+
30+
describe("file-stream", () => {
31+
let testFilePath;
32+
let server;
33+
const testPort = 3892;
34+
35+
beforeAll(() => {
36+
// Create a test file
37+
testFilePath = path.join(__dirname, "test-file.txt");
38+
fs.writeFileSync(testFilePath, "test content for file stream");
39+
40+
// Create a simple HTTP server for testing remote files
41+
server = http.createServer((req, res) => {
42+
if (req.url === "/test-file.txt") {
43+
res.writeHead(200, {
44+
"Content-Type": "text/plain",
45+
"Content-Length": "28",
46+
"Last-Modified": new Date().toUTCString(),
47+
"ETag": "\"test-etag\"",
48+
});
49+
res.end("test content for file stream");
50+
} else if (req.url === "/no-content-length") {
51+
res.writeHead(200, {
52+
"Content-Type": "application/json",
53+
"Last-Modified": new Date().toUTCString(),
54+
});
55+
res.end("{\"test\": \"data\"}");
56+
} else if (req.url === "/error") {
57+
res.writeHead(404, "Not Found");
58+
res.end();
59+
} else {
60+
res.writeHead(404, "Not Found");
61+
res.end();
62+
}
63+
});
64+
65+
return new Promise((resolve) =>
66+
server.listen(testPort, resolve));
67+
});
68+
69+
afterAll(() => {
70+
// Clean up test file
71+
if (fs.existsSync(testFilePath)) {
72+
fs.unlinkSync(testFilePath);
73+
}
74+
75+
if (server) {
76+
return new Promise((resolve) =>
77+
server.close(resolve));
78+
}
79+
});
80+
81+
describe("getFileStream", () => {
82+
it("should return readable stream for local file", async () => {
83+
const stream = await getFileStream(testFilePath);
84+
expect(stream).toBeDefined();
85+
expect(typeof stream.read).toBe("function");
86+
87+
const content = await readStreamContent(stream);
88+
expect(content).toBe("test content for file stream");
89+
});
90+
91+
it("should return readable stream for remote URL", async () => {
92+
const stream = await getFileStream(`http://localhost:${testPort}/test-file.txt`);
93+
expect(stream).toBeDefined();
94+
expect(typeof stream.read).toBe("function");
95+
96+
const content = await readStreamContent(stream);
97+
expect(content).toBe("test content for file stream");
98+
});
99+
100+
it("should throw error for invalid URL", async () => {
101+
await expect(getFileStream(`http://localhost:${testPort}/error`))
102+
.rejects.toThrow("Failed to fetch");
103+
});
104+
105+
it("should throw error for non-existent local file", async () => {
106+
await expect(getFileStream("/non/existent/file.txt"))
107+
.rejects.toThrow();
108+
});
109+
});
110+
111+
describe("getFileStreamAndMetadata", () => {
112+
it("should return stream and metadata for local file", async () => {
113+
const result = await getFileStreamAndMetadata(testFilePath);
114+
115+
expect(result.stream).toBeDefined();
116+
expect(typeof result.stream.read).toBe("function");
117+
expect(result.metadata).toMatchObject({
118+
size: 28,
119+
name: "test-file.txt",
120+
});
121+
expect(result.metadata.lastModified.constructor.name).toBe("Date");
122+
const content = await readStreamContent(result.stream);
123+
expect(content).toBe("test content for file stream");
124+
});
125+
126+
it("should return stream and metadata for remote file with content-length", async () => {
127+
const result = await getFileStreamAndMetadata(`http://localhost:${testPort}/test-file.txt`);
128+
129+
expect(result.stream).toBeDefined();
130+
expect(typeof result.stream.read).toBe("function");
131+
expect(result.metadata).toMatchObject({
132+
size: 28,
133+
contentType: "text/plain",
134+
name: "test-file.txt",
135+
etag: "\"test-etag\"",
136+
});
137+
expect(result.metadata.lastModified).toBeInstanceOf(Date);
138+
const content = await readStreamContent(result.stream);
139+
expect(content).toBe("test content for file stream");
140+
});
141+
142+
it("should handle remote file without content-length", async () => {
143+
const result = await getFileStreamAndMetadata(`http://localhost:${testPort}/no-content-length`);
144+
145+
expect(result.stream).toBeDefined();
146+
expect(typeof result.stream.read).toBe("function");
147+
148+
expect(result.metadata).toMatchObject({
149+
size: 16, // Size determined after download
150+
contentType: "application/json",
151+
});
152+
expect(result.metadata.lastModified).toBeInstanceOf(Date);
153+
154+
const content = await readStreamContent(result.stream);
155+
expect(content).toBe("{\"test\": \"data\"}");
156+
});
157+
158+
it("should throw error for invalid remote URL", async () => {
159+
await expect(getFileStreamAndMetadata(`http://localhost:${testPort}/error`))
160+
.rejects.toThrow("Failed to fetch");
161+
});
162+
});
163+
164+
describe("temporary file cleanup", () => {
165+
it("should clean up temporary files after stream ends", async () => {
166+
const tmpDir = os.tmpdir();
167+
const tempFilesBefore = fs.readdirSync(tmpDir);
168+
const result = await getFileStreamAndMetadata(`http://localhost:${testPort}/no-content-length`);
169+
170+
const content = await readStreamContent(result.stream);
171+
// Wait for cleanup to complete by listening to close event
172+
await waitForStreamCleanup(result.stream);
173+
174+
// Check that temp files were cleaned up
175+
const tempFilesAfter = fs.readdirSync(tmpDir);
176+
expect(tempFilesAfter.length).toEqual(tempFilesBefore.length);
177+
expect(content).toBe("{\"test\": \"data\"}");
178+
});
179+
180+
it("should clean up temporary files on stream error", async () => {
181+
// Check temp files before
182+
const tmpDir = os.tmpdir();
183+
const tempFilesBefore = fs.readdirSync(tmpDir);
184+
185+
const result = await getFileStreamAndMetadata(`http://localhost:${testPort}/no-content-length`);
186+
187+
// Trigger an error and wait for cleanup
188+
result.stream.destroy(new Error("Test error"));
189+
await waitForStreamCleanup(result.stream);
190+
191+
// Check that temp files were cleaned up
192+
const tempFilesAfter = fs.readdirSync(tmpDir);
193+
expect(tempFilesAfter.length).toEqual(tempFilesBefore.length);
194+
});
195+
});
196+
});

platform/dist/file-stream.d.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/// <reference types="node" />
2+
import { Readable } from "stream";
3+
export interface FileMetadata {
4+
size: number;
5+
contentType?: string;
6+
lastModified?: Date;
7+
name?: string;
8+
etag?: string;
9+
}
10+
/**
11+
* @param pathOrUrl - a file path or a URL
12+
* @returns a Readable stream of the file content
13+
*/
14+
export declare function getFileStream(pathOrUrl: string): Promise<Readable>;
15+
/**
16+
* @param pathOrUrl - a file path or a URL
17+
* @returns a Readable stream of the file content and its metadata
18+
*/
19+
export declare function getFileStreamAndMetadata(pathOrUrl: string): Promise<{
20+
stream: Readable;
21+
metadata: FileMetadata;
22+
}>;

0 commit comments

Comments
 (0)