@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.
- package/dist/AsanaConnector.d.ts +161 -0
- package/dist/AsanaConnector.js +450 -0
- package/dist/AsanaConnector.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/package.json +40 -0
|
@@ -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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"}
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|