@memberjunction/connector-asana 0.2.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.
@@ -0,0 +1,161 @@
1
+ import { type UserInfo } from '@memberjunction/core';
2
+ import type { MJCompanyIntegrationEntity, MJIntegrationObjectEntity, MJIntegrationObjectFieldEntity } from '@memberjunction/core-entities';
3
+ import { BaseRESTIntegrationConnector, type RESTAuthContext, type RESTResponse, type PaginationState, type PaginationType, type ConnectionTestResult, type FetchContext, type FetchBatchResult } from '@memberjunction/integration-engine';
4
+ /**
5
+ * Asana work-management connector (REST API v1.0, read-only).
6
+ *
7
+ * ── Why this connector is almost entirely metadata ──
8
+ * Asana's four synced surfaces are plain GETs whose paths, page sizes and field projections are all
9
+ * expressible as IntegrationObject metadata, so the base class drives the whole fetch. What is left
10
+ * here is exactly the five things metadata cannot express: the bearer header, the per-tenant
11
+ * workspace scope, Asana's non-standard cursor spelling, the `modified_since` incremental filter,
12
+ * and the flattening of Asana's nested sub-objects onto declared columns.
13
+ *
14
+ * ── Workspace scope is a query param, NOT a template variable ──
15
+ * Every Asana listing is workspace-scoped, and the workspace gid lives on
16
+ * CompanyIntegration.ExternalSystemID. It is deliberately NOT modelled as a `{workspace}` template
17
+ * var: template vars are resolved by iterating a *synced parent object*, and there is no Workspaces
18
+ * object to iterate (nor should there be — a connection is one workspace). It is injected as a query
19
+ * param instead, which is the tenant-level scoping that the `CONNECTION_VARS` exemption in
20
+ * scripts/validate-parent-declarations.mjs describes.
21
+ *
22
+ * ── Tasks and Subtasks are templated child doors ──
23
+ * Asana publishes no workspace-wide task listing: tasks are addressable only per project
24
+ * (`/tasks?project=`) and subtasks only per task (`/tasks/{gid}/subtasks`). Both objects therefore
25
+ * declare `Configuration.parentObjectName` so the engine iterates the already-synced parents. Without
26
+ * that declaration each would fetch zero rows and the run would still report success — the exact
27
+ * silent-empty class validate-parent-declarations.mjs guards.
28
+ *
29
+ * ── Left behind from the legacy AIDP driver on purpose ──
30
+ * The legacy driver resolved Asana records against AIDP's own Employee/Project/Task tables and wrote
31
+ * three hardcoded custom-field gids back into Asana. None of that is vendor shape — it is one
32
+ * tenant's model — so this connector lands raw Asana records and nothing else. Likewise the driver's
33
+ * four named custom fields (Role / Skills / Status / ETC) are one workspace's configuration, not
34
+ * Asana's: custom fields are declared per workspace and cannot be columns, so the whole array lands
35
+ * as `custom_fields_json` for downstream projection.
36
+ */
37
+ export declare class AsanaConnector extends BaseRESTIntegrationConnector {
38
+ /** Verbatim three-way invariant name: ClassName / IntegrationName getter / MJ: Integrations.Name. */
39
+ get IntegrationName(): string;
40
+ /**
41
+ * The watermark is `modified_at`, which Asana advances monotonically, and the incremental filter
42
+ * (`modified_since`) is inclusive-from — so a resumed sync can never step backwards over records
43
+ * it already holds.
44
+ */
45
+ get MonotonicWatermark(): boolean;
46
+ /**
47
+ * Asana's list endpoints have no documented stable sort key and no `order_by` parameter, so a
48
+ * keyset resume would be resuming against an ordering the vendor never promised. Paging is the
49
+ * opaque `offset` cursor instead.
50
+ */
51
+ StableOrderingKey(_objectName: string): string | null;
52
+ /**
53
+ * The watermark for the object currently being fetched, stashed by FetchChanges so
54
+ * AppendDefaultQueryParams — which the base calls per page and which receives no context — can
55
+ * apply `modified_since`. Cleared on the way out so a non-incremental object can never inherit
56
+ * the previous object's filter.
57
+ */
58
+ protected currentWatermark: string | null;
59
+ /** Workspace gid for the connection currently being fetched — stashed for the same reason. */
60
+ protected currentWorkspace: string | null;
61
+ /**
62
+ * Resolves the personal access token and the workspace gid. The token comes from the linked
63
+ * Credential entity, falling back to CompanyIntegration.Configuration; the workspace comes from
64
+ * ExternalSystemID, which is what that column means for a workspace-scoped vendor, with a
65
+ * Configuration override for tenants that set it there instead.
66
+ */
67
+ protected Authenticate(companyIntegration: MJCompanyIntegrationEntity, contextUser: UserInfo): Promise<AsanaAuthContext>;
68
+ protected BuildHeaders(auth: AsanaAuthContext): Record<string, string>;
69
+ protected GetBaseURL(_companyIntegration: MJCompanyIntegrationEntity, _auth: AsanaAuthContext): string;
70
+ protected MakeHTTPRequest(_auth: AsanaAuthContext, url: string, method: string, headers: Record<string, string>, body?: unknown): Promise<RESTResponse>;
71
+ /** Asana wraps every collection response in `{ "data": [...] }` (the declared ResponseDataKey). */
72
+ protected NormalizeResponse(rawBody: unknown, responseDataKey: string | null): Record<string, unknown>[];
73
+ /**
74
+ * Asana's cursor lives at `next_page.offset`, and its ABSENCE — not a flag — is what ends the
75
+ * stream: `next_page` is null on the last page. So treating a missing cursor as "done" is the
76
+ * vendor's own contract rather than an inference.
77
+ */
78
+ protected ExtractPaginationInfo(rawBody: unknown, paginationType: PaginationType, _currentPage: number, _currentOffset: number, _pageSize: number): PaginationState;
79
+ /**
80
+ * Asana spells its cursor `offset` and its page size `limit`, where the base class's Cursor case
81
+ * emits `cursor=`. Sending the base's spelling is not an error Asana reports — it ignores the
82
+ * unknown param and re-serves page one, which the base's duplicate-page guard would catch only
83
+ * after a wasted round trip and a truncated object. This override is the one place that mismatch
84
+ * is fixed.
85
+ *
86
+ * `limit` is clamped into Asana's documented 1..100 range: the base passes the remaining batch
87
+ * capacity as the effective page size, and a value outside that range is a 400 from Asana.
88
+ */
89
+ protected BuildPaginatedURL(basePath: string, obj: MJIntegrationObjectEntity, page: number, offset: number, cursor?: string, effectivePageSize?: number): string;
90
+ /**
91
+ * Adds the two params that are per-connection or per-run rather than per-object: the workspace
92
+ * scope and the incremental `modified_since` filter.
93
+ *
94
+ * The static `opt_fields` projection is deliberately NOT added here — it is declared metadata
95
+ * (DefaultQueryParams), which the base appends, so the field projection stays next to the field
96
+ * declarations it has to agree with.
97
+ */
98
+ protected AppendDefaultQueryParams(url: string, obj: MJIntegrationObjectEntity): string;
99
+ /**
100
+ * Delegates the whole fetch to the base (pagination, parent iteration, batching) and adds only
101
+ * what the base has no way to know: the per-run scope params, and the new watermark.
102
+ *
103
+ * The watermark advances to the maximum `modified_at` actually observed, never to the wall clock.
104
+ * A clock-based watermark would silently skip anything modified between the last page fetched and
105
+ * the moment the run finished, and `modified_since` is compared against Asana's server clock, not
106
+ * ours — so the only safe high-water mark is one the server itself stamped on a record we hold.
107
+ */
108
+ FetchChanges(ctx: FetchContext): Promise<FetchBatchResult>;
109
+ /**
110
+ * Flattens Asana's nested sub-objects onto the declared columns.
111
+ *
112
+ * Asana returns compound values as objects (`owner: {gid}`, `current_status: {color,title,text}`,
113
+ * `memberships: [{section: {name}}]`), and the sync engine maps a declared column only from a
114
+ * top-level key of the same name — so without this every one of those columns lands null while
115
+ * the run reports success. The base's applyTransformPreservingKeys keeps the original nested keys
116
+ * alongside these, so full-record custom-column capture still sees everything Asana sent.
117
+ */
118
+ protected TransformRecord(raw: Record<string, unknown>, _obj: MJIntegrationObjectEntity, _fields: MJIntegrationObjectFieldEntity[]): Record<string, unknown>;
119
+ /**
120
+ * Probes `/users/me`, the one Asana endpoint valid for every token regardless of workspace
121
+ * membership or scope — so a failure there is unambiguously a credential problem rather than a
122
+ * permissions one. The configured workspace is then checked against the workspaces the token can
123
+ * actually see, because a token that authenticates but cannot see the configured workspace
124
+ * produces empty-but-successful syncs, which is the failure most worth catching here.
125
+ */
126
+ TestConnection(companyIntegration: MJCompanyIntegrationEntity, contextUser: UserInfo): Promise<ConnectionTestResult>;
127
+ /**
128
+ * Resolves the access token from the linked Credential entity, falling back to the
129
+ * CompanyIntegration.Configuration JSON.
130
+ *
131
+ * CompanyIntegration.APIKey is deliberately NOT read: it is not a decrypt-on-read column, so a
132
+ * value written through mj-sync encryption comes back as the literal `$ENC$…` string and would be
133
+ * sent to Asana verbatim — authenticating as nobody while looking configured.
134
+ */
135
+ private LoadCredentials;
136
+ private LoadFromCredentialEntity;
137
+ /** Parses a credential/Configuration JSON blob, tolerating the usual casing/naming aliases. */
138
+ private ParseCredentialJson;
139
+ }
140
+ /** Auth context: bearer token plus the workspace every listing is scoped to. */
141
+ interface AsanaAuthContext extends RESTAuthContext {
142
+ Token: string;
143
+ Workspace: string;
144
+ }
145
+ /**
146
+ * Asana's `modified_since` wants a full ISO-8601 instant. A watermark that already is one passes
147
+ * through untouched; a date-only watermark is widened to the start of that day rather than narrowed,
148
+ * because widening re-reads records (harmless — they upsert by gid) while narrowing loses them.
149
+ */
150
+ export declare function toAsanaTimestamp(watermark: string): string;
151
+ /**
152
+ * The highest `modified_at` across a batch, or null when the batch moves it nowhere.
153
+ *
154
+ * Compared as ISO strings, which is lexicographically correct for the fixed-width UTC form Asana
155
+ * emits. The previous watermark seeds the comparison so a batch containing only older records can
156
+ * never drag the high-water mark backwards.
157
+ */
158
+ export declare function maxModifiedAt(records: ReadonlyArray<{
159
+ Fields: Record<string, unknown>;
160
+ }>, previous: string | null): string | null;
161
+ export {};
@@ -0,0 +1,450 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { RegisterClass } from '@memberjunction/global';
8
+ import { Metadata } from '@memberjunction/core';
9
+ import { BaseIntegrationConnector, BaseRESTIntegrationConnector, } from '@memberjunction/integration-engine';
10
+ import { z } from 'zod';
11
+ /**
12
+ * Asana work-management connector (REST API v1.0, read-only).
13
+ *
14
+ * ── Why this connector is almost entirely metadata ──
15
+ * Asana's four synced surfaces are plain GETs whose paths, page sizes and field projections are all
16
+ * expressible as IntegrationObject metadata, so the base class drives the whole fetch. What is left
17
+ * here is exactly the five things metadata cannot express: the bearer header, the per-tenant
18
+ * workspace scope, Asana's non-standard cursor spelling, the `modified_since` incremental filter,
19
+ * and the flattening of Asana's nested sub-objects onto declared columns.
20
+ *
21
+ * ── Workspace scope is a query param, NOT a template variable ──
22
+ * Every Asana listing is workspace-scoped, and the workspace gid lives on
23
+ * CompanyIntegration.ExternalSystemID. It is deliberately NOT modelled as a `{workspace}` template
24
+ * var: template vars are resolved by iterating a *synced parent object*, and there is no Workspaces
25
+ * object to iterate (nor should there be — a connection is one workspace). It is injected as a query
26
+ * param instead, which is the tenant-level scoping that the `CONNECTION_VARS` exemption in
27
+ * scripts/validate-parent-declarations.mjs describes.
28
+ *
29
+ * ── Tasks and Subtasks are templated child doors ──
30
+ * Asana publishes no workspace-wide task listing: tasks are addressable only per project
31
+ * (`/tasks?project=`) and subtasks only per task (`/tasks/{gid}/subtasks`). Both objects therefore
32
+ * declare `Configuration.parentObjectName` so the engine iterates the already-synced parents. Without
33
+ * that declaration each would fetch zero rows and the run would still report success — the exact
34
+ * silent-empty class validate-parent-declarations.mjs guards.
35
+ *
36
+ * ── Left behind from the legacy AIDP driver on purpose ──
37
+ * The legacy driver resolved Asana records against AIDP's own Employee/Project/Task tables and wrote
38
+ * three hardcoded custom-field gids back into Asana. None of that is vendor shape — it is one
39
+ * tenant's model — so this connector lands raw Asana records and nothing else. Likewise the driver's
40
+ * four named custom fields (Role / Skills / Status / ETC) are one workspace's configuration, not
41
+ * Asana's: custom fields are declared per workspace and cannot be columns, so the whole array lands
42
+ * as `custom_fields_json` for downstream projection.
43
+ */
44
+ // Primary key follows the catalog convention (className == npm package name; see
45
+ // scripts/build-connectors-catalog.mjs) — instance discovery reports the package name, so a bare
46
+ // class-symbol key would never match in the catalog. The bare symbol stays registered as an alias.
47
+ let AsanaConnector = class AsanaConnector extends BaseRESTIntegrationConnector {
48
+ constructor() {
49
+ super(...arguments);
50
+ /**
51
+ * The watermark for the object currently being fetched, stashed by FetchChanges so
52
+ * AppendDefaultQueryParams — which the base calls per page and which receives no context — can
53
+ * apply `modified_since`. Cleared on the way out so a non-incremental object can never inherit
54
+ * the previous object's filter.
55
+ */
56
+ this.currentWatermark = null;
57
+ /** Workspace gid for the connection currently being fetched — stashed for the same reason. */
58
+ this.currentWorkspace = null;
59
+ }
60
+ /** Verbatim three-way invariant name: ClassName / IntegrationName getter / MJ: Integrations.Name. */
61
+ get IntegrationName() {
62
+ return 'Asana';
63
+ }
64
+ /**
65
+ * The watermark is `modified_at`, which Asana advances monotonically, and the incremental filter
66
+ * (`modified_since`) is inclusive-from — so a resumed sync can never step backwards over records
67
+ * it already holds.
68
+ */
69
+ get MonotonicWatermark() {
70
+ return true;
71
+ }
72
+ /**
73
+ * Asana's list endpoints have no documented stable sort key and no `order_by` parameter, so a
74
+ * keyset resume would be resuming against an ordering the vendor never promised. Paging is the
75
+ * opaque `offset` cursor instead.
76
+ */
77
+ StableOrderingKey(_objectName) {
78
+ return null;
79
+ }
80
+ // ─── Auth + transport (BaseRESTIntegrationConnector abstracts) ────
81
+ /**
82
+ * Resolves the personal access token and the workspace gid. The token comes from the linked
83
+ * Credential entity, falling back to CompanyIntegration.Configuration; the workspace comes from
84
+ * ExternalSystemID, which is what that column means for a workspace-scoped vendor, with a
85
+ * Configuration override for tenants that set it there instead.
86
+ */
87
+ async Authenticate(companyIntegration, contextUser) {
88
+ const resolved = await this.LoadCredentials(companyIntegration, contextUser);
89
+ const workspace = companyIntegration.ExternalSystemID ?? resolved.Workspace;
90
+ if (!workspace) {
91
+ throw new Error('Asana workspace is not configured. Every Asana listing is workspace-scoped; set the ' +
92
+ 'workspace gid on CompanyIntegration.ExternalSystemID (or "workspace" in the Configuration JSON).');
93
+ }
94
+ return { Token: resolved.Token, Workspace: String(workspace) };
95
+ }
96
+ BuildHeaders(auth) {
97
+ return {
98
+ 'Authorization': `Bearer ${auth.Token}`,
99
+ 'Accept': 'application/json',
100
+ // Asana gates behaviour changes behind opt-in headers rather than a version in the URL.
101
+ // These two are the deprecations the legacy driver already opted into; without them the
102
+ // shapes of user task lists and goal memberships differ from what this catalog declares.
103
+ 'Asana-Enable': 'new_goal_memberships,new_user_task_lists',
104
+ };
105
+ }
106
+ GetBaseURL(_companyIntegration, _auth) {
107
+ return ASANA_API_BASE;
108
+ }
109
+ async MakeHTTPRequest(_auth, url, method, headers, body) {
110
+ const init = { method, headers };
111
+ if (body !== undefined && method !== 'GET' && method !== 'HEAD') {
112
+ init.body = typeof body === 'string' ? body : JSON.stringify(body);
113
+ init.headers['Content-Type'] = 'application/json';
114
+ }
115
+ const response = await fetch(url, init);
116
+ const responseHeaders = {};
117
+ response.headers.forEach((value, key) => { responseHeaders[key.toLowerCase()] = value; });
118
+ const text = await response.text();
119
+ let parsed = text;
120
+ const contentType = responseHeaders['content-type'] ?? '';
121
+ if (contentType.includes('json') || (text.length > 0 && (text[0] === '{' || text[0] === '['))) {
122
+ try {
123
+ parsed = JSON.parse(text);
124
+ }
125
+ catch {
126
+ parsed = text;
127
+ }
128
+ }
129
+ return { Status: response.status, Body: parsed, Headers: responseHeaders };
130
+ }
131
+ /** Asana wraps every collection response in `{ "data": [...] }` (the declared ResponseDataKey). */
132
+ NormalizeResponse(rawBody, responseDataKey) {
133
+ const key = responseDataKey ?? 'data';
134
+ if (isRecord(rawBody)) {
135
+ const inner = rawBody[key];
136
+ if (Array.isArray(inner))
137
+ return inner.filter(isRecord);
138
+ if (isRecord(inner))
139
+ return [inner];
140
+ }
141
+ if (Array.isArray(rawBody))
142
+ return rawBody.filter(isRecord);
143
+ return [];
144
+ }
145
+ /**
146
+ * Asana's cursor lives at `next_page.offset`, and its ABSENCE — not a flag — is what ends the
147
+ * stream: `next_page` is null on the last page. So treating a missing cursor as "done" is the
148
+ * vendor's own contract rather than an inference.
149
+ */
150
+ ExtractPaginationInfo(rawBody, paginationType, _currentPage, _currentOffset, _pageSize) {
151
+ if (paginationType !== 'Cursor')
152
+ return { HasMore: false };
153
+ if (isRecord(rawBody)) {
154
+ const nextPage = rawBody['next_page'];
155
+ if (isRecord(nextPage)) {
156
+ const offset = nextPage['offset'];
157
+ if (typeof offset === 'string' && offset.length > 0) {
158
+ return { HasMore: true, NextCursor: offset };
159
+ }
160
+ }
161
+ }
162
+ return { HasMore: false };
163
+ }
164
+ /**
165
+ * Asana spells its cursor `offset` and its page size `limit`, where the base class's Cursor case
166
+ * emits `cursor=`. Sending the base's spelling is not an error Asana reports — it ignores the
167
+ * unknown param and re-serves page one, which the base's duplicate-page guard would catch only
168
+ * after a wasted round trip and a truncated object. This override is the one place that mismatch
169
+ * is fixed.
170
+ *
171
+ * `limit` is clamped into Asana's documented 1..100 range: the base passes the remaining batch
172
+ * capacity as the effective page size, and a value outside that range is a 400 from Asana.
173
+ */
174
+ BuildPaginatedURL(basePath, obj, page, offset, cursor, effectivePageSize) {
175
+ if (obj.PaginationType !== 'Cursor') {
176
+ return super.BuildPaginatedURL(basePath, obj, page, offset, cursor, effectivePageSize);
177
+ }
178
+ const requested = effectivePageSize ?? obj.DefaultPageSize ?? ASANA_MAX_PAGE_SIZE;
179
+ const limit = Math.min(ASANA_MAX_PAGE_SIZE, Math.max(1, requested));
180
+ const separator = basePath.includes('?') ? '&' : '?';
181
+ return cursor
182
+ ? `${basePath}${separator}offset=${encodeURIComponent(cursor)}&limit=${limit}`
183
+ : `${basePath}${separator}limit=${limit}`;
184
+ }
185
+ /**
186
+ * Adds the two params that are per-connection or per-run rather than per-object: the workspace
187
+ * scope and the incremental `modified_since` filter.
188
+ *
189
+ * The static `opt_fields` projection is deliberately NOT added here — it is declared metadata
190
+ * (DefaultQueryParams), which the base appends, so the field projection stays next to the field
191
+ * declarations it has to agree with.
192
+ */
193
+ AppendDefaultQueryParams(url, obj) {
194
+ let out = super.AppendDefaultQueryParams(url, obj);
195
+ if (WORKSPACE_SCOPED_OBJECTS.has(obj.Name) && this.currentWorkspace) {
196
+ out = appendParam(out, 'workspace', this.currentWorkspace);
197
+ }
198
+ if (obj.SupportsIncrementalSync && this.currentWatermark) {
199
+ out = appendParam(out, 'modified_since', toAsanaTimestamp(this.currentWatermark));
200
+ }
201
+ return out;
202
+ }
203
+ // ─── Fetch ───────────────────────────────────────────────────────
204
+ /**
205
+ * Delegates the whole fetch to the base (pagination, parent iteration, batching) and adds only
206
+ * what the base has no way to know: the per-run scope params, and the new watermark.
207
+ *
208
+ * The watermark advances to the maximum `modified_at` actually observed, never to the wall clock.
209
+ * A clock-based watermark would silently skip anything modified between the last page fetched and
210
+ * the moment the run finished, and `modified_since` is compared against Asana's server clock, not
211
+ * ours — so the only safe high-water mark is one the server itself stamped on a record we hold.
212
+ */
213
+ async FetchChanges(ctx) {
214
+ const auth = await this.Authenticate(ctx.CompanyIntegration, ctx.ContextUser);
215
+ this.currentWorkspace = auth.Workspace;
216
+ this.currentWatermark = ctx.WatermarkValue;
217
+ try {
218
+ const result = await super.FetchChanges(ctx);
219
+ const newWatermark = maxModifiedAt(result.Records, ctx.WatermarkValue);
220
+ return newWatermark ? { ...result, NewWatermarkValue: newWatermark } : result;
221
+ }
222
+ finally {
223
+ this.currentWorkspace = null;
224
+ this.currentWatermark = null;
225
+ }
226
+ }
227
+ /**
228
+ * Flattens Asana's nested sub-objects onto the declared columns.
229
+ *
230
+ * Asana returns compound values as objects (`owner: {gid}`, `current_status: {color,title,text}`,
231
+ * `memberships: [{section: {name}}]`), and the sync engine maps a declared column only from a
232
+ * top-level key of the same name — so without this every one of those columns lands null while
233
+ * the run reports success. The base's applyTransformPreservingKeys keeps the original nested keys
234
+ * alongside these, so full-record custom-column capture still sees everything Asana sent.
235
+ */
236
+ TransformRecord(raw, _obj, _fields) {
237
+ const out = { ...raw };
238
+ for (const [source, target] of NESTED_GID_FIELDS) {
239
+ const value = raw[source];
240
+ if (isRecord(value))
241
+ out[target] = stringOrNull(value['gid']);
242
+ else if (value === null)
243
+ out[target] = null;
244
+ }
245
+ const status = raw['current_status'];
246
+ if (isRecord(status)) {
247
+ out['current_status_color'] = stringOrNull(status['color']);
248
+ out['current_status_title'] = stringOrNull(status['title']);
249
+ out['current_status_text'] = stringOrNull(status['text']);
250
+ }
251
+ else if (status === null) {
252
+ out['current_status_color'] = null;
253
+ out['current_status_title'] = null;
254
+ out['current_status_text'] = null;
255
+ }
256
+ // Asana returns one membership per project the task belongs to. This connector fetches tasks
257
+ // one project at a time, so the first membership is the one for the project door we came
258
+ // through — the same choice the legacy driver made, but without its extra per-task GET:
259
+ // `memberships.section.name` is an opt_field on the listing, so the section arrives with the
260
+ // task rather than costing one request per record.
261
+ const memberships = raw['memberships'];
262
+ if (Array.isArray(memberships)) {
263
+ const first = memberships.find(isRecord);
264
+ const section = first ? first['section'] : undefined;
265
+ out['section_name'] = isRecord(section) ? stringOrNull(section['name']) : null;
266
+ }
267
+ const customFields = raw['custom_fields'];
268
+ if (customFields !== undefined) {
269
+ out['custom_fields_json'] = Array.isArray(customFields) && customFields.length > 0
270
+ ? JSON.stringify(customFields)
271
+ : null;
272
+ }
273
+ return out;
274
+ }
275
+ // ─── Connection test ─────────────────────────────────────────────
276
+ /**
277
+ * Probes `/users/me`, the one Asana endpoint valid for every token regardless of workspace
278
+ * membership or scope — so a failure there is unambiguously a credential problem rather than a
279
+ * permissions one. The configured workspace is then checked against the workspaces the token can
280
+ * actually see, because a token that authenticates but cannot see the configured workspace
281
+ * produces empty-but-successful syncs, which is the failure most worth catching here.
282
+ */
283
+ async TestConnection(companyIntegration, contextUser) {
284
+ try {
285
+ const auth = await this.Authenticate(companyIntegration, contextUser);
286
+ const headers = this.BuildHeaders(auth);
287
+ const me = await this.MakeHTTPRequest(auth, `${ASANA_API_BASE}/users/me`, 'GET', headers);
288
+ if (me.Status === 401) {
289
+ return { Success: false, Message: 'Asana rejected the access token (HTTP 401).' };
290
+ }
291
+ if (me.Status >= 400) {
292
+ return { Success: false, Message: `Asana /users/me returned HTTP ${me.Status}.` };
293
+ }
294
+ const body = isRecord(me.Body) ? me.Body : {};
295
+ const data = isRecord(body['data']) ? body['data'] : {};
296
+ const who = stringOrNull(data['name']) ?? stringOrNull(data['gid']) ?? 'unknown user';
297
+ const workspaces = Array.isArray(data['workspaces']) ? data['workspaces'].filter(isRecord) : [];
298
+ const visible = workspaces
299
+ .map(w => stringOrNull(w['gid']))
300
+ .filter((g) => g !== null);
301
+ if (visible.length > 0 && !visible.includes(auth.Workspace)) {
302
+ return {
303
+ Success: false,
304
+ Message: `Asana token authenticated as ${who}, but workspace "${auth.Workspace}" is not one of ` +
305
+ `the workspaces it can see (${visible.join(', ')}). Every listing is workspace-scoped, ` +
306
+ `so this connection would sync zero records.`,
307
+ };
308
+ }
309
+ return { Success: true, Message: `Connected to Asana as ${who} (workspace ${auth.Workspace}).` };
310
+ }
311
+ catch (err) {
312
+ const message = err instanceof Error ? err.message : String(err);
313
+ return { Success: false, Message: `Asana connection error: ${message}` };
314
+ }
315
+ }
316
+ // ─── Credential resolution ───────────────────────────────────────
317
+ /**
318
+ * Resolves the access token from the linked Credential entity, falling back to the
319
+ * CompanyIntegration.Configuration JSON.
320
+ *
321
+ * CompanyIntegration.APIKey is deliberately NOT read: it is not a decrypt-on-read column, so a
322
+ * value written through mj-sync encryption comes back as the literal `$ENC$…` string and would be
323
+ * sent to Asana verbatim — authenticating as nobody while looking configured.
324
+ */
325
+ async LoadCredentials(companyIntegration, contextUser) {
326
+ let token;
327
+ let workspace;
328
+ if (companyIntegration.CredentialID) {
329
+ const fromCred = await this.LoadFromCredentialEntity(companyIntegration.CredentialID, contextUser);
330
+ if (fromCred) {
331
+ token = fromCred.Token ?? token;
332
+ workspace = fromCred.Workspace ?? workspace;
333
+ }
334
+ }
335
+ if (companyIntegration.Configuration) {
336
+ const fromConfig = this.ParseCredentialJson(companyIntegration.Configuration);
337
+ if (fromConfig) {
338
+ token = token ?? fromConfig.Token;
339
+ workspace = workspace ?? fromConfig.Workspace;
340
+ }
341
+ }
342
+ if (!token) {
343
+ throw new Error('No Asana credential found — link an "API Key" credential holding a personal access ' +
344
+ 'token, or supply one as "token" in the CompanyIntegration.Configuration JSON.');
345
+ }
346
+ return { Token: token, Workspace: workspace };
347
+ }
348
+ async LoadFromCredentialEntity(credentialID, contextUser, provider) {
349
+ const md = provider ?? new Metadata();
350
+ const credential = await md.GetEntityObject('MJ: Credentials', contextUser);
351
+ const loaded = await credential.Load(credentialID);
352
+ if (!loaded || !credential.Values)
353
+ return null;
354
+ return this.ParseCredentialJson(credential.Values);
355
+ }
356
+ /** Parses a credential/Configuration JSON blob, tolerating the usual casing/naming aliases. */
357
+ ParseCredentialJson(json) {
358
+ try {
359
+ const result = AsanaCredentialSchema.safeParse(JSON.parse(json));
360
+ if (!result.success)
361
+ return null;
362
+ const p = result.data;
363
+ const token = p.Token ?? p.token ?? p.apiKey ?? p.ApiKey ?? p.accessToken ?? p.personalAccessToken;
364
+ const workspace = p.Workspace ?? p.workspace ?? p.workspaceGid ?? p.WorkspaceID;
365
+ if (token == null && workspace == null)
366
+ return null;
367
+ return {
368
+ Token: token != null ? String(token) : undefined,
369
+ Workspace: workspace != null ? String(workspace) : undefined,
370
+ };
371
+ }
372
+ catch {
373
+ return null;
374
+ }
375
+ }
376
+ };
377
+ AsanaConnector = __decorate([
378
+ RegisterClass(BaseIntegrationConnector, '@memberjunction/connector-asana'),
379
+ RegisterClass(BaseIntegrationConnector, 'AsanaConnector')
380
+ ], AsanaConnector);
381
+ export { AsanaConnector };
382
+ // ─── Module-level constants, types + helpers (mechanism, NOT a catalog) ───
383
+ /** Asana's REST base. Single-tenant SaaS with no per-customer host, so there is nothing to configure. */
384
+ const ASANA_API_BASE = 'https://app.asana.com/api/1.0';
385
+ /** Asana rejects `limit` outside 1..100 with a 400. */
386
+ const ASANA_MAX_PAGE_SIZE = 100;
387
+ /**
388
+ * The objects whose listing needs an explicit `workspace` param. The templated child doors are
389
+ * already scoped by the parent id in their path, and Asana rejects `workspace` alongside `project`.
390
+ */
391
+ const WORKSPACE_SCOPED_OBJECTS = new Set(['Users', 'Projects']);
392
+ /** Nested `{gid}` sub-objects → the flat column each is projected onto. */
393
+ const NESTED_GID_FIELDS = [
394
+ ['owner', 'owner_gid'],
395
+ ['team', 'team_gid'],
396
+ ['workspace', 'workspace_gid'],
397
+ ['assignee', 'assignee_gid'],
398
+ ['parent', 'parent_gid'],
399
+ ];
400
+ const AsanaCredentialSchema = z.object({
401
+ Token: z.string().optional(),
402
+ token: z.string().optional(),
403
+ apiKey: z.string().optional(),
404
+ ApiKey: z.string().optional(),
405
+ accessToken: z.string().optional(),
406
+ personalAccessToken: z.string().optional(),
407
+ Workspace: z.union([z.string(), z.number()]).optional(),
408
+ workspace: z.union([z.string(), z.number()]).optional(),
409
+ workspaceGid: z.union([z.string(), z.number()]).optional(),
410
+ WorkspaceID: z.union([z.string(), z.number()]).optional(),
411
+ }).passthrough();
412
+ function isRecord(v) {
413
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
414
+ }
415
+ function stringOrNull(v) {
416
+ return typeof v === 'string' && v.length > 0 ? v : null;
417
+ }
418
+ function appendParam(url, key, value) {
419
+ const separator = url.includes('?') ? '&' : '?';
420
+ return `${url}${separator}${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
421
+ }
422
+ /**
423
+ * Asana's `modified_since` wants a full ISO-8601 instant. A watermark that already is one passes
424
+ * through untouched; a date-only watermark is widened to the start of that day rather than narrowed,
425
+ * because widening re-reads records (harmless — they upsert by gid) while narrowing loses them.
426
+ */
427
+ export function toAsanaTimestamp(watermark) {
428
+ if (watermark.includes('T'))
429
+ return watermark;
430
+ return `${watermark}T00:00:00.000Z`;
431
+ }
432
+ /**
433
+ * The highest `modified_at` across a batch, or null when the batch moves it nowhere.
434
+ *
435
+ * Compared as ISO strings, which is lexicographically correct for the fixed-width UTC form Asana
436
+ * emits. The previous watermark seeds the comparison so a batch containing only older records can
437
+ * never drag the high-water mark backwards.
438
+ */
439
+ export function maxModifiedAt(records, previous) {
440
+ let best = previous;
441
+ for (const record of records) {
442
+ const value = record.Fields['modified_at'];
443
+ if (typeof value !== 'string' || value.length === 0)
444
+ continue;
445
+ if (best === null || value > best)
446
+ best = value;
447
+ }
448
+ return best === previous ? null : best;
449
+ }
450
+ //# sourceMappingURL=AsanaConnector.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AsanaConnector.js","sourceRoot":"","sources":["../src/AsanaConnector.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ export * from './AsanaConnector.js';
2
+ /** Open App bootstrap entry: importing this module ran the connector's @RegisterClass decorator;
3
+ * this no-op satisfies the loader's required startupExport and forces the import at MJAPI boot. */
4
+ export declare function registerConnector(): void;
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export * from './AsanaConnector.js';
2
+ /** Open App bootstrap entry: importing this module ran the connector's @RegisterClass decorator;
3
+ * this no-op satisfies the loader's required startupExport and forces the import at MJAPI boot. */
4
+ export function registerConnector() { }
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAC;AAEpC;oGACoG;AACpG,MAAM,UAAU,iBAAiB,KAAiD,CAAC"}
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@memberjunction/connector-asana",
3
+ "version": "0.2.0",
4
+ "description": "MemberJunction Asana connector.",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "files": [
9
+ "/dist"
10
+ ],
11
+ "scripts": {
12
+ "build": "tsc && tsc-alias -f",
13
+ "test": "vitest run"
14
+ },
15
+ "author": "MemberJunction.com",
16
+ "license": "ISC",
17
+ "peerDependencies": {
18
+ "@memberjunction/core": ">=5.43.0 <6.0.0",
19
+ "@memberjunction/core-entities": ">=5.43.0 <6.0.0",
20
+ "@memberjunction/global": ">=5.43.0 <6.0.0",
21
+ "@memberjunction/integration-engine": ">=5.43.0 <6.0.0"
22
+ },
23
+ "dependencies": {
24
+ "zod": "~3.24.4"
25
+ },
26
+ "devDependencies": {
27
+ "@memberjunction/core": "^5.43.0",
28
+ "@memberjunction/core-entities": "^5.43.0",
29
+ "@memberjunction/global": "^5.43.0",
30
+ "@memberjunction/integration-engine": "^5.43.0",
31
+ "@types/node": "24.10.11",
32
+ "tsc-alias": "^1.8.16",
33
+ "typescript": "^5.9.3",
34
+ "vitest": "^4.0.18"
35
+ },
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "https://github.com/MemberJunction/Integrations"
39
+ }
40
+ }