@typeb-digital/nucleus-sdk 0.6.0 → 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 +96 -0
- package/README.md +137 -0
- package/dist/cjs/index.cjs +135 -107
- package/dist/cjs/index.d.cts +216 -9
- package/dist/cjs/index.d.cts.map +1 -1
- package/dist/es/index.d.ts +216 -9
- package/dist/es/index.d.ts.map +1 -1
- package/dist/es/index.js +133 -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
|
|
|
@@ -1643,9 +1606,22 @@ class TimesheetsAccessor {
|
|
|
1643
1606
|
}
|
|
1644
1607
|
}
|
|
1645
1608
|
|
|
1609
|
+
function actorQuery(actor) {
|
|
1610
|
+
return {
|
|
1611
|
+
employeeId: actor.employeeId,
|
|
1612
|
+
organizer: actor.organizer,
|
|
1613
|
+
calendarId: actor.calendarId
|
|
1614
|
+
};
|
|
1615
|
+
}
|
|
1646
1616
|
/**
|
|
1647
|
-
* CalendarAccessor — read workspace users' Google Calendar events (WS3).
|
|
1648
|
-
* 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.
|
|
1649
1625
|
*/ class CalendarAccessor {
|
|
1650
1626
|
constructor(transport){
|
|
1651
1627
|
this.transport = transport;
|
|
@@ -1655,7 +1631,32 @@ class TimesheetsAccessor {
|
|
|
1655
1631
|
employeeId: params.employeeId,
|
|
1656
1632
|
timeMin: params.timeMin,
|
|
1657
1633
|
timeMax: params.timeMax,
|
|
1658
|
-
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
|
|
1659
1660
|
});
|
|
1660
1661
|
if ('error' in result) return result;
|
|
1661
1662
|
return {
|
|
@@ -1664,6 +1665,29 @@ class TimesheetsAccessor {
|
|
|
1664
1665
|
}
|
|
1665
1666
|
}
|
|
1666
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
|
+
|
|
1667
1691
|
/**
|
|
1668
1692
|
* EmailAccessor — send Nucleus-mediated email (WS5). Requires the app's `email:send` scope.
|
|
1669
1693
|
* In test mode the message is marked `[SANDBOX]` and external recipients are suppressed.
|
|
@@ -2032,6 +2056,7 @@ class AuthAccessor {
|
|
|
2032
2056
|
this.timesheets = new TimesheetsAccessor(transport);
|
|
2033
2057
|
this.calendar = new CalendarAccessor(transport);
|
|
2034
2058
|
this.email = new EmailAccessor(transport);
|
|
2059
|
+
this.chat = new ChatAccessor(transport);
|
|
2035
2060
|
this.employeeCompensation = new EmployeeCompensationAccessor(transport);
|
|
2036
2061
|
this.employeeProfiles = new EmployeeProfilesAccessor(transport);
|
|
2037
2062
|
this.employeeReviews = new EmployeeReviewsAccessor(transport);
|
|
@@ -2053,4 +2078,4 @@ function isError(result) {
|
|
|
2053
2078
|
return 'error' in result;
|
|
2054
2079
|
}
|
|
2055
2080
|
|
|
2056
|
-
export { FilesAccessor, NucleusClient, isError };
|
|
2081
|
+
export { CalendarAccessor, ChatAccessor, EmailAccessor, FilesAccessor, NucleusClient, isError };
|