@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.
@@ -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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"}
@@ -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
+ }