@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/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
- async get(path, query) {
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: 'GET',
35
- headers: this.bodylessHeaders,
36
- params
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('FORBIDDEN', envelope.error);
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
- const code = statusToCode(err.status ?? 0, body?.error);
48
- return makeErrorResult(code, err.message);
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
- async put(path, body) {
73
- try {
74
- const envelope = await $fetch(`${this.baseUrl}${path}`, {
75
- method: 'PUT',
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 patch(path, body) {
92
- try {
93
- const envelope = await $fetch(`${this.baseUrl}${path}`, {
94
- method: 'PATCH',
95
- headers: this.headers,
96
- body: JSON.stringify(body)
97
- });
98
- if (!envelope.success) return makeErrorResult('FORBIDDEN', envelope.error);
99
- return {
100
- data: envelope.data
101
- };
102
- } catch (err) {
103
- if (err instanceof FetchError) {
104
- const errBody = err.data;
105
- return makeErrorResult(statusToCode(err.status ?? 0, errBody?.error), err.message);
106
- }
107
- return makeErrorResult('NETWORK_ERROR', String(err));
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
- try {
112
- const params = query ? Object.fromEntries(Object.entries(query).filter(([, v])=>v !== undefined)) : undefined;
113
- const envelope = await $fetch(`${this.baseUrl}${path}`, {
114
- method: 'DELETE',
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
- try {
132
- const params = query ? Object.fromEntries(Object.entries(query).filter(([, v])=>v !== undefined)) : undefined;
133
- const envelope = await $fetch(`${this.baseUrl}${path}`, {
134
- method: 'GET',
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
- } catch (err) {
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. Not part of the bucket/CRUD scope system.
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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typeb-digital/nucleus-sdk",
3
- "version": "0.5.3",
3
+ "version": "0.8.0",
4
4
  "description": "Server-side TypeScript SDK for the Nucleus data platform",
5
5
  "type": "module",
6
6
  "engines": {