@typeb-digital/nucleus-sdk 0.5.3 → 0.8.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/CHANGELOG.md +107 -0
- package/README.md +137 -0
- package/dist/cjs/index.cjs +136 -107
- package/dist/cjs/index.d.cts +218 -9
- package/dist/cjs/index.d.cts.map +1 -1
- package/dist/es/index.d.ts +218 -9
- package/dist/es/index.d.ts.map +1 -1
- package/dist/es/index.js +134 -108
- package/package.json +1 -1
package/dist/es/index.js
CHANGED
|
@@ -8,14 +8,23 @@ function statusToCode(status, apiCode) {
|
|
|
8
8
|
if (status === 429) return 'RATE_LIMITED';
|
|
9
9
|
return 'FORBIDDEN';
|
|
10
10
|
}
|
|
11
|
-
function makeErrorResult(code, message) {
|
|
11
|
+
function makeErrorResult(code, message, apiCode) {
|
|
12
12
|
return {
|
|
13
13
|
error: {
|
|
14
14
|
code,
|
|
15
|
-
message
|
|
15
|
+
message,
|
|
16
|
+
...apiCode ? {
|
|
17
|
+
apiCode
|
|
18
|
+
} : {}
|
|
16
19
|
}
|
|
17
20
|
};
|
|
18
21
|
}
|
|
22
|
+
function stripUndefined(query) {
|
|
23
|
+
if (!query) return undefined;
|
|
24
|
+
return Object.fromEntries(Object.entries(query).filter((entry)=>{
|
|
25
|
+
return entry[1] !== undefined;
|
|
26
|
+
}));
|
|
27
|
+
}
|
|
19
28
|
class NucleusTransport {
|
|
20
29
|
constructor(token, baseUrl){
|
|
21
30
|
this.bodylessHeaders = {
|
|
@@ -27,117 +36,78 @@ class NucleusTransport {
|
|
|
27
36
|
};
|
|
28
37
|
this.baseUrl = (baseUrl ?? DEFAULT_BASE_URL$1).replace(/\/$/, '');
|
|
29
38
|
}
|
|
30
|
-
|
|
39
|
+
/**
|
|
40
|
+
* The one place a request is issued and a failure is interpreted. Every verb below
|
|
41
|
+
* funnels through here: when the error mapping lived in each verb, a fix to it had to
|
|
42
|
+
* be made five times, and one of them (reading the API's `code`) was wrong for years.
|
|
43
|
+
*/ async request(method, path, opts, onSuccess) {
|
|
31
44
|
try {
|
|
32
|
-
const params = query ? Object.fromEntries(Object.entries(query).filter(([, v])=>v !== undefined)) : undefined;
|
|
33
45
|
const envelope = await $fetch(`${this.baseUrl}${path}`, {
|
|
34
|
-
method
|
|
35
|
-
headers: this.bodylessHeaders,
|
|
36
|
-
|
|
46
|
+
method,
|
|
47
|
+
headers: opts.hasBody ? this.headers : this.bodylessHeaders,
|
|
48
|
+
...opts.hasBody ? {
|
|
49
|
+
body: JSON.stringify(opts.body)
|
|
50
|
+
} : {},
|
|
51
|
+
params: stripUndefined(opts.query)
|
|
37
52
|
});
|
|
53
|
+
// A 2xx carrying `success: false` shouldn't happen, but the envelope allows it.
|
|
38
54
|
if (!envelope.success) {
|
|
39
|
-
return makeErrorResult(
|
|
55
|
+
return makeErrorResult(statusToCode(0, envelope.code), envelope.error, envelope.code);
|
|
40
56
|
}
|
|
41
|
-
return
|
|
42
|
-
data: envelope.data
|
|
43
|
-
};
|
|
57
|
+
return onSuccess(envelope);
|
|
44
58
|
} catch (err) {
|
|
45
59
|
if (err instanceof FetchError) {
|
|
46
60
|
const body = err.data;
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
return makeErrorResult('NETWORK_ERROR', String(err));
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
async post(path, body) {
|
|
54
|
-
try {
|
|
55
|
-
const envelope = await $fetch(`${this.baseUrl}${path}`, {
|
|
56
|
-
method: 'POST',
|
|
57
|
-
headers: this.headers,
|
|
58
|
-
body: JSON.stringify(body)
|
|
59
|
-
});
|
|
60
|
-
if (!envelope.success) return makeErrorResult('FORBIDDEN', envelope.error);
|
|
61
|
-
return {
|
|
62
|
-
data: envelope.data
|
|
63
|
-
};
|
|
64
|
-
} catch (err) {
|
|
65
|
-
if (err instanceof FetchError) {
|
|
66
|
-
const errBody = err.data;
|
|
67
|
-
return makeErrorResult(statusToCode(err.status ?? 0, errBody?.error), err.message);
|
|
61
|
+
// Prefer the API's own message ("start.timeZone: is required") over ofetch's
|
|
62
|
+
// generic "[POST] …: 400 Bad Request", which says nothing actionable.
|
|
63
|
+
return makeErrorResult(statusToCode(err.status ?? 0, body?.code), body?.error ?? err.message, body?.code);
|
|
68
64
|
}
|
|
69
65
|
return makeErrorResult('NETWORK_ERROR', String(err));
|
|
70
66
|
}
|
|
71
67
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
headers: this.headers,
|
|
77
|
-
body: JSON.stringify(body)
|
|
78
|
-
});
|
|
79
|
-
if (!envelope.success) return makeErrorResult('FORBIDDEN', envelope.error);
|
|
80
|
-
return {
|
|
81
|
-
data: envelope.data
|
|
82
|
-
};
|
|
83
|
-
} catch (err) {
|
|
84
|
-
if (err instanceof FetchError) {
|
|
85
|
-
const errBody = err.data;
|
|
86
|
-
return makeErrorResult(statusToCode(err.status ?? 0, errBody?.error), err.message);
|
|
87
|
-
}
|
|
88
|
-
return makeErrorResult('NETWORK_ERROR', String(err));
|
|
89
|
-
}
|
|
68
|
+
single(envelope) {
|
|
69
|
+
return {
|
|
70
|
+
data: envelope.data
|
|
71
|
+
};
|
|
90
72
|
}
|
|
91
|
-
async
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
73
|
+
async get(path, query) {
|
|
74
|
+
return this.request('GET', path, {
|
|
75
|
+
query,
|
|
76
|
+
hasBody: false
|
|
77
|
+
}, (e)=>this.single(e));
|
|
78
|
+
}
|
|
79
|
+
async post(path, body, query) {
|
|
80
|
+
return this.request('POST', path, {
|
|
81
|
+
body,
|
|
82
|
+
query,
|
|
83
|
+
hasBody: true
|
|
84
|
+
}, (e)=>this.single(e));
|
|
85
|
+
}
|
|
86
|
+
async put(path, body, query) {
|
|
87
|
+
return this.request('PUT', path, {
|
|
88
|
+
body,
|
|
89
|
+
query,
|
|
90
|
+
hasBody: true
|
|
91
|
+
}, (e)=>this.single(e));
|
|
92
|
+
}
|
|
93
|
+
async patch(path, body, query) {
|
|
94
|
+
return this.request('PATCH', path, {
|
|
95
|
+
body,
|
|
96
|
+
query,
|
|
97
|
+
hasBody: true
|
|
98
|
+
}, (e)=>this.single(e));
|
|
109
99
|
}
|
|
110
100
|
async del(path, query) {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
headers: this.bodylessHeaders,
|
|
116
|
-
params
|
|
117
|
-
});
|
|
118
|
-
if (!envelope.success) return makeErrorResult('FORBIDDEN', envelope.error);
|
|
119
|
-
return {
|
|
120
|
-
data: envelope.data
|
|
121
|
-
};
|
|
122
|
-
} catch (err) {
|
|
123
|
-
if (err instanceof FetchError) {
|
|
124
|
-
const errBody = err.data;
|
|
125
|
-
return makeErrorResult(statusToCode(err.status ?? 0, errBody?.error), err.message);
|
|
126
|
-
}
|
|
127
|
-
return makeErrorResult('NETWORK_ERROR', String(err));
|
|
128
|
-
}
|
|
101
|
+
return this.request('DELETE', path, {
|
|
102
|
+
query,
|
|
103
|
+
hasBody: false
|
|
104
|
+
}, (e)=>this.single(e));
|
|
129
105
|
}
|
|
130
106
|
async getList(path, query) {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
headers: this.bodylessHeaders,
|
|
136
|
-
params
|
|
137
|
-
});
|
|
138
|
-
if (!envelope.success) {
|
|
139
|
-
return makeErrorResult('FORBIDDEN', envelope.error);
|
|
140
|
-
}
|
|
107
|
+
return this.request('GET', path, {
|
|
108
|
+
query,
|
|
109
|
+
hasBody: false
|
|
110
|
+
}, (envelope)=>{
|
|
141
111
|
const { data, meta } = envelope;
|
|
142
112
|
const listMeta = {
|
|
143
113
|
total: meta.total,
|
|
@@ -149,14 +119,7 @@ class NucleusTransport {
|
|
|
149
119
|
data,
|
|
150
120
|
meta: listMeta
|
|
151
121
|
};
|
|
152
|
-
}
|
|
153
|
-
if (err instanceof FetchError) {
|
|
154
|
-
const body = err.data;
|
|
155
|
-
const code = statusToCode(err.status ?? 0, body?.error);
|
|
156
|
-
return makeErrorResult(code, err.message);
|
|
157
|
-
}
|
|
158
|
-
return makeErrorResult('NETWORK_ERROR', String(err));
|
|
159
|
-
}
|
|
122
|
+
});
|
|
160
123
|
}
|
|
161
124
|
}
|
|
162
125
|
|
|
@@ -245,6 +208,7 @@ function transformEmployee(raw) {
|
|
|
245
208
|
if ('job_title' in r) out['jobTitle'] = str(r['job_title']);
|
|
246
209
|
if ('department' in r) out['department'] = str(r['department']);
|
|
247
210
|
if ('employment_status' in r) out['employmentStatus'] = str(r['employment_status']);
|
|
211
|
+
if ('employee_code' in r) out['employeeCode'] = str(r['employee_code']);
|
|
248
212
|
if ('employment_type' in r) out['employmentType'] = str(r['employment_type']);
|
|
249
213
|
if ('is_external' in r) out['isExternal'] = bool(r['is_external']);
|
|
250
214
|
// employment
|
|
@@ -1642,9 +1606,22 @@ class TimesheetsAccessor {
|
|
|
1642
1606
|
}
|
|
1643
1607
|
}
|
|
1644
1608
|
|
|
1609
|
+
function actorQuery(actor) {
|
|
1610
|
+
return {
|
|
1611
|
+
employeeId: actor.employeeId,
|
|
1612
|
+
organizer: actor.organizer,
|
|
1613
|
+
calendarId: actor.calendarId
|
|
1614
|
+
};
|
|
1615
|
+
}
|
|
1645
1616
|
/**
|
|
1646
|
-
* CalendarAccessor — read workspace users' Google Calendar events (WS3).
|
|
1647
|
-
* Requires the app's `calendar:read` scope
|
|
1617
|
+
* CalendarAccessor — read and write workspace users' Google Calendar events (WS3).
|
|
1618
|
+
* Requires the app's `calendar:read` scope to list, and `calendar:create` /
|
|
1619
|
+
* `calendar:update` / `calendar:delete` to write. Not part of the bucket/CRUD scope
|
|
1620
|
+
* system, so these are not declared in the client's typed `scopes` config.
|
|
1621
|
+
*
|
|
1622
|
+
* Writes are mode-aware: under a test token every event is redirected to a sandbox
|
|
1623
|
+
* calendar with attendees stripped (reported in `suppressedAttendees`) and invitations
|
|
1624
|
+
* forced off, so a sandbox caller can never touch a real calendar or email a real guest.
|
|
1648
1625
|
*/ class CalendarAccessor {
|
|
1649
1626
|
constructor(transport){
|
|
1650
1627
|
this.transport = transport;
|
|
@@ -1654,7 +1631,32 @@ class TimesheetsAccessor {
|
|
|
1654
1631
|
employeeId: params.employeeId,
|
|
1655
1632
|
timeMin: params.timeMin,
|
|
1656
1633
|
timeMax: params.timeMax,
|
|
1657
|
-
q: params.q
|
|
1634
|
+
q: params.q,
|
|
1635
|
+
calendarId: params.calendarId
|
|
1636
|
+
});
|
|
1637
|
+
if ('error' in result) return result;
|
|
1638
|
+
return {
|
|
1639
|
+
data: result.data
|
|
1640
|
+
};
|
|
1641
|
+
}
|
|
1642
|
+
async createEvent(actor, input) {
|
|
1643
|
+
const result = await this.transport.post('/api/v1/data/calendar/events', input, actorQuery(actor));
|
|
1644
|
+
if ('error' in result) return result;
|
|
1645
|
+
return {
|
|
1646
|
+
data: result.data
|
|
1647
|
+
};
|
|
1648
|
+
}
|
|
1649
|
+
async updateEvent(actor, eventId, input) {
|
|
1650
|
+
const result = await this.transport.patch(`/api/v1/data/calendar/events/${encodeURIComponent(eventId)}`, input, actorQuery(actor));
|
|
1651
|
+
if ('error' in result) return result;
|
|
1652
|
+
return {
|
|
1653
|
+
data: result.data
|
|
1654
|
+
};
|
|
1655
|
+
}
|
|
1656
|
+
async deleteEvent(actor, eventId, opts) {
|
|
1657
|
+
const result = await this.transport.del(`/api/v1/data/calendar/events/${encodeURIComponent(eventId)}`, {
|
|
1658
|
+
...actorQuery(actor),
|
|
1659
|
+
sendUpdates: opts?.sendUpdates
|
|
1658
1660
|
});
|
|
1659
1661
|
if ('error' in result) return result;
|
|
1660
1662
|
return {
|
|
@@ -1663,6 +1665,29 @@ class TimesheetsAccessor {
|
|
|
1663
1665
|
}
|
|
1664
1666
|
}
|
|
1665
1667
|
|
|
1668
|
+
/**
|
|
1669
|
+
* ChatAccessor — send Google Chat messages as the company's Chat app. Requires the
|
|
1670
|
+
* app's `chat:send` scope. Not part of the bucket/CRUD scope system.
|
|
1671
|
+
*
|
|
1672
|
+
* This is a transport, not a notification system: no templates, no audience resolution,
|
|
1673
|
+
* no per-person preferences. Your app decides who should hear what.
|
|
1674
|
+
*
|
|
1675
|
+
* Mode-aware: under a test token the message is redirected to a sandbox space with a
|
|
1676
|
+
* banner naming the intended recipient, and no direct message with a real person is ever
|
|
1677
|
+
* opened.
|
|
1678
|
+
*/ class ChatAccessor {
|
|
1679
|
+
constructor(transport){
|
|
1680
|
+
this.transport = transport;
|
|
1681
|
+
}
|
|
1682
|
+
async send(input) {
|
|
1683
|
+
const result = await this.transport.post('/api/v1/data/chat/messages', input);
|
|
1684
|
+
if ('error' in result) return result;
|
|
1685
|
+
return {
|
|
1686
|
+
data: result.data
|
|
1687
|
+
};
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1666
1691
|
/**
|
|
1667
1692
|
* EmailAccessor — send Nucleus-mediated email (WS5). Requires the app's `email:send` scope.
|
|
1668
1693
|
* In test mode the message is marked `[SANDBOX]` and external recipients are suppressed.
|
|
@@ -2031,6 +2056,7 @@ class AuthAccessor {
|
|
|
2031
2056
|
this.timesheets = new TimesheetsAccessor(transport);
|
|
2032
2057
|
this.calendar = new CalendarAccessor(transport);
|
|
2033
2058
|
this.email = new EmailAccessor(transport);
|
|
2059
|
+
this.chat = new ChatAccessor(transport);
|
|
2034
2060
|
this.employeeCompensation = new EmployeeCompensationAccessor(transport);
|
|
2035
2061
|
this.employeeProfiles = new EmployeeProfilesAccessor(transport);
|
|
2036
2062
|
this.employeeReviews = new EmployeeReviewsAccessor(transport);
|
|
@@ -2052,4 +2078,4 @@ function isError(result) {
|
|
|
2052
2078
|
return 'error' in result;
|
|
2053
2079
|
}
|
|
2054
2080
|
|
|
2055
|
-
export { FilesAccessor, NucleusClient, isError };
|
|
2081
|
+
export { CalendarAccessor, ChatAccessor, EmailAccessor, FilesAccessor, NucleusClient, isError };
|