@rawdash/connector-asana 0.27.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 ADDED
@@ -0,0 +1,106 @@
1
+ <!-- This file is generated from connector metadata by scripts/generate-connector-docs.ts. Do not edit by hand. -->
2
+
3
+ # @rawdash/connector-asana
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@rawdash/connector-asana)](https://www.npmjs.com/package/@rawdash/connector-asana)
6
+ [![license](https://img.shields.io/npm/l/@rawdash/connector-asana)](https://github.com/rawdash/rawdash/blob/main/LICENSE)
7
+
8
+ Sync projects, users, tasks, and task state-change events from an Asana workspace.
9
+
10
+ ## Install
11
+
12
+ ```sh
13
+ npm install @rawdash/connector-asana
14
+ ```
15
+
16
+ ## Authentication
17
+
18
+ Authenticates with a personal access token sent as a Bearer credential. The token inherits the permissions of the account that created it.
19
+
20
+ 1. Open app.asana.com -> Settings -> Apps -> Developer apps.
21
+ 2. Under Personal access tokens, create a new token and copy its value.
22
+ 3. Store the token as a secret and reference it from the connector config as `apiToken: secret("ASANA_API_TOKEN")`, alongside the numeric workspaceGid.
23
+ 4. Find your workspace GID at https://app.asana.com/api/1.0/workspaces while authenticated.
24
+
25
+ ## Configuration
26
+
27
+ | Field | Type | Required | Description |
28
+ | -------------- | ------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
29
+ | `apiToken` | secret | Yes | Asana personal access token. Create one at app.asana.com → Settings → Apps → Developer apps → Personal access tokens. |
30
+ | `workspaceGid` | string | Yes | Numeric GID of the workspace to sync. Find it at app.asana.com/api/1.0/workspaces. |
31
+ | `projectGids` | array | No | Restrict the task sync to specific project GIDs. Omit to sync tasks from every project in the workspace. |
32
+ | `resources` | array | No | Which Asana resources to sync. Omit to sync all of them. 'task_events' shares the tasks scan - enabling it without 'tasks' still walks tasks (and fetches their stories) but skips writing task entities. |
33
+
34
+ ## Resources
35
+
36
+ - **`asana_project`** _(entity)_ - Projects in the workspace with name, archived state, owner, team, and timestamps.
37
+ - Endpoint: `GET /projects`
38
+ - **`asana_user`** _(entity)_ - Users in the workspace with display name and email.
39
+ - Endpoint: `GET /users`
40
+ - **`asana_task`** _(entity)_ - Tasks with completion state, assignee, due date, owning project, and timestamps.
41
+ - Endpoint: `GET /tasks?project={projectGid}`
42
+ - Tasks are walked project-by-project; a task in multiple projects is attributed to the first project scanned.
43
+ - **`asana_task_event`** _(event)_ - Task state-change events derived from system stories (completed, assigned, due-date changes, etc.).
44
+ - Endpoint: `GET /tasks/{taskGid}/stories`
45
+ - Only system stories are written; comments are skipped. start_ts is the story time, end_ts is null. Timestamps are Unix epoch milliseconds.
46
+
47
+ ## Example
48
+
49
+ ```ts
50
+ import {
51
+ defineConfig,
52
+ defineDashboard,
53
+ defineMetric,
54
+ secret,
55
+ } from '@rawdash/core';
56
+
57
+ const asana = {
58
+ name: 'asana',
59
+ connectorId: 'asana',
60
+ config: {
61
+ apiToken: secret('ASANA_API_TOKEN'),
62
+ workspaceGid: '1201234567890',
63
+ },
64
+ };
65
+
66
+ export default defineConfig({
67
+ connectors: [asana],
68
+ dashboards: {
69
+ delivery: defineDashboard({
70
+ widgets: {
71
+ open_tasks: {
72
+ kind: 'stat',
73
+ title: 'Open Tasks',
74
+ metric: defineMetric({
75
+ connector: asana,
76
+ shape: 'entity',
77
+ entityType: 'asana_task',
78
+ fn: 'count',
79
+ filter: [{ field: 'completed', op: 'eq', value: false }],
80
+ }),
81
+ },
82
+ },
83
+ }),
84
+ },
85
+ });
86
+ ```
87
+
88
+ ## Rate limits
89
+
90
+ Asana enforces per-token rate limits (150 req/min on free plans, 1500 on paid); 429 responses with Retry-After are honored.
91
+
92
+ ## Limitations
93
+
94
+ - Task state-change events are derived from each task story; only system stories (not comments) are written.
95
+ - A task in multiple projects is stored once, attributed to the first project it is scanned under.
96
+ - Workspace-wide task search requires a paid plan, so tasks are walked project-by-project; omit projectGids to scan every project.
97
+
98
+ ## Links
99
+
100
+ - [Rawdash docs](https://rawdash.dev/docs/connectors)
101
+ - [Asana API docs](https://developers.asana.com/reference/rest-api-reference)
102
+ - [GitHub](https://github.com/rawdash/rawdash)
103
+
104
+ ## License
105
+
106
+ Apache-2.0
@@ -0,0 +1,339 @@
1
+ import { BaseConnector, ConnectorContext, SyncOptions, StorageHandle, SyncResult, ConnectorDoc } from '@rawdash/core';
2
+ import { z } from 'zod';
3
+
4
+ declare const configFields: z.ZodObject<{
5
+ apiToken: z.ZodObject<{
6
+ $secret: z.ZodString;
7
+ }, z.core.$strip>;
8
+ workspaceGid: z.ZodString;
9
+ projectGids: z.ZodOptional<z.ZodArray<z.ZodString>>;
10
+ resources: z.ZodOptional<z.ZodArray<z.ZodEnum<{
11
+ projects: "projects";
12
+ users: "users";
13
+ tasks: "tasks";
14
+ task_events: "task_events";
15
+ }>>>;
16
+ }, z.core.$strip>;
17
+ declare const doc: ConnectorDoc;
18
+ type AsanaResource = 'projects' | 'users' | 'tasks' | 'task_events';
19
+ interface AsanaSettings {
20
+ workspaceGid: string;
21
+ projectGids?: readonly string[];
22
+ resources?: readonly AsanaResource[];
23
+ }
24
+ declare const asanaCredentials: {
25
+ apiToken: {
26
+ description: string;
27
+ auth: "required";
28
+ };
29
+ };
30
+ type AsanaCredentials = typeof asanaCredentials;
31
+ declare const asanaResources: {
32
+ readonly asana_project: {
33
+ readonly shape: "entity";
34
+ readonly filterable: [{
35
+ readonly field: "archived";
36
+ readonly ops: ["eq"];
37
+ }];
38
+ readonly description: "Projects in the workspace with name, archived state, owner, team, and timestamps.";
39
+ readonly endpoint: "GET /projects";
40
+ readonly responses: {
41
+ readonly projects: z.ZodObject<{
42
+ data: z.ZodArray<z.ZodObject<{
43
+ gid: z.ZodString;
44
+ name: z.ZodString;
45
+ archived: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
46
+ created_at: z.ZodOptional<z.ZodNullable<z.ZodISODateTime>>;
47
+ modified_at: z.ZodOptional<z.ZodNullable<z.ZodISODateTime>>;
48
+ owner: z.ZodOptional<z.ZodNullable<z.ZodObject<{
49
+ gid: z.ZodString;
50
+ }, z.core.$strip>>>;
51
+ team: z.ZodOptional<z.ZodNullable<z.ZodObject<{
52
+ name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
53
+ }, z.core.$strip>>>;
54
+ }, z.core.$strip>>;
55
+ next_page: z.ZodOptional<z.ZodNullable<z.ZodObject<{
56
+ offset: z.ZodString;
57
+ }, z.core.$strip>>>;
58
+ }, z.core.$strip>;
59
+ };
60
+ };
61
+ readonly asana_user: {
62
+ readonly shape: "entity";
63
+ readonly filterable: [];
64
+ readonly description: "Users in the workspace with display name and email.";
65
+ readonly endpoint: "GET /users";
66
+ readonly responses: {
67
+ readonly users: z.ZodObject<{
68
+ data: z.ZodArray<z.ZodObject<{
69
+ gid: z.ZodString;
70
+ name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
71
+ email: z.ZodOptional<z.ZodNullable<z.ZodString>>;
72
+ }, z.core.$strip>>;
73
+ next_page: z.ZodOptional<z.ZodNullable<z.ZodObject<{
74
+ offset: z.ZodString;
75
+ }, z.core.$strip>>>;
76
+ }, z.core.$strip>;
77
+ };
78
+ };
79
+ readonly asana_task: {
80
+ readonly shape: "entity";
81
+ readonly filterable: [{
82
+ readonly field: "completed";
83
+ readonly ops: ["eq"];
84
+ }, {
85
+ readonly field: "projectGid";
86
+ readonly ops: ["eq"];
87
+ }, {
88
+ readonly field: "assigneeId";
89
+ readonly ops: ["eq"];
90
+ }];
91
+ readonly description: "Tasks with completion state, assignee, due date, owning project, and timestamps.";
92
+ readonly endpoint: "GET /tasks?project={projectGid}";
93
+ readonly notes: "Tasks are walked project-by-project; a task in multiple projects is attributed to the first project scanned.";
94
+ readonly responses: {
95
+ readonly tasks: z.ZodObject<{
96
+ data: z.ZodArray<z.ZodObject<{
97
+ gid: z.ZodString;
98
+ name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
99
+ completed: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
100
+ completed_at: z.ZodOptional<z.ZodNullable<z.ZodISODateTime>>;
101
+ created_at: z.ZodOptional<z.ZodNullable<z.ZodISODateTime>>;
102
+ modified_at: z.ZodOptional<z.ZodNullable<z.ZodISODateTime>>;
103
+ due_on: z.ZodOptional<z.ZodNullable<z.ZodString>>;
104
+ assignee: z.ZodOptional<z.ZodNullable<z.ZodObject<{
105
+ gid: z.ZodString;
106
+ }, z.core.$strip>>>;
107
+ }, z.core.$strip>>;
108
+ next_page: z.ZodOptional<z.ZodNullable<z.ZodObject<{
109
+ offset: z.ZodString;
110
+ }, z.core.$strip>>>;
111
+ }, z.core.$strip>;
112
+ };
113
+ };
114
+ readonly asana_task_event: {
115
+ readonly shape: "event";
116
+ readonly filterable: [];
117
+ readonly description: "Task state-change events derived from system stories (completed, assigned, due-date changes, etc.).";
118
+ readonly endpoint: "GET /tasks/{taskGid}/stories";
119
+ readonly notes: "Only system stories are written; comments are skipped. start_ts is the story time, end_ts is null. Timestamps are Unix epoch milliseconds.";
120
+ readonly responses: {
121
+ readonly stories: z.ZodObject<{
122
+ data: z.ZodArray<z.ZodObject<{
123
+ gid: z.ZodString;
124
+ type: z.ZodOptional<z.ZodNullable<z.ZodString>>;
125
+ resource_subtype: z.ZodOptional<z.ZodNullable<z.ZodString>>;
126
+ created_at: z.ZodISODateTime;
127
+ created_by: z.ZodOptional<z.ZodNullable<z.ZodObject<{
128
+ gid: z.ZodString;
129
+ }, z.core.$strip>>>;
130
+ text: z.ZodOptional<z.ZodNullable<z.ZodString>>;
131
+ }, z.core.$strip>>;
132
+ next_page: z.ZodOptional<z.ZodNullable<z.ZodObject<{
133
+ offset: z.ZodString;
134
+ }, z.core.$strip>>>;
135
+ }, z.core.$strip>;
136
+ };
137
+ };
138
+ };
139
+ declare const id = "asana";
140
+ declare class AsanaConnector extends BaseConnector<AsanaSettings, AsanaCredentials> {
141
+ static readonly id = "asana";
142
+ static readonly resources: {
143
+ readonly asana_project: {
144
+ readonly shape: "entity";
145
+ readonly filterable: [{
146
+ readonly field: "archived";
147
+ readonly ops: ["eq"];
148
+ }];
149
+ readonly description: "Projects in the workspace with name, archived state, owner, team, and timestamps.";
150
+ readonly endpoint: "GET /projects";
151
+ readonly responses: {
152
+ readonly projects: z.ZodObject<{
153
+ data: z.ZodArray<z.ZodObject<{
154
+ gid: z.ZodString;
155
+ name: z.ZodString;
156
+ archived: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
157
+ created_at: z.ZodOptional<z.ZodNullable<z.ZodISODateTime>>;
158
+ modified_at: z.ZodOptional<z.ZodNullable<z.ZodISODateTime>>;
159
+ owner: z.ZodOptional<z.ZodNullable<z.ZodObject<{
160
+ gid: z.ZodString;
161
+ }, z.core.$strip>>>;
162
+ team: z.ZodOptional<z.ZodNullable<z.ZodObject<{
163
+ name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
164
+ }, z.core.$strip>>>;
165
+ }, z.core.$strip>>;
166
+ next_page: z.ZodOptional<z.ZodNullable<z.ZodObject<{
167
+ offset: z.ZodString;
168
+ }, z.core.$strip>>>;
169
+ }, z.core.$strip>;
170
+ };
171
+ };
172
+ readonly asana_user: {
173
+ readonly shape: "entity";
174
+ readonly filterable: [];
175
+ readonly description: "Users in the workspace with display name and email.";
176
+ readonly endpoint: "GET /users";
177
+ readonly responses: {
178
+ readonly users: z.ZodObject<{
179
+ data: z.ZodArray<z.ZodObject<{
180
+ gid: z.ZodString;
181
+ name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
182
+ email: z.ZodOptional<z.ZodNullable<z.ZodString>>;
183
+ }, z.core.$strip>>;
184
+ next_page: z.ZodOptional<z.ZodNullable<z.ZodObject<{
185
+ offset: z.ZodString;
186
+ }, z.core.$strip>>>;
187
+ }, z.core.$strip>;
188
+ };
189
+ };
190
+ readonly asana_task: {
191
+ readonly shape: "entity";
192
+ readonly filterable: [{
193
+ readonly field: "completed";
194
+ readonly ops: ["eq"];
195
+ }, {
196
+ readonly field: "projectGid";
197
+ readonly ops: ["eq"];
198
+ }, {
199
+ readonly field: "assigneeId";
200
+ readonly ops: ["eq"];
201
+ }];
202
+ readonly description: "Tasks with completion state, assignee, due date, owning project, and timestamps.";
203
+ readonly endpoint: "GET /tasks?project={projectGid}";
204
+ readonly notes: "Tasks are walked project-by-project; a task in multiple projects is attributed to the first project scanned.";
205
+ readonly responses: {
206
+ readonly tasks: z.ZodObject<{
207
+ data: z.ZodArray<z.ZodObject<{
208
+ gid: z.ZodString;
209
+ name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
210
+ completed: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
211
+ completed_at: z.ZodOptional<z.ZodNullable<z.ZodISODateTime>>;
212
+ created_at: z.ZodOptional<z.ZodNullable<z.ZodISODateTime>>;
213
+ modified_at: z.ZodOptional<z.ZodNullable<z.ZodISODateTime>>;
214
+ due_on: z.ZodOptional<z.ZodNullable<z.ZodString>>;
215
+ assignee: z.ZodOptional<z.ZodNullable<z.ZodObject<{
216
+ gid: z.ZodString;
217
+ }, z.core.$strip>>>;
218
+ }, z.core.$strip>>;
219
+ next_page: z.ZodOptional<z.ZodNullable<z.ZodObject<{
220
+ offset: z.ZodString;
221
+ }, z.core.$strip>>>;
222
+ }, z.core.$strip>;
223
+ };
224
+ };
225
+ readonly asana_task_event: {
226
+ readonly shape: "event";
227
+ readonly filterable: [];
228
+ readonly description: "Task state-change events derived from system stories (completed, assigned, due-date changes, etc.).";
229
+ readonly endpoint: "GET /tasks/{taskGid}/stories";
230
+ readonly notes: "Only system stories are written; comments are skipped. start_ts is the story time, end_ts is null. Timestamps are Unix epoch milliseconds.";
231
+ readonly responses: {
232
+ readonly stories: z.ZodObject<{
233
+ data: z.ZodArray<z.ZodObject<{
234
+ gid: z.ZodString;
235
+ type: z.ZodOptional<z.ZodNullable<z.ZodString>>;
236
+ resource_subtype: z.ZodOptional<z.ZodNullable<z.ZodString>>;
237
+ created_at: z.ZodISODateTime;
238
+ created_by: z.ZodOptional<z.ZodNullable<z.ZodObject<{
239
+ gid: z.ZodString;
240
+ }, z.core.$strip>>>;
241
+ text: z.ZodOptional<z.ZodNullable<z.ZodString>>;
242
+ }, z.core.$strip>>;
243
+ next_page: z.ZodOptional<z.ZodNullable<z.ZodObject<{
244
+ offset: z.ZodString;
245
+ }, z.core.$strip>>>;
246
+ }, z.core.$strip>;
247
+ };
248
+ };
249
+ };
250
+ static readonly schemas: {
251
+ readonly projects: z.ZodObject<{
252
+ data: z.ZodArray<z.ZodObject<{
253
+ gid: z.ZodString;
254
+ name: z.ZodString;
255
+ archived: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
256
+ created_at: z.ZodOptional<z.ZodNullable<z.ZodISODateTime>>;
257
+ modified_at: z.ZodOptional<z.ZodNullable<z.ZodISODateTime>>;
258
+ owner: z.ZodOptional<z.ZodNullable<z.ZodObject<{
259
+ gid: z.ZodString;
260
+ }, z.core.$strip>>>;
261
+ team: z.ZodOptional<z.ZodNullable<z.ZodObject<{
262
+ name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
263
+ }, z.core.$strip>>>;
264
+ }, z.core.$strip>>;
265
+ next_page: z.ZodOptional<z.ZodNullable<z.ZodObject<{
266
+ offset: z.ZodString;
267
+ }, z.core.$strip>>>;
268
+ }, z.core.$strip>;
269
+ } & {
270
+ readonly users: z.ZodObject<{
271
+ data: z.ZodArray<z.ZodObject<{
272
+ gid: z.ZodString;
273
+ name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
274
+ email: z.ZodOptional<z.ZodNullable<z.ZodString>>;
275
+ }, z.core.$strip>>;
276
+ next_page: z.ZodOptional<z.ZodNullable<z.ZodObject<{
277
+ offset: z.ZodString;
278
+ }, z.core.$strip>>>;
279
+ }, z.core.$strip>;
280
+ } & {
281
+ readonly tasks: z.ZodObject<{
282
+ data: z.ZodArray<z.ZodObject<{
283
+ gid: z.ZodString;
284
+ name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
285
+ completed: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
286
+ completed_at: z.ZodOptional<z.ZodNullable<z.ZodISODateTime>>;
287
+ created_at: z.ZodOptional<z.ZodNullable<z.ZodISODateTime>>;
288
+ modified_at: z.ZodOptional<z.ZodNullable<z.ZodISODateTime>>;
289
+ due_on: z.ZodOptional<z.ZodNullable<z.ZodString>>;
290
+ assignee: z.ZodOptional<z.ZodNullable<z.ZodObject<{
291
+ gid: z.ZodString;
292
+ }, z.core.$strip>>>;
293
+ }, z.core.$strip>>;
294
+ next_page: z.ZodOptional<z.ZodNullable<z.ZodObject<{
295
+ offset: z.ZodString;
296
+ }, z.core.$strip>>>;
297
+ }, z.core.$strip>;
298
+ } & {
299
+ readonly stories: z.ZodObject<{
300
+ data: z.ZodArray<z.ZodObject<{
301
+ gid: z.ZodString;
302
+ type: z.ZodOptional<z.ZodNullable<z.ZodString>>;
303
+ resource_subtype: z.ZodOptional<z.ZodNullable<z.ZodString>>;
304
+ created_at: z.ZodISODateTime;
305
+ created_by: z.ZodOptional<z.ZodNullable<z.ZodObject<{
306
+ gid: z.ZodString;
307
+ }, z.core.$strip>>>;
308
+ text: z.ZodOptional<z.ZodNullable<z.ZodString>>;
309
+ }, z.core.$strip>>;
310
+ next_page: z.ZodOptional<z.ZodNullable<z.ZodObject<{
311
+ offset: z.ZodString;
312
+ }, z.core.$strip>>>;
313
+ }, z.core.$strip>;
314
+ } & Readonly<Record<string, z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>>>;
315
+ static create(input: unknown, ctx?: ConnectorContext): AsanaConnector;
316
+ readonly id = "asana";
317
+ readonly credentials: {
318
+ apiToken: {
319
+ description: string;
320
+ auth: "required";
321
+ };
322
+ };
323
+ private buildHeaders;
324
+ private fetch;
325
+ private activePhases;
326
+ private singleSpec;
327
+ private fetchCollection;
328
+ private fetchProjectsPage;
329
+ private fetchUsersPage;
330
+ private fetchTasksForProject;
331
+ private fetchStoriesForTask;
332
+ private fetchTasksPage;
333
+ private writeProjects;
334
+ private writeUsers;
335
+ private writeTasks;
336
+ sync(options: SyncOptions, storage: StorageHandle, signal?: AbortSignal): Promise<SyncResult>;
337
+ }
338
+
339
+ export { AsanaConnector, type AsanaResource, type AsanaSettings, configFields, AsanaConnector as default, doc, id, asanaResources as resources };
package/dist/index.js ADDED
@@ -0,0 +1,534 @@
1
+ // ../../connector-shared/dist/index.js
2
+ var HTTP_CLIENT_VERSION = "0.0.0";
3
+ var DEFAULT_USER_AGENT = `rawdash-connector/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;
4
+ function connectorUserAgent(connectorId) {
5
+ return `rawdash-connector-${connectorId}/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;
6
+ }
7
+ function parseEpoch(value, unit) {
8
+ if (value === null || value === void 0) {
9
+ return null;
10
+ }
11
+ if (unit === "iso") {
12
+ if (typeof value !== "string") {
13
+ return null;
14
+ }
15
+ const ms = new Date(value).getTime();
16
+ return Number.isFinite(ms) ? ms : null;
17
+ }
18
+ if (typeof value === "string" && value.trim() === "") {
19
+ return null;
20
+ }
21
+ const n = typeof value === "number" ? value : Number(value);
22
+ if (!Number.isFinite(n)) {
23
+ return null;
24
+ }
25
+ const result = unit === "s" ? n * 1e3 : n;
26
+ return Number.isFinite(result) ? result : null;
27
+ }
28
+
29
+ // src/asana.ts
30
+ import {
31
+ BaseConnector,
32
+ defineConfigFields,
33
+ defineConnectorDoc,
34
+ defineResources,
35
+ makeChunkedCursorGuard,
36
+ paginateChunked,
37
+ schemasFromResources,
38
+ selectActivePhases
39
+ } from "@rawdash/core";
40
+ import { z } from "zod";
41
+ var configFields = defineConfigFields(
42
+ z.object({
43
+ apiToken: z.object({ $secret: z.string() }).meta({
44
+ label: "Personal access token",
45
+ description: "Asana personal access token. Create one at app.asana.com \u2192 Settings \u2192 Apps \u2192 Developer apps \u2192 Personal access tokens.",
46
+ placeholder: "2/1201234567890/...",
47
+ secret: true
48
+ }),
49
+ workspaceGid: z.string().min(1).regex(/^\d+$/, "Workspace GID is the numeric id of the workspace.").meta({
50
+ label: "Workspace GID",
51
+ description: "Numeric GID of the workspace to sync. Find it at app.asana.com/api/1.0/workspaces.",
52
+ placeholder: "1201234567890"
53
+ }),
54
+ projectGids: z.array(z.string().regex(/^\d+$/)).nonempty().optional().meta({
55
+ label: "Project GIDs (optional)",
56
+ description: "Restrict the task sync to specific project GIDs. Omit to sync tasks from every project in the workspace."
57
+ }),
58
+ resources: z.array(z.enum(["projects", "users", "tasks", "task_events"])).nonempty().optional().meta({
59
+ label: "Resources",
60
+ description: "Which Asana resources to sync. Omit to sync all of them. 'task_events' shares the tasks scan - enabling it without 'tasks' still walks tasks (and fetches their stories) but skips writing task entities."
61
+ })
62
+ })
63
+ );
64
+ var doc = defineConnectorDoc({
65
+ displayName: "Asana",
66
+ category: "product",
67
+ brandColor: "#F06A6A",
68
+ tagline: "Sync projects, users, tasks, and task state-change events from an Asana workspace.",
69
+ vendor: {
70
+ name: "Asana",
71
+ domain: "asana.com",
72
+ apiDocs: "https://developers.asana.com/reference/rest-api-reference",
73
+ website: "https://asana.com"
74
+ },
75
+ auth: {
76
+ summary: "Authenticates with a personal access token sent as a Bearer credential. The token inherits the permissions of the account that created it.",
77
+ setup: [
78
+ "Open app.asana.com -> Settings -> Apps -> Developer apps.",
79
+ "Under Personal access tokens, create a new token and copy its value.",
80
+ 'Store the token as a secret and reference it from the connector config as `apiToken: secret("ASANA_API_TOKEN")`, alongside the numeric workspaceGid.',
81
+ "Find your workspace GID at https://app.asana.com/api/1.0/workspaces while authenticated."
82
+ ]
83
+ },
84
+ rateLimit: "Asana enforces per-token rate limits (150 req/min on free plans, 1500 on paid); 429 responses with Retry-After are honored.",
85
+ limitations: [
86
+ "Task state-change events are derived from each task story; only system stories (not comments) are written.",
87
+ "A task in multiple projects is stored once, attributed to the first project it is scanned under.",
88
+ "Workspace-wide task search requires a paid plan, so tasks are walked project-by-project; omit projectGids to scan every project."
89
+ ]
90
+ });
91
+ var asanaCredentials = {
92
+ apiToken: {
93
+ description: "Asana personal access token",
94
+ auth: "required"
95
+ }
96
+ };
97
+ var PHASE_ORDER = ["projects", "users", "tasks"];
98
+ var isAsanaSyncCursor = makeChunkedCursorGuard(PHASE_ORDER);
99
+ var gid = z.string().min(1);
100
+ var nextPageSchema = z.object({ offset: z.string() }).nullable().optional();
101
+ var projectsResponseSchema = z.object({
102
+ data: z.array(
103
+ z.object({
104
+ gid,
105
+ name: z.string(),
106
+ archived: z.boolean().nullable().optional(),
107
+ created_at: z.iso.datetime().nullable().optional(),
108
+ modified_at: z.iso.datetime().nullable().optional(),
109
+ owner: z.object({ gid }).nullable().optional(),
110
+ team: z.object({ name: z.string().nullable().optional() }).nullable().optional()
111
+ })
112
+ ),
113
+ next_page: nextPageSchema
114
+ });
115
+ var usersResponseSchema = z.object({
116
+ data: z.array(
117
+ z.object({
118
+ gid,
119
+ name: z.string().nullable().optional(),
120
+ email: z.string().nullable().optional()
121
+ })
122
+ ),
123
+ next_page: nextPageSchema
124
+ });
125
+ var tasksResponseSchema = z.object({
126
+ data: z.array(
127
+ z.object({
128
+ gid,
129
+ name: z.string().nullable().optional(),
130
+ completed: z.boolean().nullable().optional(),
131
+ completed_at: z.iso.datetime().nullable().optional(),
132
+ created_at: z.iso.datetime().nullable().optional(),
133
+ modified_at: z.iso.datetime().nullable().optional(),
134
+ due_on: z.string().nullable().optional(),
135
+ assignee: z.object({ gid }).nullable().optional()
136
+ })
137
+ ),
138
+ next_page: nextPageSchema
139
+ });
140
+ var storiesResponseSchema = z.object({
141
+ data: z.array(
142
+ z.object({
143
+ gid,
144
+ type: z.string().nullable().optional(),
145
+ resource_subtype: z.string().nullable().optional(),
146
+ created_at: z.iso.datetime(),
147
+ created_by: z.object({ gid }).nullable().optional(),
148
+ text: z.string().nullable().optional()
149
+ })
150
+ ),
151
+ next_page: nextPageSchema
152
+ });
153
+ var asanaResources = defineResources({
154
+ asana_project: {
155
+ shape: "entity",
156
+ filterable: [{ field: "archived", ops: ["eq"] }],
157
+ description: "Projects in the workspace with name, archived state, owner, team, and timestamps.",
158
+ endpoint: "GET /projects",
159
+ responses: { projects: projectsResponseSchema }
160
+ },
161
+ asana_user: {
162
+ shape: "entity",
163
+ filterable: [],
164
+ description: "Users in the workspace with display name and email.",
165
+ endpoint: "GET /users",
166
+ responses: { users: usersResponseSchema }
167
+ },
168
+ asana_task: {
169
+ shape: "entity",
170
+ filterable: [
171
+ { field: "completed", ops: ["eq"] },
172
+ { field: "projectGid", ops: ["eq"] },
173
+ { field: "assigneeId", ops: ["eq"] }
174
+ ],
175
+ description: "Tasks with completion state, assignee, due date, owning project, and timestamps.",
176
+ endpoint: "GET /tasks?project={projectGid}",
177
+ notes: "Tasks are walked project-by-project; a task in multiple projects is attributed to the first project scanned.",
178
+ responses: { tasks: tasksResponseSchema }
179
+ },
180
+ asana_task_event: {
181
+ shape: "event",
182
+ filterable: [],
183
+ description: "Task state-change events derived from system stories (completed, assigned, due-date changes, etc.).",
184
+ endpoint: "GET /tasks/{taskGid}/stories",
185
+ notes: "Only system stories are written; comments are skipped. start_ts is the story time, end_ts is null. Timestamps are Unix epoch milliseconds.",
186
+ responses: { stories: storiesResponseSchema }
187
+ }
188
+ });
189
+ var API_BASE = "https://app.asana.com/api/1.0";
190
+ var PROJECTS_PAGE_SIZE = 50;
191
+ var USERS_PAGE_SIZE = 100;
192
+ var TASKS_PAGE_SIZE = 100;
193
+ var STORIES_PAGE_SIZE = 100;
194
+ var PROJECT_FIELDS = "name,archived,created_at,modified_at,owner.gid,team.name";
195
+ var USER_FIELDS = "name,email";
196
+ var TASK_FIELDS = "name,completed,completed_at,created_at,modified_at,due_on,assignee.gid";
197
+ var STORY_FIELDS = "type,resource_subtype,created_at,created_by.gid,text";
198
+ function pushableEq(filter, field) {
199
+ if (!filter) {
200
+ return null;
201
+ }
202
+ for (const clause of filter) {
203
+ if ("field" in clause && clause.field === field && clause.op === "eq" && typeof clause.value === "string") {
204
+ return clause.value;
205
+ }
206
+ }
207
+ return null;
208
+ }
209
+ var id = "asana";
210
+ var AsanaConnector = class _AsanaConnector extends BaseConnector {
211
+ static id = id;
212
+ static resources = asanaResources;
213
+ static schemas = schemasFromResources(asanaResources);
214
+ static create(input, ctx) {
215
+ const parsed = configFields.parse(input);
216
+ return new _AsanaConnector(
217
+ {
218
+ workspaceGid: parsed.workspaceGid,
219
+ projectGids: parsed.projectGids,
220
+ resources: parsed.resources
221
+ },
222
+ { apiToken: parsed.apiToken },
223
+ ctx
224
+ );
225
+ }
226
+ id = id;
227
+ credentials = asanaCredentials;
228
+ buildHeaders() {
229
+ return {
230
+ Authorization: `Bearer ${this.creds.apiToken}`,
231
+ Accept: "application/json",
232
+ "User-Agent": connectorUserAgent("asana")
233
+ };
234
+ }
235
+ fetch(url, resource, signal) {
236
+ return this.get(url, {
237
+ resource,
238
+ headers: this.buildHeaders(),
239
+ signal
240
+ });
241
+ }
242
+ activePhases() {
243
+ return selectActivePhases(
244
+ (r) => {
245
+ switch (r) {
246
+ case "projects":
247
+ return "projects";
248
+ case "users":
249
+ return "users";
250
+ case "tasks":
251
+ case "task_events":
252
+ return "tasks";
253
+ }
254
+ },
255
+ PHASE_ORDER,
256
+ this.settings.resources
257
+ );
258
+ }
259
+ singleSpec(options, resource) {
260
+ const specs = options.fetchSpecs?.[resource];
261
+ return specs && specs.length === 1 ? specs[0] : void 0;
262
+ }
263
+ async fetchCollection(path, params, offset, pageSize, resource, signal) {
264
+ const u = new URL(`${API_BASE}${path}`);
265
+ for (const [k, v] of Object.entries(params)) {
266
+ u.searchParams.set(k, v);
267
+ }
268
+ u.searchParams.set("limit", String(pageSize));
269
+ if (offset !== null) {
270
+ u.searchParams.set("offset", offset);
271
+ }
272
+ const res = await this.fetch(u.toString(), resource, signal);
273
+ return { items: res.body.data, next: res.body.next_page?.offset ?? null };
274
+ }
275
+ fetchProjectsPage(page, signal) {
276
+ return this.fetchCollection(
277
+ "/projects",
278
+ {
279
+ workspace: this.settings.workspaceGid,
280
+ opt_fields: PROJECT_FIELDS,
281
+ archived: "false"
282
+ },
283
+ page,
284
+ PROJECTS_PAGE_SIZE,
285
+ "projects",
286
+ signal
287
+ );
288
+ }
289
+ fetchUsersPage(page, signal) {
290
+ return this.fetchCollection(
291
+ "/users",
292
+ { workspace: this.settings.workspaceGid, opt_fields: USER_FIELDS },
293
+ page,
294
+ USERS_PAGE_SIZE,
295
+ "users",
296
+ signal
297
+ );
298
+ }
299
+ async fetchTasksForProject(projectGid, options, signal) {
300
+ const wantEvents = this.isResourceEnabled("task_events");
301
+ const completed = pushableEq(
302
+ this.singleSpec(options, "asana_task")?.filter,
303
+ "completed"
304
+ );
305
+ const params = {
306
+ project: projectGid,
307
+ opt_fields: TASK_FIELDS
308
+ };
309
+ if (options.mode === "latest" && options.since && !wantEvents) {
310
+ params["modified_since"] = options.since;
311
+ }
312
+ if (completed !== null) {
313
+ params["completed_since"] = completed === "false" ? "now" : "1970-01-01";
314
+ }
315
+ const out = [];
316
+ let offset = null;
317
+ do {
318
+ signal?.throwIfAborted();
319
+ const page = await this.fetchCollection(
320
+ "/tasks",
321
+ params,
322
+ offset,
323
+ TASKS_PAGE_SIZE,
324
+ "tasks",
325
+ signal
326
+ );
327
+ out.push(...page.items);
328
+ offset = page.next;
329
+ } while (offset !== null);
330
+ return out;
331
+ }
332
+ async fetchStoriesForTask(taskGid, signal) {
333
+ const out = [];
334
+ let offset = null;
335
+ do {
336
+ signal?.throwIfAborted();
337
+ const page = await this.fetchCollection(
338
+ `/tasks/${taskGid}/stories`,
339
+ { opt_fields: STORY_FIELDS },
340
+ offset,
341
+ STORIES_PAGE_SIZE,
342
+ "task_events",
343
+ signal
344
+ );
345
+ out.push(...page.items);
346
+ offset = page.next;
347
+ } while (offset !== null);
348
+ return out;
349
+ }
350
+ async fetchTasksPage(page, options, signal) {
351
+ const wantEvents = this.isResourceEnabled("task_events");
352
+ const fixed = this.settings.projectGids;
353
+ let projectGids;
354
+ let next;
355
+ if (fixed && fixed.length > 0) {
356
+ projectGids = [...fixed];
357
+ next = null;
358
+ } else {
359
+ const projectsPage = await this.fetchProjectsPage(page, signal);
360
+ projectGids = projectsPage.items.map((p) => p.gid);
361
+ next = projectsPage.next;
362
+ }
363
+ const seen = /* @__PURE__ */ new Set();
364
+ const items = [];
365
+ for (const projectGid of projectGids) {
366
+ const tasks = await this.fetchTasksForProject(
367
+ projectGid,
368
+ options,
369
+ signal
370
+ );
371
+ for (const task of tasks) {
372
+ if (seen.has(task.gid)) {
373
+ continue;
374
+ }
375
+ seen.add(task.gid);
376
+ const stories = wantEvents ? await this.fetchStoriesForTask(task.gid, signal) : [];
377
+ items.push({ task, projectGid, stories });
378
+ }
379
+ }
380
+ return { items, next };
381
+ }
382
+ async writeProjects(storage, projects) {
383
+ const now = Date.now();
384
+ for (const p of projects) {
385
+ await storage.entity({
386
+ type: "asana_project",
387
+ id: p.gid,
388
+ attributes: {
389
+ name: p.name,
390
+ archived: p.archived ?? false,
391
+ ownerId: p.owner?.gid ?? null,
392
+ teamName: p.team?.name ?? null,
393
+ createdAt: parseEpoch(p.created_at ?? null, "iso")
394
+ },
395
+ updated_at: parseEpoch(p.modified_at ?? null, "iso") ?? now
396
+ });
397
+ }
398
+ }
399
+ async writeUsers(storage, users) {
400
+ const now = Date.now();
401
+ for (const u of users) {
402
+ if (!u.gid) {
403
+ continue;
404
+ }
405
+ await storage.entity({
406
+ type: "asana_user",
407
+ id: u.gid,
408
+ attributes: {
409
+ name: u.name ?? null,
410
+ email: u.email ?? null
411
+ },
412
+ updated_at: now
413
+ });
414
+ }
415
+ }
416
+ async writeTasks(storage, items, sinceMs) {
417
+ const writeEntities = this.isResourceEnabled("tasks");
418
+ const writeEvents = this.isResourceEnabled("task_events");
419
+ const now = Date.now();
420
+ for (const { task, projectGid, stories } of items) {
421
+ const createdMs = parseEpoch(task.created_at ?? null, "iso");
422
+ const modifiedMs = parseEpoch(task.modified_at ?? null, "iso");
423
+ if (writeEntities) {
424
+ await storage.entity({
425
+ type: "asana_task",
426
+ id: task.gid,
427
+ attributes: {
428
+ name: task.name ?? null,
429
+ completed: task.completed ?? false,
430
+ assigneeId: task.assignee?.gid ?? null,
431
+ projectGid,
432
+ dueOn: task.due_on ?? null,
433
+ createdAt: createdMs,
434
+ completedAt: parseEpoch(task.completed_at ?? null, "iso")
435
+ },
436
+ updated_at: modifiedMs ?? now
437
+ });
438
+ }
439
+ if (writeEvents) {
440
+ for (const story of stories) {
441
+ if (story.type !== "system") {
442
+ continue;
443
+ }
444
+ const ts = parseEpoch(story.created_at, "iso");
445
+ if (ts === null) {
446
+ continue;
447
+ }
448
+ if (sinceMs !== null && ts <= sinceMs) {
449
+ continue;
450
+ }
451
+ const attributes = {
452
+ storyGid: story.gid,
453
+ taskGid: task.gid,
454
+ projectGid,
455
+ resourceSubtype: story.resource_subtype ?? null,
456
+ authorId: story.created_by?.gid ?? null,
457
+ text: story.text ?? null
458
+ };
459
+ await storage.event({
460
+ name: "asana_task_event",
461
+ start_ts: ts,
462
+ end_ts: null,
463
+ attributes
464
+ });
465
+ }
466
+ }
467
+ }
468
+ }
469
+ async sync(options, storage, signal) {
470
+ const cursor = isAsanaSyncCursor(options.cursor) ? options.cursor : void 0;
471
+ const isFull = options.mode === "full";
472
+ const sinceMs = options.since ? parseEpoch(options.since, "iso") : null;
473
+ const phases = this.activePhases();
474
+ return paginateChunked({
475
+ phases,
476
+ cursor,
477
+ signal,
478
+ fetchPage: async (phase, page, sig) => {
479
+ switch (phase) {
480
+ case "projects":
481
+ return this.fetchProjectsPage(page, sig);
482
+ case "users":
483
+ return this.fetchUsersPage(page, sig);
484
+ case "tasks":
485
+ return this.fetchTasksPage(page, options, sig);
486
+ }
487
+ },
488
+ writeBatch: async (phase, items, page) => {
489
+ if (isFull && page === null) {
490
+ switch (phase) {
491
+ case "projects":
492
+ await storage.entities([], { types: ["asana_project"] });
493
+ break;
494
+ case "users":
495
+ await storage.entities([], { types: ["asana_user"] });
496
+ break;
497
+ case "tasks":
498
+ if (this.isResourceEnabled("tasks")) {
499
+ await storage.entities([], { types: ["asana_task"] });
500
+ }
501
+ if (this.isResourceEnabled("task_events")) {
502
+ await storage.events([], { names: ["asana_task_event"] });
503
+ }
504
+ break;
505
+ }
506
+ }
507
+ switch (phase) {
508
+ case "projects":
509
+ return this.writeProjects(storage, items);
510
+ case "users":
511
+ return this.writeUsers(storage, items);
512
+ case "tasks":
513
+ return this.writeTasks(
514
+ storage,
515
+ items,
516
+ sinceMs
517
+ );
518
+ }
519
+ }
520
+ });
521
+ }
522
+ };
523
+
524
+ // src/index.ts
525
+ var index_default = AsanaConnector;
526
+ export {
527
+ AsanaConnector,
528
+ configFields,
529
+ index_default as default,
530
+ doc,
531
+ id,
532
+ asanaResources as resources
533
+ };
534
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../connector-shared/src/errors.ts","../../../connector-shared/src/retry.ts","../../../connector-shared/src/version.ts","../../../connector-shared/src/request.ts","../../../connector-shared/src/rate-limit.ts","../../../connector-shared/src/map-concurrent.ts","../../../connector-shared/src/sanitize.ts","../../../connector-shared/src/epoch.ts","../../../connector-shared/src/pagination.ts","../../../connector-shared/src/logger.ts","../src/asana.ts","../src/index.ts"],"sourcesContent":["import type { HttpResponse } from './types';\n\nexport type HttpErrorKind =\n | 'transient'\n | 'rate_limit'\n | 'auth'\n | 'upstream_bug'\n | 'client_bug';\n\nexport abstract class HttpClientError extends Error {\n abstract readonly kind: HttpErrorKind;\n readonly response?: HttpResponse;\n\n constructor(message: string, response?: HttpResponse) {\n super(message);\n this.name = new.target.name;\n this.response = response;\n }\n}\n\nexport class TransientError extends HttpClientError {\n readonly kind = 'transient' as const;\n}\n\nexport class RateLimitError extends HttpClientError {\n readonly kind = 'rate_limit' as const;\n readonly retryAfter?: Date;\n\n constructor(message: string, response?: HttpResponse, retryAfter?: Date) {\n super(message, response);\n this.retryAfter = retryAfter;\n }\n}\n\nexport class AuthError extends HttpClientError {\n readonly kind = 'auth' as const;\n}\n\nexport class UpstreamBugError extends HttpClientError {\n readonly kind = 'upstream_bug' as const;\n}\n\nexport class ClientBugError extends HttpClientError {\n readonly kind = 'client_bug' as const;\n}\n\nexport function classifyStatus(status: number): HttpErrorKind {\n if (status === 429) {\n return 'rate_limit';\n }\n if (status === 401 || status === 403) {\n return 'auth';\n }\n if (status === 408) {\n return 'transient';\n }\n if (status >= 500) {\n return 'upstream_bug';\n }\n if (status >= 400) {\n return 'client_bug';\n }\n return 'client_bug';\n}\n\nexport function errorForStatus(\n message: string,\n response: HttpResponse,\n retryAfter?: Date,\n): HttpClientError {\n const kind = classifyStatus(response.status);\n switch (kind) {\n case 'rate_limit':\n return new RateLimitError(message, response, retryAfter);\n case 'auth':\n return new AuthError(message, response);\n case 'transient':\n return new TransientError(message, response);\n case 'upstream_bug':\n return new UpstreamBugError(message, response);\n case 'client_bug':\n return new ClientBugError(message, response);\n }\n}\n","import { HttpClientError, RateLimitError, TransientError } from './errors';\n\nexport interface RetryPolicy {\n maxAttempts?: number;\n initialDelayMs?: number;\n maxDelayMs?: number;\n retryOn?: (status: number | null, err?: Error) => boolean;\n}\n\nexport const defaultRetryOn = (status: number | null, err?: Error): boolean => {\n if (err instanceof RateLimitError) {\n return true;\n }\n if (err instanceof TransientError) {\n return true;\n }\n if (status === null) {\n return err instanceof Error && !(err instanceof HttpClientError);\n }\n if (status === 408 || status === 429) {\n return true;\n }\n if (status >= 500) {\n return true;\n }\n return false;\n};\n\nexport function backoffDelayMs(\n attempt: number,\n policy: Required<Pick<RetryPolicy, 'initialDelayMs' | 'maxDelayMs'>>,\n): number {\n const base = policy.initialDelayMs * 2 ** attempt;\n const jitter = base * 0.25 * Math.random();\n return Math.min(base + jitter, policy.maxDelayMs);\n}\n\nexport function parseRetryAfter(\n headerValue: string | null,\n now: Date = new Date(),\n): Date | undefined {\n if (!headerValue) {\n return undefined;\n }\n const trimmed = headerValue.trim();\n if (/^\\d+$/.test(trimmed)) {\n return new Date(now.getTime() + Number(trimmed) * 1000);\n }\n const parsed = Date.parse(trimmed);\n if (Number.isNaN(parsed)) {\n return undefined;\n }\n return new Date(parsed);\n}\n\nexport function sleep(ms: number, signal?: AbortSignal): Promise<void> {\n if (signal?.aborted) {\n return Promise.reject(signal.reason ?? new Error('Aborted'));\n }\n return new Promise<void>((resolve, reject) => {\n const onAbort = () => {\n clearTimeout(timer);\n reject(signal!.reason ?? new Error('Aborted'));\n };\n const timer = setTimeout(() => {\n signal?.removeEventListener('abort', onAbort);\n resolve();\n }, ms);\n signal?.addEventListener('abort', onAbort, { once: true });\n });\n}\n","export const HTTP_CLIENT_VERSION = '0.0.0';\n\nexport const DEFAULT_USER_AGENT = `rawdash-connector/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;\n\nexport function connectorUserAgent(connectorId: string): string {\n return `rawdash-connector-${connectorId}/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;\n}\n","import {\n AuthError,\n ClientBugError,\n HttpClientError,\n RateLimitError,\n TransientError,\n UpstreamBugError,\n errorForStatus,\n} from './errors';\nimport { defaultRetryOn, parseRetryAfter, sleep } from './retry';\nimport type { FetchLike, HttpMethod, HttpRequest, HttpResponse } from './types';\nimport { DEFAULT_USER_AGENT } from './version';\n\nconst DEFAULT_TIMEOUT_MS = 10_000;\nconst DEFAULT_MAX_ATTEMPTS = 3;\nconst DEFAULT_INITIAL_DELAY_MS = 1000;\nconst DEFAULT_MAX_DELAY_MS = 60_000;\nconst OBSERVER_TIMEOUT_MS = 250;\n\nexport interface RequestObservation {\n url: string;\n method: HttpMethod;\n status: number;\n resource: string;\n requestId: string;\n body: unknown;\n}\n\nexport type RequestObserver = (\n event: RequestObservation,\n) => void | Promise<void>;\n\nexport interface RequestOptions {\n fetch?: FetchLike;\n observer?: RequestObserver;\n resource: string;\n requestId?: string;\n}\n\nasync function notifyObserver(\n observer: RequestObserver,\n event: RequestObservation,\n): Promise<void> {\n let result: void | Promise<void>;\n try {\n result = observer(event);\n } catch (err) {\n console.warn('[connector-shared] request observer threw:', err);\n return;\n }\n if (!(result instanceof Promise)) {\n return;\n }\n const guarded = result.catch((err) => {\n console.warn('[connector-shared] request observer rejected:', err);\n });\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timeout = new Promise<void>((resolve) => {\n timer = setTimeout(resolve, OBSERVER_TIMEOUT_MS);\n });\n try {\n await Promise.race([guarded, timeout]);\n } finally {\n if (timer) {\n clearTimeout(timer);\n }\n }\n}\n\nfunction newRequestId(): string {\n const c = (globalThis as { crypto?: { randomUUID?: () => string } }).crypto;\n if (c?.randomUUID) {\n return c.randomUUID();\n }\n return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;\n}\n\nfunction mergeHeaders(\n defaults: Record<string, string>,\n overrides: Record<string, string> | undefined,\n): Record<string, string> {\n const merged: Record<string, string> = {};\n for (const [k, v] of Object.entries(defaults)) {\n merged[k.toLowerCase()] = v;\n }\n if (overrides) {\n for (const [k, v] of Object.entries(overrides)) {\n merged[k.toLowerCase()] = v;\n }\n }\n return merged;\n}\n\nfunction linkTimeoutSignal(\n parent: AbortSignal | undefined,\n timeoutMs: number,\n): { signal: AbortSignal; cancel: () => void } {\n const controller = new AbortController();\n const onParentAbort = () => {\n controller.abort(parent?.reason);\n };\n if (parent) {\n if (parent.aborted) {\n controller.abort(parent.reason);\n } else {\n parent.addEventListener('abort', onParentAbort, { once: true });\n }\n }\n const timer = setTimeout(() => {\n controller.abort(new Error(`Request timed out after ${timeoutMs}ms`));\n }, timeoutMs);\n return {\n signal: controller.signal,\n cancel: () => {\n clearTimeout(timer);\n if (parent) {\n parent.removeEventListener('abort', onParentAbort);\n }\n },\n };\n}\n\nasync function readBody(res: Response, parseJson: boolean): Promise<unknown> {\n if (res.status === 204 || res.status === 205) {\n return null;\n }\n const contentType = res.headers.get('content-type') ?? '';\n if (parseJson && contentType.includes('application/json')) {\n const text = await res.text();\n if (text.length === 0) {\n return null;\n }\n return JSON.parse(text);\n }\n return res.text();\n}\n\nexport async function request<T = unknown>(\n req: HttpRequest,\n options: RequestOptions,\n): Promise<HttpResponse<T>> {\n const fetchImpl: FetchLike = options.fetch ?? (globalThis.fetch as FetchLike);\n const retry = req.retry ?? {};\n const maxAttempts = retry.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;\n const initialDelayMs = retry.initialDelayMs ?? DEFAULT_INITIAL_DELAY_MS;\n const maxDelayMs = retry.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;\n const retryOn = retry.retryOn ?? defaultRetryOn;\n const timeoutMs = req.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const parseJson = req.parseJson ?? true;\n\n const headers = mergeHeaders(\n {\n 'User-Agent': DEFAULT_USER_AGENT,\n Accept: 'application/json',\n },\n req.headers,\n );\n\n let lastErr: Error | undefined;\n\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n req.signal?.throwIfAborted();\n\n const { signal, cancel } = linkTimeoutSignal(req.signal, timeoutMs);\n let res: Response;\n try {\n res = await fetchImpl(req.url, {\n method: req.method ?? 'GET',\n headers,\n body: req.body as RequestInit['body'],\n signal,\n });\n } catch (err) {\n cancel();\n if (req.signal?.aborted) {\n throw req.signal.reason ?? err;\n }\n const error = err instanceof Error ? err : new Error(String(err));\n lastErr = error;\n if (attempt < maxAttempts - 1 && retryOn(null, error)) {\n const delay = computeDelay(attempt, initialDelayMs, maxDelayMs);\n await sleep(delay, req.signal);\n continue;\n }\n throw new TransientError(error.message);\n }\n cancel();\n\n const body = await readBody(res, parseJson);\n const httpResponse: HttpResponse<T> = {\n status: res.status,\n headers: res.headers,\n body: body as T,\n };\n if (req.rateLimit) {\n const state = req.rateLimit.parse(res.headers);\n if (state) {\n httpResponse.rateLimitState = state;\n }\n }\n\n if (options.observer) {\n await notifyObserver(options.observer, {\n url: req.url,\n method: req.method ?? 'GET',\n status: res.status,\n resource: options.resource,\n requestId: options.requestId ?? newRequestId(),\n body,\n });\n }\n\n if (res.ok) {\n return httpResponse;\n }\n\n const retryAfter = parseRetryAfter(res.headers.get('retry-after'));\n const message = `HTTP ${res.status} ${res.statusText} for ${req.method ?? 'GET'} ${req.url}`;\n const err = errorForStatus(message, httpResponse, retryAfter);\n\n if (\n attempt < maxAttempts - 1 &&\n retryOn(res.status, err) &&\n !(err instanceof AuthError) &&\n !(err instanceof ClientBugError)\n ) {\n lastErr = err;\n let delay = computeDelay(attempt, initialDelayMs, maxDelayMs);\n if (err instanceof RateLimitError && retryAfter) {\n const wait = retryAfter.getTime() - Date.now();\n if (wait > 0) {\n delay = Math.min(wait, maxDelayMs);\n }\n }\n await sleep(delay, req.signal);\n continue;\n }\n\n throw err;\n }\n\n throw lastErr ?? new UpstreamBugError('Exhausted retry attempts');\n}\n\nfunction computeDelay(\n attempt: number,\n initialDelayMs: number,\n maxDelayMs: number,\n): number {\n const base = initialDelayMs * 2 ** attempt;\n const jitter = base * 0.25 * Math.random();\n return Math.min(base + jitter, maxDelayMs);\n}\n\nexport { HttpClientError };\n","export interface RateLimitState {\n remaining: number;\n resetAt: Date;\n}\n\nexport interface RateLimitPolicy {\n parse(headers: Headers): RateLimitState | null;\n}\n\nexport interface StandardRateLimitPolicyConfig {\n remainingHeader: string;\n resetHeader: string;\n resetUnit: 's' | 'ms';\n resetFallbackMs?: number;\n}\n\nexport function standardRateLimitPolicy(\n config: StandardRateLimitPolicyConfig,\n): RateLimitPolicy {\n const { remainingHeader, resetHeader, resetUnit, resetFallbackMs } = config;\n const multiplier = resetUnit === 's' ? 1000 : 1;\n return {\n parse(h) {\n const remainingRaw = h.get(remainingHeader);\n if (remainingRaw === null || remainingRaw.trim() === '') {\n return null;\n }\n const remaining = Number(remainingRaw);\n if (!Number.isFinite(remaining)) {\n return null;\n }\n const resetRaw = h.get(resetHeader);\n if (resetRaw === null) {\n if (resetFallbackMs === undefined) {\n return null;\n }\n return {\n remaining,\n resetAt: new Date(Date.now() + resetFallbackMs),\n };\n }\n if (resetRaw.trim() === '') {\n return null;\n }\n const reset = Number(resetRaw);\n if (!Number.isFinite(reset) || reset < 0) {\n return null;\n }\n const resetMs = reset * multiplier;\n if (!Number.isFinite(resetMs)) {\n return null;\n }\n return { remaining, resetAt: new Date(resetMs) };\n },\n };\n}\n","export async function mapWithConcurrency<T, R>(\n items: readonly T[],\n concurrency: number,\n fn: (item: T, index: number) => Promise<R>,\n): Promise<R[]> {\n const results = new Array<R>(items.length);\n if (items.length === 0) {\n return results;\n }\n const normalized = Number.isFinite(concurrency) ? Math.floor(concurrency) : 1;\n const limit = Math.max(1, Math.min(normalized, items.length));\n let next = 0;\n let failed = false;\n\n async function worker(): Promise<void> {\n while (!failed) {\n const i = next++;\n if (i >= items.length) {\n return;\n }\n try {\n results[i] = await fn(items[i]!, i);\n } catch (err) {\n failed = true;\n throw err;\n }\n }\n }\n\n const workers: Promise<void>[] = [];\n for (let w = 0; w < limit; w++) {\n workers.push(worker());\n }\n await Promise.all(workers);\n return results;\n}\n","export interface SanitizeAllowedUrlOptions {\n url: string | null;\n host: string;\n pathname: string;\n protocol?: 'https:' | 'http:';\n}\n\nexport function sanitizeAllowedUrl(\n options: SanitizeAllowedUrlOptions,\n): string | null {\n const { url, host, pathname, protocol = 'https:' } = options;\n if (url === null) {\n return null;\n }\n try {\n const u = new URL(url);\n if (u.protocol !== protocol || u.host !== host || u.pathname !== pathname) {\n return null;\n }\n return u.toString();\n } catch {\n return null;\n }\n}\n","export type EpochUnit = 'ms' | 's' | 'iso';\n\nexport function parseEpoch(\n value: number | string | null | undefined,\n unit: EpochUnit,\n): number | null {\n if (value === null || value === undefined) {\n return null;\n }\n if (unit === 'iso') {\n if (typeof value !== 'string') {\n return null;\n }\n const ms = new Date(value).getTime();\n return Number.isFinite(ms) ? ms : null;\n }\n if (typeof value === 'string' && value.trim() === '') {\n return null;\n }\n const n = typeof value === 'number' ? value : Number(value);\n if (!Number.isFinite(n)) {\n return null;\n }\n const result = unit === 's' ? n * 1000 : n;\n return Number.isFinite(result) ? result : null;\n}\n","import { request } from './request';\nimport type { HttpRequest } from './types';\n\nexport function parseLinkHeader(header: string | null): Record<string, string> {\n if (!header) {\n return {};\n }\n const result: Record<string, string> = {};\n for (const part of header.split(',')) {\n const match = part.match(/<([^>]+)>\\s*;\\s*rel=\"([^\"]+)\"/);\n if (match) {\n result[match[2]!] = match[1]!;\n }\n }\n return result;\n}\n\nexport async function* paginateLink<T>(\n initial: HttpRequest,\n parse: (body: unknown) => T[],\n options: { resource: string },\n): AsyncIterable<T> {\n let next: string | null = initial.url;\n while (next) {\n const res: Awaited<ReturnType<typeof request>> = await request(\n {\n ...initial,\n url: next,\n },\n { resource: options.resource },\n );\n for (const item of parse(res.body)) {\n yield item;\n }\n const links = parseLinkHeader(res.headers.get('link'));\n next = links['next'] ?? null;\n }\n}\n\nexport async function* paginateCursor<T>(\n initial: HttpRequest,\n parse: (body: unknown) => { items: T[]; nextCursor: string | null },\n buildNext: (req: HttpRequest, cursor: string) => HttpRequest,\n options: { resource: string },\n): AsyncIterable<T> {\n let req: HttpRequest = initial;\n while (true) {\n const res = await request(req, { resource: options.resource });\n const { items, nextCursor } = parse(res.body);\n for (const item of items) {\n yield item;\n }\n if (!nextCursor) {\n return;\n }\n req = buildNext(req, nextCursor);\n }\n}\n\nexport async function* paginatePage<T>(\n initial: HttpRequest,\n parse: (body: unknown) => { items: T[]; hasMore: boolean },\n buildPage: (req: HttpRequest, page: number) => HttpRequest,\n options: { resource: string },\n): AsyncIterable<T> {\n let page = 1;\n while (true) {\n const req = page === 1 ? initial : buildPage(initial, page);\n const res = await request(req, { resource: options.resource });\n const { items, hasMore } = parse(res.body);\n for (const item of items) {\n yield item;\n }\n if (!hasMore || items.length === 0) {\n return;\n }\n page++;\n }\n}\n","export type LogFields = Record<string, unknown>;\n\nexport interface ConnectorLogger {\n info(event: string, fields?: LogFields): void;\n warn(event: string, fields?: LogFields): void;\n}\n\nexport interface ConnectorLoggerOptions {\n scope: string;\n}\n\nconst MAX_VALUE_LEN = 120;\n\nfunction truncate(s: string, max = MAX_VALUE_LEN): string {\n if (s.length <= max) {\n return s;\n }\n return `${s.slice(0, max - 1)}…`;\n}\n\nfunction formatValue(value: unknown): string {\n if (value === null) {\n return 'null';\n }\n if (value === undefined) {\n return '';\n }\n if (typeof value === 'number' || typeof value === 'boolean') {\n return String(value);\n }\n if (typeof value === 'string') {\n const t = truncate(value);\n if (/[\\s\"=]/.test(t)) {\n return JSON.stringify(t);\n }\n return t;\n }\n if (typeof value === 'bigint') {\n return value.toString();\n }\n let json: string | undefined;\n try {\n json = JSON.stringify(value);\n } catch {\n json = undefined;\n }\n return truncate(json ?? String(value));\n}\n\nexport function formatLogFields(fields?: LogFields): string {\n if (!fields) {\n return '';\n }\n const parts: string[] = [];\n for (const [k, v] of Object.entries(fields)) {\n if (v === undefined) {\n continue;\n }\n parts.push(`${k}=${formatValue(v)}`);\n }\n return parts.length > 0 ? ` ${parts.join(' ')}` : '';\n}\n\nexport function formatLogLine(\n scope: string,\n event: string,\n fields?: LogFields,\n): string {\n return `[${scope}] ${event}${formatLogFields(fields)}`;\n}\n\nexport function createDefaultConnectorLogger(\n opts: ConnectorLoggerOptions,\n): ConnectorLogger {\n return {\n info(event, fields) {\n console.info(formatLogLine(opts.scope, event, fields));\n },\n warn(event, fields) {\n console.warn(formatLogLine(opts.scope, event, fields));\n },\n };\n}\n\nconst NOOP_LOGGER: ConnectorLogger = {\n info() {},\n warn() {},\n};\n\nexport function noopConnectorLogger(): ConnectorLogger {\n return NOOP_LOGGER;\n}\n","import {\n type HttpResponse,\n connectorUserAgent,\n parseEpoch,\n} from '@rawdash/connector-shared';\nimport {\n BaseConnector,\n type ConnectorContext,\n type ConnectorDoc,\n type CredentialsSchema,\n type FetchSpec,\n type FilterClause,\n type JSONValue,\n type StorageHandle,\n type SyncOptions,\n type SyncResult,\n defineConfigFields,\n defineConnectorDoc,\n defineResources,\n makeChunkedCursorGuard,\n paginateChunked,\n schemasFromResources,\n selectActivePhases,\n} from '@rawdash/core';\nimport { z } from 'zod';\n\nexport const configFields = defineConfigFields(\n z.object({\n apiToken: z.object({ $secret: z.string() }).meta({\n label: 'Personal access token',\n description:\n 'Asana personal access token. Create one at app.asana.com → Settings → Apps → Developer apps → Personal access tokens.',\n placeholder: '2/1201234567890/...',\n secret: true,\n }),\n workspaceGid: z\n .string()\n .min(1)\n .regex(/^\\d+$/, 'Workspace GID is the numeric id of the workspace.')\n .meta({\n label: 'Workspace GID',\n description:\n 'Numeric GID of the workspace to sync. Find it at app.asana.com/api/1.0/workspaces.',\n placeholder: '1201234567890',\n }),\n projectGids: z.array(z.string().regex(/^\\d+$/)).nonempty().optional().meta({\n label: 'Project GIDs (optional)',\n description:\n 'Restrict the task sync to specific project GIDs. Omit to sync tasks from every project in the workspace.',\n }),\n resources: z\n .array(z.enum(['projects', 'users', 'tasks', 'task_events']))\n .nonempty()\n .optional()\n .meta({\n label: 'Resources',\n description:\n \"Which Asana resources to sync. Omit to sync all of them. 'task_events' shares the tasks scan - enabling it without 'tasks' still walks tasks (and fetches their stories) but skips writing task entities.\",\n }),\n }),\n);\n\nexport const doc: ConnectorDoc = defineConnectorDoc({\n displayName: 'Asana',\n category: 'product',\n brandColor: '#F06A6A',\n tagline:\n 'Sync projects, users, tasks, and task state-change events from an Asana workspace.',\n vendor: {\n name: 'Asana',\n domain: 'asana.com',\n apiDocs: 'https://developers.asana.com/reference/rest-api-reference',\n website: 'https://asana.com',\n },\n auth: {\n summary:\n 'Authenticates with a personal access token sent as a Bearer credential. The token inherits the permissions of the account that created it.',\n setup: [\n 'Open app.asana.com -> Settings -> Apps -> Developer apps.',\n 'Under Personal access tokens, create a new token and copy its value.',\n 'Store the token as a secret and reference it from the connector config as `apiToken: secret(\"ASANA_API_TOKEN\")`, alongside the numeric workspaceGid.',\n 'Find your workspace GID at https://app.asana.com/api/1.0/workspaces while authenticated.',\n ],\n },\n rateLimit:\n 'Asana enforces per-token rate limits (150 req/min on free plans, 1500 on paid); 429 responses with Retry-After are honored.',\n limitations: [\n 'Task state-change events are derived from each task story; only system stories (not comments) are written.',\n 'A task in multiple projects is stored once, attributed to the first project it is scanned under.',\n 'Workspace-wide task search requires a paid plan, so tasks are walked project-by-project; omit projectGids to scan every project.',\n ],\n});\n\nexport type AsanaResource = 'projects' | 'users' | 'tasks' | 'task_events';\n\nexport interface AsanaSettings {\n workspaceGid: string;\n projectGids?: readonly string[];\n resources?: readonly AsanaResource[];\n}\n\nconst asanaCredentials = {\n apiToken: {\n description: 'Asana personal access token',\n auth: 'required' as const,\n },\n} satisfies CredentialsSchema;\n\ntype AsanaCredentials = typeof asanaCredentials;\n\nconst PHASE_ORDER = ['projects', 'users', 'tasks'] as const;\n\ntype AsanaPhase = (typeof PHASE_ORDER)[number];\n\nconst isAsanaSyncCursor = makeChunkedCursorGuard(PHASE_ORDER);\n\ninterface AsanaGidRef {\n gid: string;\n}\n\ninterface AsanaProject {\n gid: string;\n name: string;\n archived?: boolean | null;\n created_at?: string | null;\n modified_at?: string | null;\n owner?: AsanaGidRef | null;\n team?: { name?: string | null } | null;\n}\n\ninterface AsanaUser {\n gid: string;\n name?: string | null;\n email?: string | null;\n}\n\ninterface AsanaTask {\n gid: string;\n name?: string | null;\n completed?: boolean | null;\n completed_at?: string | null;\n created_at?: string | null;\n modified_at?: string | null;\n due_on?: string | null;\n assignee?: AsanaGidRef | null;\n}\n\ninterface AsanaStory {\n gid: string;\n type?: string | null;\n resource_subtype?: string | null;\n created_at: string;\n created_by?: AsanaGidRef | null;\n text?: string | null;\n}\n\ninterface AsanaTaskWithContext {\n task: AsanaTask;\n projectGid: string;\n stories: AsanaStory[];\n}\n\ninterface AsanaPage<T> {\n data: T[];\n next_page?: { offset: string } | null;\n}\n\nconst gid = z.string().min(1);\n\nconst nextPageSchema = z.object({ offset: z.string() }).nullable().optional();\n\nconst projectsResponseSchema = z.object({\n data: z.array(\n z.object({\n gid,\n name: z.string(),\n archived: z.boolean().nullable().optional(),\n created_at: z.iso.datetime().nullable().optional(),\n modified_at: z.iso.datetime().nullable().optional(),\n owner: z.object({ gid }).nullable().optional(),\n team: z\n .object({ name: z.string().nullable().optional() })\n .nullable()\n .optional(),\n }),\n ),\n next_page: nextPageSchema,\n});\n\nconst usersResponseSchema = z.object({\n data: z.array(\n z.object({\n gid,\n name: z.string().nullable().optional(),\n email: z.string().nullable().optional(),\n }),\n ),\n next_page: nextPageSchema,\n});\n\nconst tasksResponseSchema = z.object({\n data: z.array(\n z.object({\n gid,\n name: z.string().nullable().optional(),\n completed: z.boolean().nullable().optional(),\n completed_at: z.iso.datetime().nullable().optional(),\n created_at: z.iso.datetime().nullable().optional(),\n modified_at: z.iso.datetime().nullable().optional(),\n due_on: z.string().nullable().optional(),\n assignee: z.object({ gid }).nullable().optional(),\n }),\n ),\n next_page: nextPageSchema,\n});\n\nconst storiesResponseSchema = z.object({\n data: z.array(\n z.object({\n gid,\n type: z.string().nullable().optional(),\n resource_subtype: z.string().nullable().optional(),\n created_at: z.iso.datetime(),\n created_by: z.object({ gid }).nullable().optional(),\n text: z.string().nullable().optional(),\n }),\n ),\n next_page: nextPageSchema,\n});\n\nexport const asanaResources = defineResources({\n asana_project: {\n shape: 'entity',\n filterable: [{ field: 'archived', ops: ['eq'] }],\n description:\n 'Projects in the workspace with name, archived state, owner, team, and timestamps.',\n endpoint: 'GET /projects',\n responses: { projects: projectsResponseSchema },\n },\n asana_user: {\n shape: 'entity',\n filterable: [],\n description: 'Users in the workspace with display name and email.',\n endpoint: 'GET /users',\n responses: { users: usersResponseSchema },\n },\n asana_task: {\n shape: 'entity',\n filterable: [\n { field: 'completed', ops: ['eq'] },\n { field: 'projectGid', ops: ['eq'] },\n { field: 'assigneeId', ops: ['eq'] },\n ],\n description:\n 'Tasks with completion state, assignee, due date, owning project, and timestamps.',\n endpoint: 'GET /tasks?project={projectGid}',\n notes:\n 'Tasks are walked project-by-project; a task in multiple projects is attributed to the first project scanned.',\n responses: { tasks: tasksResponseSchema },\n },\n asana_task_event: {\n shape: 'event',\n filterable: [],\n description:\n 'Task state-change events derived from system stories (completed, assigned, due-date changes, etc.).',\n endpoint: 'GET /tasks/{taskGid}/stories',\n notes:\n 'Only system stories are written; comments are skipped. start_ts is the story time, end_ts is null. Timestamps are Unix epoch milliseconds.',\n responses: { stories: storiesResponseSchema },\n },\n});\n\nconst API_BASE = 'https://app.asana.com/api/1.0';\nconst PROJECTS_PAGE_SIZE = 50;\nconst USERS_PAGE_SIZE = 100;\nconst TASKS_PAGE_SIZE = 100;\nconst STORIES_PAGE_SIZE = 100;\n\nconst PROJECT_FIELDS =\n 'name,archived,created_at,modified_at,owner.gid,team.name';\nconst USER_FIELDS = 'name,email';\nconst TASK_FIELDS =\n 'name,completed,completed_at,created_at,modified_at,due_on,assignee.gid';\nconst STORY_FIELDS = 'type,resource_subtype,created_at,created_by.gid,text';\n\nfunction pushableEq(\n filter: FilterClause[] | undefined,\n field: string,\n): string | null {\n if (!filter) {\n return null;\n }\n for (const clause of filter) {\n if (\n 'field' in clause &&\n clause.field === field &&\n clause.op === 'eq' &&\n typeof clause.value === 'string'\n ) {\n return clause.value;\n }\n }\n return null;\n}\n\nexport const id = 'asana';\n\nexport class AsanaConnector extends BaseConnector<\n AsanaSettings,\n AsanaCredentials\n> {\n static readonly id = id;\n\n static readonly resources = asanaResources;\n\n static readonly schemas = schemasFromResources(asanaResources);\n\n static create(input: unknown, ctx?: ConnectorContext): AsanaConnector {\n const parsed = configFields.parse(input);\n return new AsanaConnector(\n {\n workspaceGid: parsed.workspaceGid,\n projectGids: parsed.projectGids,\n resources: parsed.resources,\n },\n { apiToken: parsed.apiToken },\n ctx,\n );\n }\n\n readonly id = id;\n override readonly credentials = asanaCredentials;\n\n private buildHeaders(): Record<string, string> {\n return {\n Authorization: `Bearer ${this.creds.apiToken}`,\n Accept: 'application/json',\n 'User-Agent': connectorUserAgent('asana'),\n };\n }\n\n private fetch<T>(\n url: string,\n resource: string,\n signal?: AbortSignal,\n ): Promise<HttpResponse<T>> {\n return this.get<T>(url, {\n resource,\n headers: this.buildHeaders(),\n signal,\n });\n }\n\n private activePhases(): AsanaPhase[] {\n return selectActivePhases<AsanaResource, AsanaPhase>(\n (r) => {\n switch (r) {\n case 'projects':\n return 'projects';\n case 'users':\n return 'users';\n case 'tasks':\n case 'task_events':\n return 'tasks';\n }\n },\n PHASE_ORDER,\n this.settings.resources,\n );\n }\n\n private singleSpec(\n options: SyncOptions,\n resource: string,\n ): FetchSpec | undefined {\n const specs = options.fetchSpecs?.[resource];\n return specs && specs.length === 1 ? specs[0] : undefined;\n }\n\n private async fetchCollection<T>(\n path: string,\n params: Record<string, string>,\n offset: string | null,\n pageSize: number,\n resource: string,\n signal: AbortSignal | undefined,\n ): Promise<{ items: T[]; next: string | null }> {\n const u = new URL(`${API_BASE}${path}`);\n for (const [k, v] of Object.entries(params)) {\n u.searchParams.set(k, v);\n }\n u.searchParams.set('limit', String(pageSize));\n if (offset !== null) {\n u.searchParams.set('offset', offset);\n }\n const res = await this.fetch<AsanaPage<T>>(u.toString(), resource, signal);\n return { items: res.body.data, next: res.body.next_page?.offset ?? null };\n }\n\n private fetchProjectsPage(\n page: string | null,\n signal: AbortSignal | undefined,\n ): Promise<{ items: AsanaProject[]; next: string | null }> {\n return this.fetchCollection<AsanaProject>(\n '/projects',\n {\n workspace: this.settings.workspaceGid,\n opt_fields: PROJECT_FIELDS,\n archived: 'false',\n },\n page,\n PROJECTS_PAGE_SIZE,\n 'projects',\n signal,\n );\n }\n\n private fetchUsersPage(\n page: string | null,\n signal: AbortSignal | undefined,\n ): Promise<{ items: AsanaUser[]; next: string | null }> {\n return this.fetchCollection<AsanaUser>(\n '/users',\n { workspace: this.settings.workspaceGid, opt_fields: USER_FIELDS },\n page,\n USERS_PAGE_SIZE,\n 'users',\n signal,\n );\n }\n\n private async fetchTasksForProject(\n projectGid: string,\n options: SyncOptions,\n signal: AbortSignal | undefined,\n ): Promise<AsanaTask[]> {\n const wantEvents = this.isResourceEnabled('task_events');\n const completed = pushableEq(\n this.singleSpec(options, 'asana_task')?.filter,\n 'completed',\n );\n const params: Record<string, string> = {\n project: projectGid,\n opt_fields: TASK_FIELDS,\n };\n if (options.mode === 'latest' && options.since && !wantEvents) {\n params['modified_since'] = options.since;\n }\n if (completed !== null) {\n params['completed_since'] = completed === 'false' ? 'now' : '1970-01-01';\n }\n const out: AsanaTask[] = [];\n let offset: string | null = null;\n do {\n signal?.throwIfAborted();\n const page: { items: AsanaTask[]; next: string | null } =\n await this.fetchCollection<AsanaTask>(\n '/tasks',\n params,\n offset,\n TASKS_PAGE_SIZE,\n 'tasks',\n signal,\n );\n out.push(...page.items);\n offset = page.next;\n } while (offset !== null);\n return out;\n }\n\n private async fetchStoriesForTask(\n taskGid: string,\n signal: AbortSignal | undefined,\n ): Promise<AsanaStory[]> {\n const out: AsanaStory[] = [];\n let offset: string | null = null;\n do {\n signal?.throwIfAborted();\n const page: { items: AsanaStory[]; next: string | null } =\n await this.fetchCollection<AsanaStory>(\n `/tasks/${taskGid}/stories`,\n { opt_fields: STORY_FIELDS },\n offset,\n STORIES_PAGE_SIZE,\n 'task_events',\n signal,\n );\n out.push(...page.items);\n offset = page.next;\n } while (offset !== null);\n return out;\n }\n\n private async fetchTasksPage(\n page: string | null,\n options: SyncOptions,\n signal: AbortSignal | undefined,\n ): Promise<{ items: AsanaTaskWithContext[]; next: string | null }> {\n const wantEvents = this.isResourceEnabled('task_events');\n const fixed = this.settings.projectGids;\n\n let projectGids: string[];\n let next: string | null;\n if (fixed && fixed.length > 0) {\n projectGids = [...fixed];\n next = null;\n } else {\n const projectsPage = await this.fetchProjectsPage(page, signal);\n projectGids = projectsPage.items.map((p) => p.gid);\n next = projectsPage.next;\n }\n\n const seen = new Set<string>();\n const items: AsanaTaskWithContext[] = [];\n for (const projectGid of projectGids) {\n const tasks = await this.fetchTasksForProject(\n projectGid,\n options,\n signal,\n );\n for (const task of tasks) {\n if (seen.has(task.gid)) {\n continue;\n }\n seen.add(task.gid);\n const stories = wantEvents\n ? await this.fetchStoriesForTask(task.gid, signal)\n : [];\n items.push({ task, projectGid, stories });\n }\n }\n return { items, next };\n }\n\n private async writeProjects(\n storage: StorageHandle,\n projects: AsanaProject[],\n ): Promise<void> {\n const now = Date.now();\n for (const p of projects) {\n await storage.entity({\n type: 'asana_project',\n id: p.gid,\n attributes: {\n name: p.name,\n archived: p.archived ?? false,\n ownerId: p.owner?.gid ?? null,\n teamName: p.team?.name ?? null,\n createdAt: parseEpoch(p.created_at ?? null, 'iso'),\n },\n updated_at: parseEpoch(p.modified_at ?? null, 'iso') ?? now,\n });\n }\n }\n\n private async writeUsers(\n storage: StorageHandle,\n users: AsanaUser[],\n ): Promise<void> {\n const now = Date.now();\n for (const u of users) {\n if (!u.gid) {\n continue;\n }\n await storage.entity({\n type: 'asana_user',\n id: u.gid,\n attributes: {\n name: u.name ?? null,\n email: u.email ?? null,\n },\n updated_at: now,\n });\n }\n }\n\n private async writeTasks(\n storage: StorageHandle,\n items: AsanaTaskWithContext[],\n sinceMs: number | null,\n ): Promise<void> {\n const writeEntities = this.isResourceEnabled('tasks');\n const writeEvents = this.isResourceEnabled('task_events');\n const now = Date.now();\n\n for (const { task, projectGid, stories } of items) {\n const createdMs = parseEpoch(task.created_at ?? null, 'iso');\n const modifiedMs = parseEpoch(task.modified_at ?? null, 'iso');\n\n if (writeEntities) {\n await storage.entity({\n type: 'asana_task',\n id: task.gid,\n attributes: {\n name: task.name ?? null,\n completed: task.completed ?? false,\n assigneeId: task.assignee?.gid ?? null,\n projectGid,\n dueOn: task.due_on ?? null,\n createdAt: createdMs,\n completedAt: parseEpoch(task.completed_at ?? null, 'iso'),\n },\n updated_at: modifiedMs ?? now,\n });\n }\n\n if (writeEvents) {\n for (const story of stories) {\n if (story.type !== 'system') {\n continue;\n }\n const ts = parseEpoch(story.created_at, 'iso');\n if (ts === null) {\n continue;\n }\n if (sinceMs !== null && ts <= sinceMs) {\n continue;\n }\n const attributes: Record<string, JSONValue> = {\n storyGid: story.gid,\n taskGid: task.gid,\n projectGid,\n resourceSubtype: story.resource_subtype ?? null,\n authorId: story.created_by?.gid ?? null,\n text: story.text ?? null,\n };\n await storage.event({\n name: 'asana_task_event',\n start_ts: ts,\n end_ts: null,\n attributes,\n });\n }\n }\n }\n }\n\n async sync(\n options: SyncOptions,\n storage: StorageHandle,\n signal?: AbortSignal,\n ): Promise<SyncResult> {\n const cursor = isAsanaSyncCursor(options.cursor)\n ? options.cursor\n : undefined;\n const isFull = options.mode === 'full';\n const sinceMs = options.since ? parseEpoch(options.since, 'iso') : null;\n const phases = this.activePhases();\n\n return paginateChunked<AsanaPhase, string>({\n phases,\n cursor,\n signal,\n fetchPage: async (phase, page, sig) => {\n switch (phase) {\n case 'projects':\n return this.fetchProjectsPage(page, sig);\n case 'users':\n return this.fetchUsersPage(page, sig);\n case 'tasks':\n return this.fetchTasksPage(page, options, sig);\n }\n },\n writeBatch: async (phase, items, page) => {\n if (isFull && page === null) {\n switch (phase) {\n case 'projects':\n await storage.entities([], { types: ['asana_project'] });\n break;\n case 'users':\n await storage.entities([], { types: ['asana_user'] });\n break;\n case 'tasks':\n if (this.isResourceEnabled('tasks')) {\n await storage.entities([], { types: ['asana_task'] });\n }\n if (this.isResourceEnabled('task_events')) {\n await storage.events([], { names: ['asana_task_event'] });\n }\n break;\n }\n }\n switch (phase) {\n case 'projects':\n return this.writeProjects(storage, items as AsanaProject[]);\n case 'users':\n return this.writeUsers(storage, items as AsanaUser[]);\n case 'tasks':\n return this.writeTasks(\n storage,\n items as AsanaTaskWithContext[],\n sinceMs,\n );\n }\n },\n });\n }\n}\n","import { AsanaConnector } from './asana';\n\nexport {\n configFields,\n doc,\n AsanaConnector,\n asanaResources as resources,\n id,\n} from './asana';\nexport type { AsanaSettings, AsanaResource } from './asana';\nexport default AsanaConnector;\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@rawdash/connector-asana",
3
+ "version": "0.27.0",
4
+ "description": "Rawdash connector for Asana — syncs projects, users, tasks, and task state-change events from a workspace into the six-shape storage model",
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/rawdash/rawdash.git",
11
+ "directory": "packages/connectors/asana"
12
+ },
13
+ "files": [
14
+ "dist",
15
+ "README.md",
16
+ "LICENSE"
17
+ ],
18
+ "exports": {
19
+ ".": {
20
+ "@rawdash/source": "./src/index.ts",
21
+ "types": "./dist/index.d.ts",
22
+ "import": "./dist/index.js"
23
+ }
24
+ },
25
+ "scripts": {
26
+ "build": "tsup",
27
+ "typecheck": "tsc --noEmit",
28
+ "lint": "eslint src",
29
+ "test": "vitest run"
30
+ },
31
+ "dependencies": {
32
+ "@rawdash/core": "workspace:*",
33
+ "zod": "^4.4.3"
34
+ },
35
+ "devDependencies": {
36
+ "@rawdash/connector-shared": "workspace:*",
37
+ "@rawdash/connector-test-utils": "workspace:*",
38
+ "fast-check": "^4.8.0",
39
+ "tsup": "^8.0.0",
40
+ "typescript": "^5.7.2",
41
+ "vitest": "^4.1.4"
42
+ }
43
+ }