@memberjunction/connector-everhour 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/EverhourConnector.d.ts +228 -0
- package/dist/EverhourConnector.js +543 -0
- package/dist/EverhourConnector.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,228 @@
|
|
|
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
|
+
* Everhour time-tracking connector (REST API v1.2, read-only).
|
|
6
|
+
*
|
|
7
|
+
* ── Everhour ids carry a platform prefix, and that is the whole `as:` story ──
|
|
8
|
+
* An Everhour project id is literally `as:1234567890` (Asana-sourced), `jr:…` (Jira), `ev:…` (native),
|
|
9
|
+
* and so on — the prefix is part of the vendor's own identifier, documented in its Project schema, not
|
|
10
|
+
* an addressing convention a caller adds. The legacy AIDP driver looked like it was doing something
|
|
11
|
+
* exotic (`/projects/as:${externalId}/tasks`) only because it had *stripped* the prefix on the way in,
|
|
12
|
+
* with `id.slice(3)`, to make the remainder match an Asana gid — then had to put it back to make the
|
|
13
|
+
* next call. This connector keeps every id exactly as Everhour issued it, so the prefix never has to
|
|
14
|
+
* be reconstructed and the connector never has to know that some Everhour workspaces are backed by
|
|
15
|
+
* Asana. Nothing here is Asana-aware.
|
|
16
|
+
*
|
|
17
|
+
* ── Vendor units are preserved, not converted ──
|
|
18
|
+
* Everhour reports money in cents and durations in seconds. The legacy driver divided fees, rates and
|
|
19
|
+
* budgets by 100 on the way into AIDP's schema; that is a presentation choice belonging to whoever
|
|
20
|
+
* consumes the data, and doing it here would make the landed value disagree with what the API returned
|
|
21
|
+
* and with what Everhour's own UI reports. Every amount lands in the vendor's unit and every field
|
|
22
|
+
* description names that unit.
|
|
23
|
+
*
|
|
24
|
+
* ── Time records come from the team-wide door, not per project ──
|
|
25
|
+
* The legacy driver read time one project at a time (`/projects/{id}/time`), which is an N+1 over the
|
|
26
|
+
* project list and, at Everhour's ~20 requests / 10 seconds, the dominant cost of a run. Everhour also
|
|
27
|
+
* publishes `/team/time?from=&to=`, which returns the same records for the whole team in one paged
|
|
28
|
+
* stream. TimeRecords therefore has no parent door at all, and `from`/`to` gives the incremental
|
|
29
|
+
* filter directly.
|
|
30
|
+
*
|
|
31
|
+
* ── Tasks is a templated child door ──
|
|
32
|
+
* Everhour exposes no unfiltered team-wide task listing (`/tasks/search` requires a search term), so
|
|
33
|
+
* tasks are addressable only per project. Tasks declares `Configuration.parentObjectName: "Projects"`
|
|
34
|
+
* so the engine iterates the already-synced projects; without that declaration it would fetch zero
|
|
35
|
+
* rows and the run would still report success.
|
|
36
|
+
*/
|
|
37
|
+
export declare class EverhourConnector extends BaseRESTIntegrationConnector {
|
|
38
|
+
/** Verbatim three-way invariant name: ClassName / IntegrationName getter / MJ: Integrations.Name. */
|
|
39
|
+
get IntegrationName(): string;
|
|
40
|
+
/**
|
|
41
|
+
* The only incremental object is TimeRecords, watermarked on `date`, and the watermark only ever
|
|
42
|
+
* advances to the maximum date actually observed. Records are never re-dated backwards by Everhour
|
|
43
|
+
* — an edit changes `time`/`comment`, not which day the work happened — so the high-water mark is
|
|
44
|
+
* monotonic even though the records behind it are mutable. The lookback window below is what
|
|
45
|
+
* covers those mutations.
|
|
46
|
+
*/
|
|
47
|
+
get MonotonicWatermark(): boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Everhour's list endpoints document no ordering guarantee and offer no sort parameter, so there
|
|
50
|
+
* is no key a keyset resume could resume against. Paging is `page`/`limit`.
|
|
51
|
+
*/
|
|
52
|
+
StableOrderingKey(_objectName: string): string | null;
|
|
53
|
+
/**
|
|
54
|
+
* The watermark for the object currently being fetched, stashed by FetchChanges so
|
|
55
|
+
* AppendDefaultQueryParams — which the base calls per page and which receives no context — can
|
|
56
|
+
* apply the `from`/`to` window. Cleared on the way out so a non-incremental object can never
|
|
57
|
+
* inherit the previous object's filter.
|
|
58
|
+
*/
|
|
59
|
+
protected currentWatermark: string | null;
|
|
60
|
+
/**
|
|
61
|
+
* Resolves the API key. Everhour has no separate tenant identifier: the key *is* the team scope,
|
|
62
|
+
* which is why nothing here reads ExternalSystemID and why there is no workspace parameter on any
|
|
63
|
+
* request.
|
|
64
|
+
*/
|
|
65
|
+
protected Authenticate(companyIntegration: MJCompanyIntegrationEntity, contextUser: UserInfo): Promise<EverhourAuthContext>;
|
|
66
|
+
protected BuildHeaders(auth: EverhourAuthContext): Record<string, string>;
|
|
67
|
+
protected GetBaseURL(_companyIntegration: MJCompanyIntegrationEntity, _auth: EverhourAuthContext): string;
|
|
68
|
+
/**
|
|
69
|
+
* Plain fetch, with one deliberate URL normalization: `%3A` is decoded back to `:`.
|
|
70
|
+
*
|
|
71
|
+
* Every Everhour project and task id contains a colon (`as:1234567890`), and the base substitutes
|
|
72
|
+
* template variables with encodeURIComponent, which turns the id in `/projects/{project_id}/tasks`
|
|
73
|
+
* into `as%3A1234567890`. That substitution helper is private, so the path cannot be fixed where it
|
|
74
|
+
* is built — this is the one seam that sees the final URL. A colon is a legal character in both a
|
|
75
|
+
* path segment and a query value under RFC 3986, so decoding it is safe everywhere in the URL, and
|
|
76
|
+
* it removes any dependence on Everhour's router happening to percent-decode before matching.
|
|
77
|
+
*/
|
|
78
|
+
protected MakeHTTPRequest(_auth: EverhourAuthContext, url: string, method: string, headers: Record<string, string>, body?: unknown): Promise<RESTResponse>;
|
|
79
|
+
/**
|
|
80
|
+
* Everhour returns bare JSON arrays with no envelope — there is no data key to unwrap, which is
|
|
81
|
+
* why every object declares an empty ResponseDataKey.
|
|
82
|
+
*/
|
|
83
|
+
protected NormalizeResponse(rawBody: unknown, responseDataKey: string | null): Record<string, unknown>[];
|
|
84
|
+
/**
|
|
85
|
+
* A bare array carries no next-page marker, so the only available signal is whether the page came
|
|
86
|
+
* back full. A short page is the last page; a full page means ask for another.
|
|
87
|
+
*
|
|
88
|
+
* That inference is safe against the one case it could loop on — an endpoint that silently ignores
|
|
89
|
+
* `page` and re-serves page one forever — because the base compares the first record of
|
|
90
|
+
* consecutive pages and stops on a repeat. This matters concretely: Everhour documents `page` on
|
|
91
|
+
* `/projects/{id}/tasks` and `/team/time` but NOT on `/projects`, where only `limit` is listed.
|
|
92
|
+
* The parameter does work there (the legacy driver paged projects in production for years), but
|
|
93
|
+
* the guarantee that a doc omission cannot become an infinite loop comes from that guard, not from
|
|
94
|
+
* the vendor.
|
|
95
|
+
*/
|
|
96
|
+
protected ExtractPaginationInfo(rawBody: unknown, paginationType: PaginationType, currentPage: number, _currentOffset: number, pageSize: number): PaginationState;
|
|
97
|
+
/**
|
|
98
|
+
* Everhour spells its page size `limit`, where the base's PageNumber case emits `pageSize`. An
|
|
99
|
+
* unknown parameter is not an error Everhour reports — it falls back to its own default page size,
|
|
100
|
+
* so the fetch would quietly run at the wrong width and, worse, `ExtractPaginationInfo` would then
|
|
101
|
+
* be comparing the returned count against a page size that was never requested and could end the
|
|
102
|
+
* object early. This override is the one place that mismatch is fixed.
|
|
103
|
+
*
|
|
104
|
+
* `limit` is also clamped to the documented per-endpoint maximum: Everhour rejects an over-large
|
|
105
|
+
* `limit` on tasks (250 max) rather than silently capping it.
|
|
106
|
+
*/
|
|
107
|
+
protected BuildPaginatedURL(basePath: string, obj: MJIntegrationObjectEntity, page: number, offset: number, cursor?: string, effectivePageSize?: number): string;
|
|
108
|
+
/**
|
|
109
|
+
* Adds the incremental date window, which is per-run rather than per-object and so cannot be
|
|
110
|
+
* declared metadata.
|
|
111
|
+
*
|
|
112
|
+
* `from` is deliberately backdated by a lookback window (default 7 days, overridable per tenant as
|
|
113
|
+
* `Configuration.lookbackDays`). A time record's `date` is the day the work happened, but the
|
|
114
|
+
* record itself stays editable — comments, durations and invoiced/locked flags change after the
|
|
115
|
+
* fact. Filtering strictly from the high-water mark would land those edits never. Re-reading a
|
|
116
|
+
* week of days costs nothing beyond the read: records upsert by id, and the engine's content-hash
|
|
117
|
+
* prefetch turns unchanged ones into zero writes.
|
|
118
|
+
*
|
|
119
|
+
* `to` is sent explicitly rather than left to Everhour's default, so the window is one this
|
|
120
|
+
* connector defined instead of one the vendor may redefine.
|
|
121
|
+
*/
|
|
122
|
+
protected AppendDefaultQueryParams(url: string, obj: MJIntegrationObjectEntity): string;
|
|
123
|
+
/** Today in UTC as `YYYY-MM-DD`. Isolated so tests can pin the clock. */
|
|
124
|
+
protected Today(): string;
|
|
125
|
+
/**
|
|
126
|
+
* Delegates the whole fetch to the base (pagination, parent iteration, batching) and adds only
|
|
127
|
+
* what the base has no way to know: the per-run date window, and the new watermark.
|
|
128
|
+
*
|
|
129
|
+
* The watermark advances to the maximum `date` actually observed, never to the wall clock. A
|
|
130
|
+
* clock-based watermark would claim coverage of days whose records had not been fetched yet when
|
|
131
|
+
* the run ended.
|
|
132
|
+
*/
|
|
133
|
+
FetchChanges(ctx: FetchContext): Promise<FetchBatchResult>;
|
|
134
|
+
/**
|
|
135
|
+
* Flattens Everhour's nested sub-objects onto the declared columns.
|
|
136
|
+
*
|
|
137
|
+
* Everhour returns compound values as objects (`billing: {type,fee}`, `estimate: {total,type}`,
|
|
138
|
+
* `task: {id,name,…}`) and open-ended ones as arrays or maps (`labels`, `users`, per-integration
|
|
139
|
+
* `attributes`/`metrics`). The sync engine maps a declared column only from a top-level key of the
|
|
140
|
+
* same name, so without this every one of those columns lands null while the run reports success.
|
|
141
|
+
* The base's applyTransformPreservingKeys keeps the original nested keys alongside these, so
|
|
142
|
+
* full-record custom-column capture still sees everything Everhour sent.
|
|
143
|
+
*
|
|
144
|
+
* The open-ended ones are serialized rather than declared: `attributes` and `metrics` are whatever
|
|
145
|
+
* the upstream integration defines per workspace, `userRateOverrides` is keyed by user id, and
|
|
146
|
+
* `labels`/`users` are unbounded. None can be a column in a fixed catalog, so each lands as JSON
|
|
147
|
+
* for downstream projection — an empty collection as null rather than "[]", so "no labels" and
|
|
148
|
+
* "not returned" read the same downstream.
|
|
149
|
+
*/
|
|
150
|
+
protected TransformRecord(raw: Record<string, unknown>, _obj: MJIntegrationObjectEntity, _fields: MJIntegrationObjectFieldEntity[]): Record<string, unknown>;
|
|
151
|
+
/**
|
|
152
|
+
* Probes `/users/me`. Unlike a workspace-scoped vendor there is no second thing to verify: the API
|
|
153
|
+
* key carries the team, so a key that authenticates is a key that can see the team's data. A 401
|
|
154
|
+
* is reported distinctly from any other failure because it is the only one the tenant can fix by
|
|
155
|
+
* re-entering a credential.
|
|
156
|
+
*/
|
|
157
|
+
TestConnection(companyIntegration: MJCompanyIntegrationEntity, contextUser: UserInfo): Promise<ConnectionTestResult>;
|
|
158
|
+
/**
|
|
159
|
+
* Resolves the API key from the linked Credential entity, falling back to the
|
|
160
|
+
* CompanyIntegration.Configuration JSON.
|
|
161
|
+
*
|
|
162
|
+
* CompanyIntegration.APIKey is deliberately NOT read — which is a change from the legacy driver,
|
|
163
|
+
* whose `connect()` took the key straight off that column. It is not a decrypt-on-read column, so
|
|
164
|
+
* a value written through mj-sync encryption comes back as the literal `$ENC$…` string and would
|
|
165
|
+
* be sent to Everhour verbatim, authenticating as nobody while looking configured.
|
|
166
|
+
*/
|
|
167
|
+
private LoadCredentials;
|
|
168
|
+
private LoadFromCredentialEntity;
|
|
169
|
+
/** Parses a credential/Configuration JSON blob, tolerating the usual casing/naming aliases. */
|
|
170
|
+
private ParseCredentialJson;
|
|
171
|
+
}
|
|
172
|
+
/** Auth context: Everhour's single API key, which carries the team scope by itself. */
|
|
173
|
+
interface EverhourAuthContext extends RESTAuthContext {
|
|
174
|
+
ApiKey: string;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Undoes percent-encoding of the colon.
|
|
178
|
+
*
|
|
179
|
+
* Exported for the tests that pin this behaviour: it is the difference between
|
|
180
|
+
* `/projects/as%3A123/tasks` and `/projects/as:123/tasks`, and every Everhour project id has a colon
|
|
181
|
+
* in it. Only `%3A`/`%3a` is touched — this is not a general URL decode, which would corrupt any
|
|
182
|
+
* legitimately encoded `&`, `=` or space in a query value.
|
|
183
|
+
*/
|
|
184
|
+
export declare function restoreColons(url: string): string;
|
|
185
|
+
/**
|
|
186
|
+
* Serializes an unbounded collection to JSON, or null when it is empty or absent.
|
|
187
|
+
*
|
|
188
|
+
* Empty collapses to null so that "the project has no assigned users" and "Everhour did not return
|
|
189
|
+
* users" are the same landed value — a literal `"[]"` would read as data downstream and would also
|
|
190
|
+
* change the record's content hash the first time the vendor started omitting an empty array.
|
|
191
|
+
*/
|
|
192
|
+
export declare function serializeCollection(value: unknown): string | null;
|
|
193
|
+
/**
|
|
194
|
+
* The source-platform prefix of an Everhour id (`as:1234` → `as`), or null when the id carries none.
|
|
195
|
+
*
|
|
196
|
+
* Deliberately narrow: two characters followed by a colon is the documented shape of every platform
|
|
197
|
+
* code Everhour publishes (`as`, `ev`, `b2`, `b3`, `pv`, `gh`, `in`, `tr`, `jr` — note two of them
|
|
198
|
+
* contain a digit, hence alphanumeric rather than alphabetic). Anything else is left alone rather than
|
|
199
|
+
* guessed at, so a future id format cannot silently produce a garbage platform value.
|
|
200
|
+
*/
|
|
201
|
+
export declare function platformFromID(id: unknown): string | null;
|
|
202
|
+
/**
|
|
203
|
+
* The `from` date for an incremental fetch: the watermark backdated by the lookback window, clamped
|
|
204
|
+
* so it can never land in the future.
|
|
205
|
+
*
|
|
206
|
+
* With no watermark yet — a first run — this returns the epoch bound rather than `today - lookback`,
|
|
207
|
+
* so the initial sync pulls the full history. Getting that backwards would make a first sync look
|
|
208
|
+
* complete while holding one week of data.
|
|
209
|
+
*/
|
|
210
|
+
export declare function incrementalFromDate(watermark: string | null, lookbackDays: number, today: string): string;
|
|
211
|
+
/**
|
|
212
|
+
* Reads `lookbackDays` out of an IntegrationObject's Configuration JSON, falling back to the default.
|
|
213
|
+
*
|
|
214
|
+
* A non-numeric, negative or absurd value falls back rather than throwing: this is tenant-editable
|
|
215
|
+
* configuration, and a typo in it should not take the object's sync down.
|
|
216
|
+
*/
|
|
217
|
+
export declare function parseLookbackDays(configuration: string | null): number;
|
|
218
|
+
/**
|
|
219
|
+
* The highest `date` across a batch, or null when the batch moves it nowhere.
|
|
220
|
+
*
|
|
221
|
+
* Compared as strings, which is correct for the fixed-width `YYYY-MM-DD` form Everhour emits. The
|
|
222
|
+
* previous watermark seeds the comparison so a batch containing only older records — which the
|
|
223
|
+
* lookback window guarantees on every incremental run — can never drag the high-water mark backwards.
|
|
224
|
+
*/
|
|
225
|
+
export declare function maxDate(records: ReadonlyArray<{
|
|
226
|
+
Fields: Record<string, unknown>;
|
|
227
|
+
}>, previous: string | null): string | null;
|
|
228
|
+
export {};
|
|
@@ -0,0 +1,543 @@
|
|
|
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
|
+
* Everhour time-tracking connector (REST API v1.2, read-only).
|
|
13
|
+
*
|
|
14
|
+
* ── Everhour ids carry a platform prefix, and that is the whole `as:` story ──
|
|
15
|
+
* An Everhour project id is literally `as:1234567890` (Asana-sourced), `jr:…` (Jira), `ev:…` (native),
|
|
16
|
+
* and so on — the prefix is part of the vendor's own identifier, documented in its Project schema, not
|
|
17
|
+
* an addressing convention a caller adds. The legacy AIDP driver looked like it was doing something
|
|
18
|
+
* exotic (`/projects/as:${externalId}/tasks`) only because it had *stripped* the prefix on the way in,
|
|
19
|
+
* with `id.slice(3)`, to make the remainder match an Asana gid — then had to put it back to make the
|
|
20
|
+
* next call. This connector keeps every id exactly as Everhour issued it, so the prefix never has to
|
|
21
|
+
* be reconstructed and the connector never has to know that some Everhour workspaces are backed by
|
|
22
|
+
* Asana. Nothing here is Asana-aware.
|
|
23
|
+
*
|
|
24
|
+
* ── Vendor units are preserved, not converted ──
|
|
25
|
+
* Everhour reports money in cents and durations in seconds. The legacy driver divided fees, rates and
|
|
26
|
+
* budgets by 100 on the way into AIDP's schema; that is a presentation choice belonging to whoever
|
|
27
|
+
* consumes the data, and doing it here would make the landed value disagree with what the API returned
|
|
28
|
+
* and with what Everhour's own UI reports. Every amount lands in the vendor's unit and every field
|
|
29
|
+
* description names that unit.
|
|
30
|
+
*
|
|
31
|
+
* ── Time records come from the team-wide door, not per project ──
|
|
32
|
+
* The legacy driver read time one project at a time (`/projects/{id}/time`), which is an N+1 over the
|
|
33
|
+
* project list and, at Everhour's ~20 requests / 10 seconds, the dominant cost of a run. Everhour also
|
|
34
|
+
* publishes `/team/time?from=&to=`, which returns the same records for the whole team in one paged
|
|
35
|
+
* stream. TimeRecords therefore has no parent door at all, and `from`/`to` gives the incremental
|
|
36
|
+
* filter directly.
|
|
37
|
+
*
|
|
38
|
+
* ── Tasks is a templated child door ──
|
|
39
|
+
* Everhour exposes no unfiltered team-wide task listing (`/tasks/search` requires a search term), so
|
|
40
|
+
* tasks are addressable only per project. Tasks declares `Configuration.parentObjectName: "Projects"`
|
|
41
|
+
* so the engine iterates the already-synced projects; without that declaration it would fetch zero
|
|
42
|
+
* rows and the run would still report success.
|
|
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 EverhourConnector = class EverhourConnector 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 the `from`/`to` window. Cleared on the way out so a non-incremental object can never
|
|
54
|
+
* inherit the previous object's filter.
|
|
55
|
+
*/
|
|
56
|
+
this.currentWatermark = null;
|
|
57
|
+
}
|
|
58
|
+
/** Verbatim three-way invariant name: ClassName / IntegrationName getter / MJ: Integrations.Name. */
|
|
59
|
+
get IntegrationName() {
|
|
60
|
+
return 'Everhour';
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The only incremental object is TimeRecords, watermarked on `date`, and the watermark only ever
|
|
64
|
+
* advances to the maximum date actually observed. Records are never re-dated backwards by Everhour
|
|
65
|
+
* — an edit changes `time`/`comment`, not which day the work happened — so the high-water mark is
|
|
66
|
+
* monotonic even though the records behind it are mutable. The lookback window below is what
|
|
67
|
+
* covers those mutations.
|
|
68
|
+
*/
|
|
69
|
+
get MonotonicWatermark() {
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Everhour's list endpoints document no ordering guarantee and offer no sort parameter, so there
|
|
74
|
+
* is no key a keyset resume could resume against. Paging is `page`/`limit`.
|
|
75
|
+
*/
|
|
76
|
+
StableOrderingKey(_objectName) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
// ─── Auth + transport (BaseRESTIntegrationConnector abstracts) ────
|
|
80
|
+
/**
|
|
81
|
+
* Resolves the API key. Everhour has no separate tenant identifier: the key *is* the team scope,
|
|
82
|
+
* which is why nothing here reads ExternalSystemID and why there is no workspace parameter on any
|
|
83
|
+
* request.
|
|
84
|
+
*/
|
|
85
|
+
async Authenticate(companyIntegration, contextUser) {
|
|
86
|
+
const apiKey = await this.LoadCredentials(companyIntegration, contextUser);
|
|
87
|
+
return { ApiKey: apiKey };
|
|
88
|
+
}
|
|
89
|
+
BuildHeaders(auth) {
|
|
90
|
+
return {
|
|
91
|
+
'X-Api-Key': auth.ApiKey,
|
|
92
|
+
'Accept': 'application/json',
|
|
93
|
+
// Everhour describes its API as BETA and, absent this header, serves whatever version is
|
|
94
|
+
// newest — so an unannounced vendor release could reshape responses under a catalog that
|
|
95
|
+
// was validated against 1.2. Pinning turns that from a silent shape change into a
|
|
96
|
+
// deliberate, reviewable version bump here.
|
|
97
|
+
'X-Accept-Version': EVERHOUR_API_VERSION,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
GetBaseURL(_companyIntegration, _auth) {
|
|
101
|
+
return EVERHOUR_API_BASE;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Plain fetch, with one deliberate URL normalization: `%3A` is decoded back to `:`.
|
|
105
|
+
*
|
|
106
|
+
* Every Everhour project and task id contains a colon (`as:1234567890`), and the base substitutes
|
|
107
|
+
* template variables with encodeURIComponent, which turns the id in `/projects/{project_id}/tasks`
|
|
108
|
+
* into `as%3A1234567890`. That substitution helper is private, so the path cannot be fixed where it
|
|
109
|
+
* is built — this is the one seam that sees the final URL. A colon is a legal character in both a
|
|
110
|
+
* path segment and a query value under RFC 3986, so decoding it is safe everywhere in the URL, and
|
|
111
|
+
* it removes any dependence on Everhour's router happening to percent-decode before matching.
|
|
112
|
+
*/
|
|
113
|
+
async MakeHTTPRequest(_auth, url, method, headers, body) {
|
|
114
|
+
const init = { method, headers };
|
|
115
|
+
if (body !== undefined && method !== 'GET' && method !== 'HEAD') {
|
|
116
|
+
init.body = typeof body === 'string' ? body : JSON.stringify(body);
|
|
117
|
+
init.headers['Content-Type'] = 'application/json';
|
|
118
|
+
}
|
|
119
|
+
const response = await fetch(restoreColons(url), init);
|
|
120
|
+
const responseHeaders = {};
|
|
121
|
+
response.headers.forEach((value, key) => { responseHeaders[key.toLowerCase()] = value; });
|
|
122
|
+
const text = await response.text();
|
|
123
|
+
let parsed = text;
|
|
124
|
+
const contentType = responseHeaders['content-type'] ?? '';
|
|
125
|
+
if (contentType.includes('json') || (text.length > 0 && (text[0] === '{' || text[0] === '['))) {
|
|
126
|
+
try {
|
|
127
|
+
parsed = JSON.parse(text);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
parsed = text;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return { Status: response.status, Body: parsed, Headers: responseHeaders };
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Everhour returns bare JSON arrays with no envelope — there is no data key to unwrap, which is
|
|
137
|
+
* why every object declares an empty ResponseDataKey.
|
|
138
|
+
*/
|
|
139
|
+
NormalizeResponse(rawBody, responseDataKey) {
|
|
140
|
+
if (responseDataKey && isRecord(rawBody)) {
|
|
141
|
+
const inner = rawBody[responseDataKey];
|
|
142
|
+
if (Array.isArray(inner))
|
|
143
|
+
return inner.filter(isRecord);
|
|
144
|
+
if (isRecord(inner))
|
|
145
|
+
return [inner];
|
|
146
|
+
}
|
|
147
|
+
if (Array.isArray(rawBody))
|
|
148
|
+
return rawBody.filter(isRecord);
|
|
149
|
+
if (isRecord(rawBody))
|
|
150
|
+
return [rawBody];
|
|
151
|
+
return [];
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* A bare array carries no next-page marker, so the only available signal is whether the page came
|
|
155
|
+
* back full. A short page is the last page; a full page means ask for another.
|
|
156
|
+
*
|
|
157
|
+
* That inference is safe against the one case it could loop on — an endpoint that silently ignores
|
|
158
|
+
* `page` and re-serves page one forever — because the base compares the first record of
|
|
159
|
+
* consecutive pages and stops on a repeat. This matters concretely: Everhour documents `page` on
|
|
160
|
+
* `/projects/{id}/tasks` and `/team/time` but NOT on `/projects`, where only `limit` is listed.
|
|
161
|
+
* The parameter does work there (the legacy driver paged projects in production for years), but
|
|
162
|
+
* the guarantee that a doc omission cannot become an infinite loop comes from that guard, not from
|
|
163
|
+
* the vendor.
|
|
164
|
+
*/
|
|
165
|
+
ExtractPaginationInfo(rawBody, paginationType, currentPage, _currentOffset, pageSize) {
|
|
166
|
+
if (paginationType !== 'PageNumber')
|
|
167
|
+
return { HasMore: false };
|
|
168
|
+
const count = Array.isArray(rawBody) ? rawBody.length : 0;
|
|
169
|
+
if (pageSize > 0 && count >= pageSize) {
|
|
170
|
+
return { HasMore: true, NextPage: currentPage + 1 };
|
|
171
|
+
}
|
|
172
|
+
return { HasMore: false };
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Everhour spells its page size `limit`, where the base's PageNumber case emits `pageSize`. An
|
|
176
|
+
* unknown parameter is not an error Everhour reports — it falls back to its own default page size,
|
|
177
|
+
* so the fetch would quietly run at the wrong width and, worse, `ExtractPaginationInfo` would then
|
|
178
|
+
* be comparing the returned count against a page size that was never requested and could end the
|
|
179
|
+
* object early. This override is the one place that mismatch is fixed.
|
|
180
|
+
*
|
|
181
|
+
* `limit` is also clamped to the documented per-endpoint maximum: Everhour rejects an over-large
|
|
182
|
+
* `limit` on tasks (250 max) rather than silently capping it.
|
|
183
|
+
*/
|
|
184
|
+
BuildPaginatedURL(basePath, obj, page, offset, cursor, effectivePageSize) {
|
|
185
|
+
if (obj.PaginationType !== 'PageNumber') {
|
|
186
|
+
return super.BuildPaginatedURL(basePath, obj, page, offset, cursor, effectivePageSize);
|
|
187
|
+
}
|
|
188
|
+
const max = MAX_PAGE_SIZE_BY_OBJECT[obj.Name] ?? DEFAULT_MAX_PAGE_SIZE;
|
|
189
|
+
const requested = effectivePageSize ?? obj.DefaultPageSize ?? max;
|
|
190
|
+
const limit = Math.min(max, Math.max(1, requested));
|
|
191
|
+
const separator = basePath.includes('?') ? '&' : '?';
|
|
192
|
+
return `${basePath}${separator}page=${page}&limit=${limit}`;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Adds the incremental date window, which is per-run rather than per-object and so cannot be
|
|
196
|
+
* declared metadata.
|
|
197
|
+
*
|
|
198
|
+
* `from` is deliberately backdated by a lookback window (default 7 days, overridable per tenant as
|
|
199
|
+
* `Configuration.lookbackDays`). A time record's `date` is the day the work happened, but the
|
|
200
|
+
* record itself stays editable — comments, durations and invoiced/locked flags change after the
|
|
201
|
+
* fact. Filtering strictly from the high-water mark would land those edits never. Re-reading a
|
|
202
|
+
* week of days costs nothing beyond the read: records upsert by id, and the engine's content-hash
|
|
203
|
+
* prefetch turns unchanged ones into zero writes.
|
|
204
|
+
*
|
|
205
|
+
* `to` is sent explicitly rather than left to Everhour's default, so the window is one this
|
|
206
|
+
* connector defined instead of one the vendor may redefine.
|
|
207
|
+
*/
|
|
208
|
+
AppendDefaultQueryParams(url, obj) {
|
|
209
|
+
let out = super.AppendDefaultQueryParams(url, obj);
|
|
210
|
+
if (obj.SupportsIncrementalSync) {
|
|
211
|
+
const today = this.Today();
|
|
212
|
+
const lookbackDays = parseLookbackDays(obj.Configuration);
|
|
213
|
+
out = appendParam(out, 'from', incrementalFromDate(this.currentWatermark, lookbackDays, today));
|
|
214
|
+
out = appendParam(out, 'to', today);
|
|
215
|
+
}
|
|
216
|
+
return out;
|
|
217
|
+
}
|
|
218
|
+
/** Today in UTC as `YYYY-MM-DD`. Isolated so tests can pin the clock. */
|
|
219
|
+
Today() {
|
|
220
|
+
return new Date().toISOString().slice(0, 10);
|
|
221
|
+
}
|
|
222
|
+
// ─── Fetch ───────────────────────────────────────────────────────
|
|
223
|
+
/**
|
|
224
|
+
* Delegates the whole fetch to the base (pagination, parent iteration, batching) and adds only
|
|
225
|
+
* what the base has no way to know: the per-run date window, and the new watermark.
|
|
226
|
+
*
|
|
227
|
+
* The watermark advances to the maximum `date` actually observed, never to the wall clock. A
|
|
228
|
+
* clock-based watermark would claim coverage of days whose records had not been fetched yet when
|
|
229
|
+
* the run ended.
|
|
230
|
+
*/
|
|
231
|
+
async FetchChanges(ctx) {
|
|
232
|
+
this.currentWatermark = ctx.WatermarkValue;
|
|
233
|
+
try {
|
|
234
|
+
const result = await super.FetchChanges(ctx);
|
|
235
|
+
const newWatermark = maxDate(result.Records, ctx.WatermarkValue);
|
|
236
|
+
return newWatermark ? { ...result, NewWatermarkValue: newWatermark } : result;
|
|
237
|
+
}
|
|
238
|
+
finally {
|
|
239
|
+
this.currentWatermark = null;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Flattens Everhour's nested sub-objects onto the declared columns.
|
|
244
|
+
*
|
|
245
|
+
* Everhour returns compound values as objects (`billing: {type,fee}`, `estimate: {total,type}`,
|
|
246
|
+
* `task: {id,name,…}`) and open-ended ones as arrays or maps (`labels`, `users`, per-integration
|
|
247
|
+
* `attributes`/`metrics`). The sync engine maps a declared column only from a top-level key of the
|
|
248
|
+
* same name, so without this every one of those columns lands null while the run reports success.
|
|
249
|
+
* The base's applyTransformPreservingKeys keeps the original nested keys alongside these, so
|
|
250
|
+
* full-record custom-column capture still sees everything Everhour sent.
|
|
251
|
+
*
|
|
252
|
+
* The open-ended ones are serialized rather than declared: `attributes` and `metrics` are whatever
|
|
253
|
+
* the upstream integration defines per workspace, `userRateOverrides` is keyed by user id, and
|
|
254
|
+
* `labels`/`users` are unbounded. None can be a column in a fixed catalog, so each lands as JSON
|
|
255
|
+
* for downstream projection — an empty collection as null rather than "[]", so "no labels" and
|
|
256
|
+
* "not returned" read the same downstream.
|
|
257
|
+
*/
|
|
258
|
+
TransformRecord(raw, _obj, _fields) {
|
|
259
|
+
const out = { ...raw };
|
|
260
|
+
for (const [parent, key, target] of NESTED_SCALARS) {
|
|
261
|
+
const value = raw[parent];
|
|
262
|
+
if (isRecord(value))
|
|
263
|
+
out[target] = value[key] ?? null;
|
|
264
|
+
else if (value === null)
|
|
265
|
+
out[target] = null;
|
|
266
|
+
}
|
|
267
|
+
for (const [source, target] of JSON_COLLECTIONS) {
|
|
268
|
+
if (source in raw)
|
|
269
|
+
out[target] = serializeCollection(raw[source]);
|
|
270
|
+
}
|
|
271
|
+
// The id prefix is the vendor's source-platform discriminator (`as` Asana, `jr` Jira, `ev`
|
|
272
|
+
// native Everhour, …) and is the same value the `platform` filter on /projects accepts. It is
|
|
273
|
+
// promoted to its own column because filtering landed rows by source is otherwise a substring
|
|
274
|
+
// match on the primary key.
|
|
275
|
+
const platform = platformFromID(raw['id']);
|
|
276
|
+
if (platform !== null)
|
|
277
|
+
out['platform'] = platform;
|
|
278
|
+
return out;
|
|
279
|
+
}
|
|
280
|
+
// ─── Connection test ─────────────────────────────────────────────
|
|
281
|
+
/**
|
|
282
|
+
* Probes `/users/me`. Unlike a workspace-scoped vendor there is no second thing to verify: the API
|
|
283
|
+
* key carries the team, so a key that authenticates is a key that can see the team's data. A 401
|
|
284
|
+
* is reported distinctly from any other failure because it is the only one the tenant can fix by
|
|
285
|
+
* re-entering a credential.
|
|
286
|
+
*/
|
|
287
|
+
async TestConnection(companyIntegration, contextUser) {
|
|
288
|
+
try {
|
|
289
|
+
const auth = await this.Authenticate(companyIntegration, contextUser);
|
|
290
|
+
const headers = this.BuildHeaders(auth);
|
|
291
|
+
const me = await this.MakeHTTPRequest(auth, `${EVERHOUR_API_BASE}/users/me`, 'GET', headers);
|
|
292
|
+
if (me.Status === 401) {
|
|
293
|
+
return { Success: false, Message: 'Everhour rejected the API key (HTTP 401).' };
|
|
294
|
+
}
|
|
295
|
+
if (me.Status >= 400) {
|
|
296
|
+
return { Success: false, Message: `Everhour /users/me returned HTTP ${me.Status}.` };
|
|
297
|
+
}
|
|
298
|
+
const body = isRecord(me.Body) ? me.Body : {};
|
|
299
|
+
const who = stringOrNull(body['name'])
|
|
300
|
+
?? stringOrNull(body['email'])
|
|
301
|
+
?? (body['id'] != null ? String(body['id']) : 'unknown user');
|
|
302
|
+
return {
|
|
303
|
+
Success: true,
|
|
304
|
+
Message: `Connected to Everhour as ${who}.`,
|
|
305
|
+
ServerVersion: EVERHOUR_API_VERSION,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
catch (err) {
|
|
309
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
310
|
+
return { Success: false, Message: `Everhour connection error: ${message}` };
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
// ─── Credential resolution ───────────────────────────────────────
|
|
314
|
+
/**
|
|
315
|
+
* Resolves the API key from the linked Credential entity, falling back to the
|
|
316
|
+
* CompanyIntegration.Configuration JSON.
|
|
317
|
+
*
|
|
318
|
+
* CompanyIntegration.APIKey is deliberately NOT read — which is a change from the legacy driver,
|
|
319
|
+
* whose `connect()` took the key straight off that column. It is not a decrypt-on-read column, so
|
|
320
|
+
* a value written through mj-sync encryption comes back as the literal `$ENC$…` string and would
|
|
321
|
+
* be sent to Everhour verbatim, authenticating as nobody while looking configured.
|
|
322
|
+
*/
|
|
323
|
+
async LoadCredentials(companyIntegration, contextUser) {
|
|
324
|
+
let apiKey;
|
|
325
|
+
if (companyIntegration.CredentialID) {
|
|
326
|
+
apiKey = await this.LoadFromCredentialEntity(companyIntegration.CredentialID, contextUser) ?? undefined;
|
|
327
|
+
}
|
|
328
|
+
if (!apiKey && companyIntegration.Configuration) {
|
|
329
|
+
apiKey = this.ParseCredentialJson(companyIntegration.Configuration) ?? undefined;
|
|
330
|
+
}
|
|
331
|
+
if (!apiKey) {
|
|
332
|
+
throw new Error('No Everhour credential found — link an "API Key" credential holding the Everhour API ' +
|
|
333
|
+
'key, or supply one as "apiKey" in the CompanyIntegration.Configuration JSON.');
|
|
334
|
+
}
|
|
335
|
+
return apiKey;
|
|
336
|
+
}
|
|
337
|
+
async LoadFromCredentialEntity(credentialID, contextUser, provider) {
|
|
338
|
+
const md = provider ?? new Metadata();
|
|
339
|
+
const credential = await md.GetEntityObject('MJ: Credentials', contextUser);
|
|
340
|
+
const loaded = await credential.Load(credentialID);
|
|
341
|
+
if (!loaded || !credential.Values)
|
|
342
|
+
return null;
|
|
343
|
+
return this.ParseCredentialJson(credential.Values);
|
|
344
|
+
}
|
|
345
|
+
/** Parses a credential/Configuration JSON blob, tolerating the usual casing/naming aliases. */
|
|
346
|
+
ParseCredentialJson(json) {
|
|
347
|
+
try {
|
|
348
|
+
const result = EverhourCredentialSchema.safeParse(JSON.parse(json));
|
|
349
|
+
if (!result.success)
|
|
350
|
+
return null;
|
|
351
|
+
const p = result.data;
|
|
352
|
+
const key = p.apiKey ?? p.ApiKey ?? p.APIKey ?? p.key ?? p.Token ?? p.token;
|
|
353
|
+
return key != null && String(key).length > 0 ? String(key) : null;
|
|
354
|
+
}
|
|
355
|
+
catch {
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
EverhourConnector = __decorate([
|
|
361
|
+
RegisterClass(BaseIntegrationConnector, '@memberjunction/connector-everhour'),
|
|
362
|
+
RegisterClass(BaseIntegrationConnector, 'EverhourConnector')
|
|
363
|
+
], EverhourConnector);
|
|
364
|
+
export { EverhourConnector };
|
|
365
|
+
// ─── Module-level constants, types + helpers (mechanism, NOT a catalog) ───
|
|
366
|
+
/** Everhour's REST base. Single-tenant SaaS with no per-customer host, so there is nothing to configure. */
|
|
367
|
+
const EVERHOUR_API_BASE = 'https://api.everhour.com';
|
|
368
|
+
/** The API version this catalog was validated against, pinned via X-Accept-Version. */
|
|
369
|
+
const EVERHOUR_API_VERSION = '1.2';
|
|
370
|
+
/**
|
|
371
|
+
* Documented per-endpoint `limit` maxima. Tasks is the only one Everhour states outright ("250 max");
|
|
372
|
+
* the others use the value the docs show, which is the conservative reading. Time records accept up to
|
|
373
|
+
* 50000, far above any batch this connector would ask for, so the lower cap costs nothing and keeps a
|
|
374
|
+
* runaway page size from turning into a multi-megabyte response.
|
|
375
|
+
*/
|
|
376
|
+
const MAX_PAGE_SIZE_BY_OBJECT = {
|
|
377
|
+
Projects: 100,
|
|
378
|
+
Tasks: 250,
|
|
379
|
+
TimeRecords: 1000,
|
|
380
|
+
};
|
|
381
|
+
const DEFAULT_MAX_PAGE_SIZE = 100;
|
|
382
|
+
/** `[nested object, key within it, flat column]` — Everhour's compound scalars. */
|
|
383
|
+
const NESTED_SCALARS = [
|
|
384
|
+
['billing', 'type', 'billing_type'],
|
|
385
|
+
['billing', 'fee', 'billing_fee'],
|
|
386
|
+
['rate', 'type', 'rate_type'],
|
|
387
|
+
['rate', 'rate', 'rate_rate'],
|
|
388
|
+
['budget', 'type', 'budget_type'],
|
|
389
|
+
['budget', 'budget', 'budget_budget'],
|
|
390
|
+
['budget', 'period', 'budget_period'],
|
|
391
|
+
['budget', 'progress', 'budget_progress'],
|
|
392
|
+
['budget', 'timeProgress', 'budget_time_progress'],
|
|
393
|
+
['budget', 'expenseProgress', 'budget_expense_progress'],
|
|
394
|
+
['budget', 'appliedFrom', 'budget_applied_from'],
|
|
395
|
+
['budget', 'threshold', 'budget_threshold'],
|
|
396
|
+
['budget', 'disallowOverbudget', 'budget_disallow_overbudget'],
|
|
397
|
+
['budget', 'excludeUnbillableTime', 'budget_exclude_unbillable_time'],
|
|
398
|
+
['budget', 'excludeExpenses', 'budget_exclude_expenses'],
|
|
399
|
+
['estimate', 'total', 'estimate_total'],
|
|
400
|
+
['estimate', 'type', 'estimate_type'],
|
|
401
|
+
['time', 'total', 'time_total'],
|
|
402
|
+
['task', 'id', 'task_id'],
|
|
403
|
+
['task', 'name', 'task_name'],
|
|
404
|
+
];
|
|
405
|
+
/** Unbounded / workspace-defined collections → the JSON column each is serialized onto. */
|
|
406
|
+
const JSON_COLLECTIONS = [
|
|
407
|
+
['users', 'users_json'],
|
|
408
|
+
['projects', 'project_ids_json'],
|
|
409
|
+
['labels', 'labels_json'],
|
|
410
|
+
['attributes', 'attributes_json'],
|
|
411
|
+
['metrics', 'metrics_json'],
|
|
412
|
+
['history', 'history_json'],
|
|
413
|
+
];
|
|
414
|
+
const EverhourCredentialSchema = z.object({
|
|
415
|
+
apiKey: z.string().optional(),
|
|
416
|
+
ApiKey: z.string().optional(),
|
|
417
|
+
APIKey: z.string().optional(),
|
|
418
|
+
key: z.string().optional(),
|
|
419
|
+
Token: z.string().optional(),
|
|
420
|
+
token: z.string().optional(),
|
|
421
|
+
}).passthrough();
|
|
422
|
+
function isRecord(v) {
|
|
423
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
424
|
+
}
|
|
425
|
+
function stringOrNull(v) {
|
|
426
|
+
return typeof v === 'string' && v.length > 0 ? v : null;
|
|
427
|
+
}
|
|
428
|
+
function appendParam(url, key, value) {
|
|
429
|
+
const separator = url.includes('?') ? '&' : '?';
|
|
430
|
+
return `${url}${separator}${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Undoes percent-encoding of the colon.
|
|
434
|
+
*
|
|
435
|
+
* Exported for the tests that pin this behaviour: it is the difference between
|
|
436
|
+
* `/projects/as%3A123/tasks` and `/projects/as:123/tasks`, and every Everhour project id has a colon
|
|
437
|
+
* in it. Only `%3A`/`%3a` is touched — this is not a general URL decode, which would corrupt any
|
|
438
|
+
* legitimately encoded `&`, `=` or space in a query value.
|
|
439
|
+
*/
|
|
440
|
+
export function restoreColons(url) {
|
|
441
|
+
return url.replace(/%3A/gi, ':');
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Serializes an unbounded collection to JSON, or null when it is empty or absent.
|
|
445
|
+
*
|
|
446
|
+
* Empty collapses to null so that "the project has no assigned users" and "Everhour did not return
|
|
447
|
+
* users" are the same landed value — a literal `"[]"` would read as data downstream and would also
|
|
448
|
+
* change the record's content hash the first time the vendor started omitting an empty array.
|
|
449
|
+
*/
|
|
450
|
+
export function serializeCollection(value) {
|
|
451
|
+
if (value == null)
|
|
452
|
+
return null;
|
|
453
|
+
if (Array.isArray(value))
|
|
454
|
+
return value.length > 0 ? JSON.stringify(value) : null;
|
|
455
|
+
if (typeof value === 'object') {
|
|
456
|
+
return Object.keys(value).length > 0 ? JSON.stringify(value) : null;
|
|
457
|
+
}
|
|
458
|
+
return null;
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* The source-platform prefix of an Everhour id (`as:1234` → `as`), or null when the id carries none.
|
|
462
|
+
*
|
|
463
|
+
* Deliberately narrow: two characters followed by a colon is the documented shape of every platform
|
|
464
|
+
* code Everhour publishes (`as`, `ev`, `b2`, `b3`, `pv`, `gh`, `in`, `tr`, `jr` — note two of them
|
|
465
|
+
* contain a digit, hence alphanumeric rather than alphabetic). Anything else is left alone rather than
|
|
466
|
+
* guessed at, so a future id format cannot silently produce a garbage platform value.
|
|
467
|
+
*/
|
|
468
|
+
export function platformFromID(id) {
|
|
469
|
+
if (typeof id !== 'string')
|
|
470
|
+
return null;
|
|
471
|
+
const match = /^([a-z][a-z0-9]):/.exec(id);
|
|
472
|
+
return match ? match[1] : null;
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* The start date for a first, unwatermarked sync. Everhour launched well after this, and its API
|
|
476
|
+
* rejects no date this old, so it is "everything" expressed as a bound the vendor will accept —
|
|
477
|
+
* `from` is not optional in the way `to` is, and omitting it lets Everhour choose the window instead.
|
|
478
|
+
*/
|
|
479
|
+
const EVERHOUR_EPOCH_DATE = '2010-01-01';
|
|
480
|
+
/** Default days of already-synced history to re-read on each incremental run. */
|
|
481
|
+
const DEFAULT_LOOKBACK_DAYS = 7;
|
|
482
|
+
/** A lookback beyond this is indistinguishable from a full re-sync; treat it as a misconfiguration. */
|
|
483
|
+
const MAX_LOOKBACK_DAYS = 3650;
|
|
484
|
+
/**
|
|
485
|
+
* The `from` date for an incremental fetch: the watermark backdated by the lookback window, clamped
|
|
486
|
+
* so it can never land in the future.
|
|
487
|
+
*
|
|
488
|
+
* With no watermark yet — a first run — this returns the epoch bound rather than `today - lookback`,
|
|
489
|
+
* so the initial sync pulls the full history. Getting that backwards would make a first sync look
|
|
490
|
+
* complete while holding one week of data.
|
|
491
|
+
*/
|
|
492
|
+
export function incrementalFromDate(watermark, lookbackDays, today) {
|
|
493
|
+
if (!watermark)
|
|
494
|
+
return EVERHOUR_EPOCH_DATE;
|
|
495
|
+
const base = Date.parse(`${watermark.slice(0, 10)}T00:00:00Z`);
|
|
496
|
+
if (Number.isNaN(base))
|
|
497
|
+
return EVERHOUR_EPOCH_DATE;
|
|
498
|
+
const shifted = new Date(base - lookbackDays * 86_400_000).toISOString().slice(0, 10);
|
|
499
|
+
return shifted > today ? today : shifted;
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Reads `lookbackDays` out of an IntegrationObject's Configuration JSON, falling back to the default.
|
|
503
|
+
*
|
|
504
|
+
* A non-numeric, negative or absurd value falls back rather than throwing: this is tenant-editable
|
|
505
|
+
* configuration, and a typo in it should not take the object's sync down.
|
|
506
|
+
*/
|
|
507
|
+
export function parseLookbackDays(configuration) {
|
|
508
|
+
if (!configuration)
|
|
509
|
+
return DEFAULT_LOOKBACK_DAYS;
|
|
510
|
+
try {
|
|
511
|
+
const parsed = JSON.parse(configuration);
|
|
512
|
+
if (!isRecord(parsed))
|
|
513
|
+
return DEFAULT_LOOKBACK_DAYS;
|
|
514
|
+
const value = parsed['lookbackDays'];
|
|
515
|
+
if (typeof value !== 'number' || !Number.isFinite(value))
|
|
516
|
+
return DEFAULT_LOOKBACK_DAYS;
|
|
517
|
+
if (value < 0 || value > MAX_LOOKBACK_DAYS)
|
|
518
|
+
return DEFAULT_LOOKBACK_DAYS;
|
|
519
|
+
return Math.floor(value);
|
|
520
|
+
}
|
|
521
|
+
catch {
|
|
522
|
+
return DEFAULT_LOOKBACK_DAYS;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* The highest `date` across a batch, or null when the batch moves it nowhere.
|
|
527
|
+
*
|
|
528
|
+
* Compared as strings, which is correct for the fixed-width `YYYY-MM-DD` form Everhour emits. The
|
|
529
|
+
* previous watermark seeds the comparison so a batch containing only older records — which the
|
|
530
|
+
* lookback window guarantees on every incremental run — can never drag the high-water mark backwards.
|
|
531
|
+
*/
|
|
532
|
+
export function maxDate(records, previous) {
|
|
533
|
+
let best = previous;
|
|
534
|
+
for (const record of records) {
|
|
535
|
+
const value = record.Fields['date'];
|
|
536
|
+
if (typeof value !== 'string' || value.length === 0)
|
|
537
|
+
continue;
|
|
538
|
+
if (best === null || value > best)
|
|
539
|
+
best = value;
|
|
540
|
+
}
|
|
541
|
+
return best === previous ? null : best;
|
|
542
|
+
}
|
|
543
|
+
//# sourceMappingURL=EverhourConnector.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EverhourConnector.js","sourceRoot":"","sources":["../src/EverhourConnector.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export * from './EverhourConnector.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 './EverhourConnector.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,wBAAwB,CAAC;AAEvC;oGACoG;AACpG,MAAM,UAAU,iBAAiB,KAAiD,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@memberjunction/connector-everhour",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "MemberJunction Everhour 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
|
+
}
|