Skip to content

Commit 9754609

Browse files
authored
Rate limit and attachment support (#24)
## Summary > Note: Should be reviewed alongside devrev/airdrop-trello-snap-in#11. These were the changes needed while I was developing functionality for: - Attachment extraction - Rate limiting These changes include, but not limited to: - Updates to `mock_devrev_server.py` - Proxy server implementation - Improving `attachment-extraction.md` - Improving document with rules for emitting events - Added a guide for metadata extraction (copied from the fern API docs) - Updates in `test_data/` - Updates in the Plain template ## Related issues https://app.devrev.ai/devrev/works/ISS-180117
1 parent 35220ab commit 9754609

23 files changed

+894
-143
lines changed

base_folder/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
"yargs": "^17.6.2"
5353
},
5454
"dependencies": {
55-
"@devrev/ts-adaas": "1.5.1",
55+
"@devrev/ts-adaas": "1.9.0",
5656
"@devrev/typescript-sdk": "1.1.63",
5757
"axios": "^1.9.0",
5858
"dotenv": "^16.0.3",

base_folder/src/core/types.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/**
2+
* Type definitions for DevRev function inputs and related types
3+
*/
4+
5+
export type Context = {
6+
// ID of the dev org for which the function is being invoked.
7+
dev_oid: string;
8+
// ID of the automation/command/snap-kit Action/Event Source for which the function is being invoked.
9+
source_id: string;
10+
// ID of the snap-in as part of which the function is being invoked.
11+
snap_in_id: string;
12+
// ID of the snap-in Version as part of which the function is being invoked.
13+
snap_in_version_id: string;
14+
// ID of the service account.
15+
service_account_id: string;
16+
// This secrets map would contain some secrets which platform would provide to the snap-in.
17+
// `service_account_token`: This is the token of the service account which belongs to this snap-in. This can be used to make API calls to DevRev.
18+
// `actor_session_token`: For commands, and snap-kits, where the user is performing some action, this is the token of the user who is performing the action.
19+
secrets: Record<string, string>;
20+
};
21+
22+
export type ExecutionMetadata = {
23+
// A unique id for the function invocation. Can be used to filter logs for a particular invocation.
24+
request_id: string;
25+
// Function name as defined in the manifest being invoked.
26+
function_name: string;
27+
// Type of event that triggered the function invocation as defined in manifest.
28+
event_type: string;
29+
// DevRev endpoint to which the function can make API calls.
30+
// Example : "https://api.devrev.ai/"
31+
devrev_endpoint: string;
32+
};
33+
34+
export type InputData = {
35+
// Map of organization inputs and their corresponding values stored in snap-in.
36+
// The values are passed as string and typing need to be handled by the function
37+
global_values: Record<string, string>;
38+
// Map of event sources and their corresponding ids stored in snap-in.
39+
// These could be used to schedule events on a schedule based event source.
40+
event_sources: Record<string, string>;
41+
};
42+
43+
// Event sent to our app.
44+
export type FunctionInput = {
45+
// Actual payload of the event.
46+
payload: Record<string, any>;
47+
// Context of the function invocation.
48+
context: Context;
49+
// Metadata of the function invocation.
50+
execution_metadata: ExecutionMetadata;
51+
input_data: InputData;
52+
};

base_folder/src/core/utils.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { AirdropEvent, AirdropMessage } from '@devrev/ts-adaas';
2+
import { FunctionInput } from './types';
3+
4+
export function convertToAirdropEvent(fi: FunctionInput): AirdropEvent {
5+
// Create a properly structured AirdropMessage
6+
const airdropMessage: AirdropMessage = {
7+
connection_data: fi.payload.connection_data,
8+
event_context: {
9+
...fi.payload.event_context,
10+
...fi.context,
11+
request_id: fi.execution_metadata?.request_id,
12+
},
13+
event_type: fi.payload.event_type,
14+
event_data: fi.payload.event_data || {},
15+
};
16+
17+
return {
18+
context: {
19+
...fi.context,
20+
secrets: {
21+
service_account_token: fi.context.secrets?.service_account_token || '',
22+
...fi.context.secrets,
23+
},
24+
},
25+
payload: airdropMessage,
26+
execution_metadata: fi.execution_metadata,
27+
input_data: fi.input_data,
28+
};
29+
}
30+

devrev-snapin-template.plain

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@
1111
- Please note that EventContextIn and EventContextOut interfaces are deprecated.
1212

1313
- The External Domain Metadata is a JSON object that describes the logical domain model of an external system. It defines the types of external record types, the relationships between those record types, and the metadata associated with each field, including the field’s data type, human-readable name, and other descriptive attributes.
14+
- Note: Resource "attachments" should not be included in The External Domain Metadata JSON object even if the external system supports attachments. This is handled automatically by The Airdrop SDK Library.
1415

1516
- The Initial Domain Mapping is a JSON object that defines how the logical domain model described by The External Domain Metadata maps to the target domain model in DevRev. It specifies the correspondence between each external record type and DevRev leaf type, as well as the mapping of individual fields, including transformation logic, value conversions, and any fixed or default values.
17+
- Note: Resource "attachments" should not be included in The Initial Domain Mapping JSON object even if the external system supports attachments. This is handled automatically by The Airdrop SDK Library.
1618

1719
- The External Sync Unit Item Count is a numeric field representing the total number of items (such as cards) contained within a given external sync unit. It is accessible as "item_count" field of the external sync unit.
1820

@@ -27,7 +29,7 @@
2729

2830
- The Snap-In will run on Node.js as the runtime environment.
2931

30-
- The Snap-In should use The Airdrop SDK Library version "1.8.0" for handling data extraction and loading, pushing data, event-driven actions, state management, and artifact handling.
32+
- The Snap-In should use The Airdrop SDK Library version "1.9.0" for handling data extraction and loading, pushing data, event-driven actions, state management, and artifact handling.
3133

3234
- Use snake_case for JSON keys.
3335

@@ -57,41 +59,59 @@
5759
- The Conformance Tests should use the The Test Snap-In Server - do not use mock server.
5860

5961
- The Conformance Tests should spin a server at http://localhost:8002 for testing callbacks (The Callback Server).
62+
- The Conformance Tests *MUST NOT* send the event directly to The Callback Server.
6063

6164
- The DevRev Server runs at http://localhost:8003, so it must not be mocked.
6265

6366
- The Worker Data Server is part of The DevRev Server and is accessible at http://localhost:8003/external-worker, so it must not be mocked.
6467

68+
- The API server is running at http://localhost:8004, so it must not be mocked.
69+
6570
- The `jest.setup.js` file is managed externally and cannot be modified within The Conformance Tests.
6671

6772
- All test files from The Conformance Tests must use the `.test.ts` suffix to be discoverable by Jest's default test pattern matching.
6873

6974
- The Conformance Tests should send a single event to the Snap-In Server in the request body without wrapping it in an array.
75+
- It is safe to assume that event payload will never be undefined, null, or empty.
76+
77+
- If generating The Acceptance Test that references a JSON file (The Attached JSON Of The Acceptance Test) in the resource, you should:
78+
- Store The Attached JSON Of The Acceptance Test in a separate JSON file.
79+
- Make sure to replace placeholders of the credentials (in `event["payload"]["connection_data"]["key"]` and `event["payload"]["connection_data"]["org_id"]`) in The Attached JSON Of The Acceptance Test with the actual values (credentials from The API).
7080

81+
- The Conformance Tests should encourage code reuse. Most notably, you should avoid duplication of the following:
82+
- Setting up The Callback Server,
83+
- Reading environment variables,
84+
- Defining the test event payload sent to The Snap-In Server.
7185

7286
# The TypeScript Node.js application boilerplate
7387

7488
***Non-Functional Requirements:***
7589

7690
- Logic of pushing data to The DevRev Servers must be implemented in the worker thread and invoked with the "spawn" method from The Airdrop SDK Library.
7791

92+
- The Worker Thread must be implemented in TypeScript and *The Worker Thread MUST BE* referenced with a .ts (TypeScript) extension, not .js (JavaScript).
93+
7894
- The Function being implemented *MUST not* be called "extraction" or "loading".
7995

8096
***Functional Requirements:***
8197

8298
- Implement The Function that only checks if The Function can be invoked.
8399

84-
- Implement The Function called "test_external_sync_units" that provides a test of the 'external sync units' part of the extraction workflow as described in the resource [external-sync-units-extraction.mdx](docs/external-sync-units-extraction.mdx). Only check if the extraction can be triggered and that it successfully completes. Disregard all other details.
100+
- Implement The Function called "test_external_sync_units" (The External Sync Units Test Function) that provides a test of the 'external sync units' part of the extraction workflow as described in the resource [external-sync-units-extraction.mdx](docs/external-sync-units-extraction.mdx).
101+
- If "event_type" equals "EXTRACTION_EXTERNAL_SYNC_UNITS_START", The External Sync Units Test Function should:
102+
- Only emit event "EXTRACTION_EXTERNAL_SYNC_UNITS_DONE".
85103

86104
***Acceptance Tests:***
87105

88106
- Test The Function using the resource [external_sync_unit_check.json](test_data/external_sync_unit_check.json). Test is successful if The Callback Server receives from DevRev an event with "event_type" that equals "EXTRACTION_EXTERNAL_SYNC_UNITS_DONE". The test must not send the event directly to The Callback Server.
89107

90108
- Implement The Function called "data_extraction_check" (The Data Extraction Check Function) that provides a test of the 'data extraction' part of the extraction workflow as described in the resource [data-extraction.md](docs/data-extraction.md). The Data Extraction Check Function should:
91-
- Initialize a single repo "users"
92-
- normalize the users data using The Normalization Function
93-
- push the normalized users data to The DevRev Servers
94-
- Note: The Data Extraction Check Function should be simplified and should disregard all other details.
109+
- if "event_type" equals "EXTRACTION_DATA_START" or "EXTRACTION_ATTACHMENTS_CONTINUE", The Data Extraction Check Function should:
110+
- Initialize a single repo "users"
111+
- normalize the users data using The Normalization Function
112+
- push the normalized users data to The DevRev Servers
113+
- Emit event "EXTRACTION_DATA_DONE".
114+
- Note: The Data Extraction Check Function should be simplified and should disregard all other details.
95115

96116
***Acceptance Tests:***
97117

docs/attachment_normalization.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Attachment normalization
2+
3+
Attachments have a special way of normalization compared to other resources. Function for normalizing "attachments" should return an object "NormalizedAttachment" instead of "NormalizedItem".
4+
5+
## NormalizedAttachment
6+
7+
NormalizedAttachment represents the standardized structure of an attachment after normalization in the Airdrop platform. This interface defines the essential properties needed to identify and link attachments to their parent items.
8+
9+
### Properties
10+
11+
- _url_
12+
13+
Required. A **string** representing the URL where the attachment can be accessed.
14+
15+
- _id_
16+
17+
Required. A **string** that uniquely identifies the normalized attachment.
18+
19+
- _file_name_
20+
21+
Required. A **string** representing the name of the attachment file.
22+
23+
- _parent_id_
24+
25+
Required. A **string** identifying the parent item this attachment belongs to.
26+
27+
- _author_id_
28+
29+
Optional. A **string** identifying the author or creator of the attachment.
30+
31+
- _grand_parent_id_
32+
33+
Optional. A **number** identifying a higher-level parent entity, if applicable.
34+
35+
### Example
36+
37+
```typescript
38+
export function normalizeAttachment(item: any): NormalizedAttachment {
39+
return {
40+
id: item.gid,
41+
url: item.download_url,
42+
file_name: item.name,
43+
parent_id: item.parent_id,
44+
};
45+
}
46+
```
47+
48+
### Further remarks
49+
50+
Note:
51+
52+
- In the example above, parent_id should be the ID of the resource that the attachment belongs to. For example, if we're normalizing an attachment for a task, parent_id should be the ID of the task.

docs/attachments-extraction.md

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,19 +22,41 @@ import {
2222
The `getAttachmentStream` function is responsible for fetching and streaming attachments from their source URLs.
2323

2424
```typescript
25+
import {
26+
axiosClient,
27+
ExternalSystemAttachmentStreamingParams,
28+
ExternalSystemAttachmentStreamingResponse,
29+
axios,
30+
serializeAxiosError,
31+
... // Other imports from @devrev/ts-adaas
32+
} from '@devrev/ts-adaas';
33+
2534
const getAttachmentStream = async ({
2635
item,
2736
}: ExternalSystemAttachmentStreamingParams): Promise<ExternalSystemAttachmentStreamingResponse> => {
37+
// IMPORTANT: "url" is not necessarily deployed on the base URL of The API. It could also be an external URL (e.g. https://example.com/attachment.pdf, https://devrev.ai, ...)
2838
const { id, url } = item;
2939

40+
// NOTE: Import axiosClient directly from @devrev/ts-adaas
3041
try {
42+
// IMPORTANT: If the URL is protected by authentication from The API, you should also use the appropriate credentials.
3143
const fileStreamResponse = await axiosClient.get(url, {
3244
responseType: 'stream',
3345
headers: {
3446
'Accept-Encoding': 'identity',
47+
'Authorization': ... // TODO: Authorization if needed
3548
},
3649
});
3750

51+
// Check if we were rate limited
52+
if (fileStreamResponse.status === 429) {
53+
const delay = ... // TODO: Calculate the delay from The API
54+
return {
55+
delay: delay
56+
};
57+
}
58+
59+
// IMPORTANT: "httpStream" should be directly Axios response stream (including headers, data, and everything else)
3860
return { httpStream: fileStreamResponse };
3961
} catch (error) {
4062
// Error handling logic
@@ -102,13 +124,6 @@ processTask({
102124
- Failed attachment metadata is logged for debugging
103125
- Error responses include the attachment ID for traceability
104126

105-
### Event Emission
106-
The worker emits different events based on the processing outcome:
107-
- `ExtractionAttachmentsDelay`: When processing needs to be delayed
108-
- `ExtractionAttachmentsError`: When an error occurs
109-
- `ExtractionAttachmentsDone`: When processing completes successfully
110-
- `ExtractionAttachmentsProgress`: During timeout handling
111-
112127
### Timeout Handling
113128
- On timeout, the current state is posted to the platform
114129
- A progress event is emitted with 50% completion
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Data Extraction Rules for Emitting Events
2+
3+
## Input event: `EXTRACTION_DATA_START` or `EXTRACTION_DATA_CONTINUE`
4+
5+
Here are the rules when `EXTRACTION_DATA_START` or `EXTRACTION_DATA_CONTINUE` event is received by the snap-in:
6+
7+
- If there's a timeout ("onTimeout" method in "spawn" method), the snap-in should emit an event with type `EXTRACTION_DATA_PROGRESS` (no "data" payload).
8+
- If the extraction is successful and all the resources have been extracted, the snap-in should emit an event with type `EXTRACTION_DATA_DONE` (no "data" payload).
9+
- In case of unexpected error, the snap-in should emit an event with type `EXTRACTION_DATA_ERROR` ("data" payload should contain attribute "error" - error message as a string).
10+
- In case of rate limiting (status code 429) for "data extraction", the snap-in *must* emit an event with type `EXTRACTION_DATA_DELAY`. If emitting event of type `EXTRACTION_DATA_DELAY`, you *must* also provide a "delay" attribute, specifying the delay in seconds as an integer.
11+
- Example: `await adapter.emit(ExtractorEventType.ExtractionDataDelay, { delay: "30" });`
12+
13+
## Input event: `EXTRACTION_EXTERNAL_SYNC_UNITS_START`
14+
15+
Here are the rules when `EXTRACTION_EXTERNAL_SYNC_UNITS_START` event is received by the snap-in:
16+
17+
- If "external sync unit extraction" is successful and the snap-in has extracted all the external sync units, the snap-in should emit an event with type `EXTRACTION_EXTERNAL_SYNC_UNITS_DONE` (no "data" payload).
18+
- In case of unexpected error, the snap-in should emit an event with type `EXTRACTION_EXTERNAL_SYNC_UNITS_ERROR` ("data" payload should contain attribute "error" - error message as a string).
19+
- In case of rate limiting (status code 429) for "external sync unit extraction", the snap-in should also emit an event with type `EXTRACTION_EXTERNAL_SYNC_UNITS_ERROR`.
20+
21+
## Input event: `EXTRACTION_METADATA_START`
22+
23+
Here are the rules when `EXTRACTION_METADATA_START` event is received by the snap-in:
24+
- If "metadata extraction" is successful and the snap-in has extracted all the metadata, the snap-in should emit an event with type `EXTRACTION_METADATA_DONE` (no "data" payload).
25+
- In case of unexpected error, the snap-in should emit an event with type `EXTRACTION_METADATA_ERROR` ("data" payload should contain attribute "error" - error message as a string).
26+
27+
## Input event: `EXTRACTION_ATTACHMENTS_START` or `EXTRACTION_ATTACHMENTS_CONTINUE`
28+
29+
Here are the rules when `EXTRACTION_ATTACHMENTS_START` or `EXTRACTION_ATTACHMENTS_CONTINUE` event is received by the snap-in:
30+
- If "attachments extraction" is successful and the snap-in has extracted all the attachments, the snap-in should emit an event with type "EXTRACTION_ATTACHMENTS_DONE"
31+
- If case of unexpected error, the snap-in should emit an event with type "EXTRACTION_ATTACHMENTS_ERROR" ("data" payload should contain attribute "error" - error message as a string).
32+
- In case of rate limiting (status code 429) for "attachments extraction", the snap-in should also emit an event with type "EXTRACTION_ATTACHMENTS_DELAY". If emitting event of type "EXTRACTION_ATTACHMENTS_DELAY", you *must* also provide a "delay" attribute, specifying the delay in seconds as an integer.
33+
- Example: `await adapter.emit(ExtractorEventType.ExtractionAttachmentsDelay, { delay: "30" });`
34+
- If there's a timeout ("onTimeout" method in "spawn" method), the snap-in should emit an event with type "EXTRACTION_ATTACHMENTS_PROGRESS".
35+
36+
37+
## IMPORTANT FOR ALL INPUT EVENTS
38+
39+
- In all cases, only a single event should be emitted.

docs/data-extraction.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,8 @@ processTask<ExtractorState>({
100100
});
101101
```
102102

103+
NOTE: Do not call `initializeRepos` multiple times. You should call it only once with all the repositories.
104+
103105
After initialization of repositories using `initializeRepos`,
104106
items should be then retrieved from the external system and stored in the correct repository by calling the `push` function.
105107

docs/function_invocation.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ A function can be invoked synchronously or asynchronously.
33
You need to implement the run method in your function. The run method is called when the function is invoked. The run method signature is defined below:
44

55
```typescript
6-
async function run(events: any[]): any;
6+
async function run(events: FunctionInput[]): any;
77
```
88

99
The value returned from the `run` method is passed back in synchronous execution modes, such as commands, snap kit actions, and event source synchronous execution. In asynchronous execution modes, such as automation execution, the return value is ignored.

0 commit comments

Comments
 (0)