rotacloud 2.1.3 → 2.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.
@@ -1,11 +1,12 @@
1
1
  import { Account, Attendance, Auth, Availability, DailyBudgets, DailyRevenue, DayNote, DaysOff, Group, Leave, Role, Shift, TimeZone, Document, LeaveEmbargo, LeaveRequest, LeaveType, Location, Pin, Terminal, ToilAccrual, ToilAllowance, UserClockedIn, User, Settings, LogbookCategory, DayNoteV2, DayNoteV2QueryParameters } from './interfaces/index.js';
2
2
  import { LogbookEntry, LogbookQueryParameters } from './interfaces/logbook.interface.js';
3
+ import { Message } from './interfaces/message.interface.js';
3
4
  import { AttendanceQueryParams, AvailabilityQueryParams, DailyBudgetsQueryParams, DailyRevenueQueryParams, DayNotesQueryParams, DaysOffQueryParams, DocumentsQueryParams, GroupsQueryParams, LeaveEmbargoesQueryParams, LeaveQueryParams, LeaveRequestsQueryParams, LocationsQueryParams, RolesQueryParams, SettingsQueryParams, ShiftsQueryParams, TerminalsQueryParams, ToilAccrualsQueryParams, ToilAllowanceQueryParams, UsersQueryParams } from './interfaces/query-params/index.js';
4
5
  import { RequirementsOf } from './utils.js';
5
6
  /** Endpoint versions supported by the API */
6
7
  export type EndpointVersion = 'v1' | 'v2';
7
8
  /** Associated types for a given API endpoint */
8
- export type Endpoint<Entity, QueryParameters = undefined, CreateEntity extends keyof Entity | Partial<Entity> = any, RequiredFields extends keyof Entity = CreateEntity extends keyof Entity ? CreateEntity : never> = {
9
+ export type Endpoint<Entity, QueryParameters = undefined, CreateEntity extends keyof Entity | object = any, RequiredFields extends keyof Entity = CreateEntity extends keyof Entity ? CreateEntity : never> = {
9
10
  /** The type returned by an endpoint */
10
11
  type: Entity;
11
12
  /** The query parameters for endpoints that support listing */
@@ -35,6 +36,10 @@ export interface EndpointEntityMap extends Record<EndpointVersion, Record<string
35
36
  leave_types: Endpoint<LeaveType>;
36
37
  leave: Endpoint<Leave, LeaveQueryParams, 'users' | 'type' | 'start_date' | 'end_date'>;
37
38
  locations: Endpoint<Location, LocationsQueryParams, 'name'>;
39
+ messages: Endpoint<Message, undefined, Pick<Message, 'message' | 'subject'> & {
40
+ users: number[];
41
+ attachments?: Pick<Message['attachments'][number], 'key' | 'bucket' | 'name' | 'extension'>[];
42
+ }>;
38
43
  pins: Endpoint<Pin>;
39
44
  roles: Endpoint<Role, RolesQueryParams, 'name'>;
40
45
  settings: Endpoint<Settings, SettingsQueryParams>;
@@ -13,3 +13,6 @@ export declare class SDKError extends Error {
13
13
  url?: string;
14
14
  });
15
15
  }
16
+ export declare class ValidationError extends Error {
17
+ name: string;
18
+ }
@@ -15,3 +15,6 @@ export class SDKError extends Error {
15
15
  this.data = errorConfig.data;
16
16
  }
17
17
  }
18
+ export class ValidationError extends Error {
19
+ name = 'ValidationError';
20
+ }
@@ -5,7 +5,7 @@ export interface Document {
5
5
  user: number;
6
6
  users: number[];
7
7
  folder_id: number | null;
8
- expires: number | null;
8
+ expires: string | null;
9
9
  public: boolean;
10
10
  created_at: number;
11
11
  created_by: number | null;
@@ -36,3 +36,4 @@ export * from './user.interface.js';
36
36
  export * from './users-clocked-in.interface.js';
37
37
  export * from './users-clocked-out.interface.js';
38
38
  export * from './document.interface.js';
39
+ export * from './message.interface.js';
@@ -36,3 +36,4 @@ export * from './user.interface.js';
36
36
  export * from './users-clocked-in.interface.js';
37
37
  export * from './users-clocked-out.interface.js';
38
38
  export * from './document.interface.js';
39
+ export * from './message.interface.js';
@@ -0,0 +1,27 @@
1
+ export interface Message {
2
+ id: number;
3
+ /** Unix timestamp in seconds */
4
+ sent_at: number;
5
+ sent_by: number;
6
+ subject: string;
7
+ message: string;
8
+ /** User recipients of the message */
9
+ users: {
10
+ /** ID of user */
11
+ user: number;
12
+ sent: boolean;
13
+ opened: boolean;
14
+ /** Unix timestamp in seconds */
15
+ opened_at: boolean;
16
+ error: string | null;
17
+ }[];
18
+ attachments: {
19
+ name: string;
20
+ extension: string;
21
+ type: string;
22
+ /** rounded integer value */
23
+ size_kb: number;
24
+ bucket: string;
25
+ key: string;
26
+ }[];
27
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/main.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export * from './interfaces/index.js';
2
2
  export * from './interfaces/query-params/index.js';
3
- export * from './models/index.js';
3
+ export * from './error.js';
4
4
  export declare const createRotaCloudClient: (config: import("./interfaces/sdk-config.interface.js").SDKConfig) => import("./client-builder.js").SdkClient<{
5
5
  account: {
6
6
  endpoint: "accounts";
@@ -115,6 +115,14 @@ export declare const createRotaCloudClient: (config: import("./interfaces/sdk-co
115
115
  };
116
116
  };
117
117
  };
118
+ message: {
119
+ endpoint: "messages";
120
+ endpointVersion: "v1";
121
+ operations: ("list" | "listAll" | "listByPage")[];
122
+ customOperations: {
123
+ send: (ctx: import("./ops.js").OperationContext, message: import("./endpoint.js").EndpointEntityMap["v1"]["messages"]["createType"]) => import("./ops.js").RequestConfig<unknown, import("./interfaces/message.interface.js").Message>;
124
+ };
125
+ };
118
126
  pin: {
119
127
  endpoint: "pins";
120
128
  endpointVersion: "v1";
package/dist/main.js CHANGED
@@ -2,5 +2,5 @@ import { createSdkClient } from './client-builder.js';
2
2
  import { SERVICES } from './service.js';
3
3
  export * from './interfaces/index.js';
4
4
  export * from './interfaces/query-params/index.js';
5
- export * from './models/index.js';
5
+ export * from './error.js';
6
6
  export const createRotaCloudClient = createSdkClient(SERVICES);
package/dist/ops.d.ts CHANGED
@@ -2,7 +2,7 @@ import { Axios, AxiosRequestConfig, AxiosResponse } from 'axios';
2
2
  import { ServiceSpecification } from './service.js';
3
3
  import { RequestOptions, QueryParameterValue, RequirementsOf } from './utils.js';
4
4
  import { Endpoint } from './endpoint.js';
5
- import { SDKConfig } from './main.js';
5
+ import { SDKConfig } from './interfaces/index.js';
6
6
  /** Supported common operations */
7
7
  export type Operation = 'get' | 'list' | 'listAll' | 'listByPage' | 'delete' | 'deleteBatch' | 'create' | 'update' | 'updateBatch';
8
8
  /** Context provided to all operations */
@@ -105,6 +105,8 @@ interface PagedResponse<T> {
105
105
  }
106
106
  /** Utility for creating a query params map needed by most API requests */
107
107
  export declare function paramsFromOptions<T>(opts: RequestOptions<T>): Record<string, QueryParameterValue>;
108
+ /** Operation for creating an entity */
109
+ export declare function createOp<T = unknown, NewEntity = unknown>(ctx: OperationContext, newEntity: NewEntity): RequestConfig<NewEntity, T>;
108
110
  /** Operation for deleting an entity */
109
111
  declare function deleteOp(ctx: OperationContext, id: number): RequestConfig<unknown, void>;
110
112
  /** Operation for deleting a list of entities */
package/dist/ops.js CHANGED
@@ -1,4 +1,20 @@
1
1
  import { assert } from './utils.js';
2
+ import { ValidationError } from './error.js';
3
+ /** For validating the query parameter supplied to list ops
4
+ *
5
+ * @throws {ValidationError} if invalid
6
+ */
7
+ function validateQueryParameter(query) {
8
+ // undefined is an accepted type for query
9
+ if (query !== undefined && (typeof query !== 'object' || query === null)) {
10
+ throw new ValidationError('Invalid type for query', {
11
+ cause: {
12
+ type: typeof query,
13
+ value: query,
14
+ },
15
+ });
16
+ }
17
+ }
2
18
  /** Utility for creating a query params map needed by most API requests */
3
19
  export function paramsFromOptions(opts) {
4
20
  return {
@@ -30,6 +46,21 @@ function* requestPaginated(response, requestConfig) {
30
46
  }
31
47
  /** Operation for getting an entity */
32
48
  function getOp(ctx, id) {
49
+ if (typeof id !== 'number') {
50
+ throw new ValidationError('Invalid type for id', {
51
+ cause: {
52
+ type: typeof id,
53
+ },
54
+ });
55
+ }
56
+ if (!Number.isSafeInteger(id)) {
57
+ throw new ValidationError('Invalid value for id', {
58
+ cause: {
59
+ reason: 'Not a safe integer',
60
+ value: id,
61
+ },
62
+ });
63
+ }
33
64
  return {
34
65
  ...ctx.request,
35
66
  method: 'GET',
@@ -37,7 +68,15 @@ function getOp(ctx, id) {
37
68
  };
38
69
  }
39
70
  /** Operation for creating an entity */
40
- function createOp(ctx, newEntity) {
71
+ export function createOp(ctx, newEntity) {
72
+ if (typeof newEntity !== 'object' || newEntity === null) {
73
+ throw new ValidationError('Invalid type for entity', {
74
+ cause: {
75
+ type: typeof newEntity,
76
+ value: newEntity,
77
+ },
78
+ });
79
+ }
41
80
  return {
42
81
  ...ctx.request,
43
82
  method: 'POST',
@@ -47,6 +86,22 @@ function createOp(ctx, newEntity) {
47
86
  }
48
87
  /** Operation for updating an entity for v1 endpoints */
49
88
  function updateV1Op(ctx, entity) {
89
+ if (typeof entity !== 'object' || entity === null || !('id' in entity)) {
90
+ throw new ValidationError('Invalid type for entity', {
91
+ cause: {
92
+ type: typeof entity,
93
+ value: entity,
94
+ },
95
+ });
96
+ }
97
+ if (!Number.isSafeInteger(entity.id)) {
98
+ throw new ValidationError('Invalid value for entity id', {
99
+ cause: {
100
+ reason: 'Not a safe integer',
101
+ value: entity.id,
102
+ },
103
+ });
104
+ }
50
105
  return {
51
106
  ...ctx.request,
52
107
  method: 'POST',
@@ -56,6 +111,22 @@ function updateV1Op(ctx, entity) {
56
111
  }
57
112
  /** Operation for updating an entity for v2 endpoints */
58
113
  function updateV2Op(ctx, entity) {
114
+ if (typeof entity !== 'object' || entity === null || !('id' in entity)) {
115
+ throw new ValidationError('Invalid type for entity', {
116
+ cause: {
117
+ type: typeof entity,
118
+ value: entity,
119
+ },
120
+ });
121
+ }
122
+ if (!Number.isSafeInteger(entity.id)) {
123
+ throw new ValidationError('Invalid value for entity id', {
124
+ cause: {
125
+ reason: 'Not a safe integer',
126
+ value: entity.id,
127
+ },
128
+ });
129
+ }
59
130
  return {
60
131
  ...ctx.request,
61
132
  method: 'PUT',
@@ -65,6 +136,13 @@ function updateV2Op(ctx, entity) {
65
136
  }
66
137
  /** Operation for deleting a list of entities */
67
138
  async function updateBatchOp(ctx, entities) {
139
+ if (!Array.isArray(entities)) {
140
+ throw new ValidationError('Invalid type for entity array', {
141
+ cause: {
142
+ type: typeof entities,
143
+ },
144
+ });
145
+ }
68
146
  const res = await ctx.client.request({
69
147
  ...ctx.request,
70
148
  method: 'POST',
@@ -83,6 +161,21 @@ async function updateBatchOp(ctx, entities) {
83
161
  }
84
162
  /** Operation for deleting an entity */
85
163
  function deleteOp(ctx, id) {
164
+ if (typeof id !== 'number') {
165
+ throw new ValidationError('Invalid type for id', {
166
+ cause: {
167
+ type: typeof id,
168
+ },
169
+ });
170
+ }
171
+ if (!Number.isSafeInteger(id)) {
172
+ throw new ValidationError('Invalid value for id', {
173
+ cause: {
174
+ reason: 'Not a safe integer',
175
+ value: id,
176
+ },
177
+ });
178
+ }
86
179
  return {
87
180
  ...ctx.request,
88
181
  method: 'DELETE',
@@ -91,6 +184,13 @@ function deleteOp(ctx, id) {
91
184
  }
92
185
  /** Operation for deleting a list of entities */
93
186
  function deleteBatchOp(ctx, ids) {
187
+ if (!Array.isArray(ids)) {
188
+ throw new ValidationError('Invalid type for id array', {
189
+ cause: {
190
+ type: typeof ids,
191
+ },
192
+ });
193
+ }
94
194
  return {
95
195
  ...ctx.request,
96
196
  method: 'DELETE',
@@ -104,6 +204,7 @@ function deleteBatchOp(ctx, ids) {
104
204
  export async function* listOp(ctx, query,
105
205
  // NOTE: offset is only supported in v1
106
206
  opts) {
207
+ validateQueryParameter(query);
107
208
  const queriedRequest = {
108
209
  ...ctx.request,
109
210
  url: `${ctx.service.endpointVersion}/${ctx.service.endpoint}`,
@@ -134,6 +235,7 @@ opts) {
134
235
  * automatically handling pagination as and when needed
135
236
  */
136
237
  export async function* listV2Op(ctx, query, opts) {
238
+ validateQueryParameter(query);
137
239
  const queriedRequest = {
138
240
  ...ctx.request,
139
241
  url: `${ctx.service.endpointVersion}/${ctx.service.endpoint}`,
@@ -194,6 +296,7 @@ export async function listAllV2Op(ctx, query, opts) {
194
296
  async function* listByPageOp(ctx, query,
195
297
  // NOTE: offset is only supported in v1
196
298
  opts) {
299
+ validateQueryParameter(query);
197
300
  const queriedRequest = {
198
301
  ...ctx.request,
199
302
  url: `${ctx.service.endpointVersion}/${ctx.service.endpoint}`,
@@ -222,6 +325,7 @@ opts) {
222
325
  * automatically handling pagination as and when needed
223
326
  */
224
327
  async function* listByPageV2Op(ctx, query, opts) {
328
+ validateQueryParameter(query);
225
329
  const queriedRequest = {
226
330
  ...ctx.request,
227
331
  url: `${ctx.service.endpointVersion}/${ctx.service.endpoint}`,
package/dist/ops.test.js CHANGED
@@ -21,7 +21,7 @@ describe('Operations', () => {
21
21
  const serviceV1 = {
22
22
  endpoint: 'settings',
23
23
  endpointVersion: 'v1',
24
- operations: ['get', 'list'],
24
+ operations: ['get', 'delete', 'list'],
25
25
  customOperations: {
26
26
  promiseOp: async () => 3,
27
27
  },
@@ -29,7 +29,7 @@ describe('Operations', () => {
29
29
  const serviceV2 = {
30
30
  endpoint: 'logbook',
31
31
  endpointVersion: 'v2',
32
- operations: ['get', 'list'],
32
+ operations: ['get', 'delete', 'list'],
33
33
  customOperations: {
34
34
  promiseOp: async () => 3,
35
35
  },
@@ -60,6 +60,21 @@ describe('Operations', () => {
60
60
  const promiseOpRes = await serviceV1.customOperations.promiseOp();
61
61
  expect(promiseOpRes).toStrictEqual(await client.service.promiseOp());
62
62
  });
63
+ describe('validation', () => {
64
+ const invalidIds = ['id', { id: 1 }, undefined, null, Infinity, -Infinity, 2.1, NaN];
65
+ test('getOp rejects invalid IDs', () => {
66
+ for (const invalidId of invalidIds) {
67
+ expect(() => client.service.get(invalidId)).toThrowError();
68
+ expect(() => client.serviceV2.get(invalidId)).toThrowError();
69
+ }
70
+ });
71
+ test('deleteOp rejects invalid IDs', () => {
72
+ for (const invalidId of invalidIds) {
73
+ expect(() => client.service.delete(invalidId)).toThrowError();
74
+ expect(() => client.serviceV2.delete(invalidId)).toThrowError();
75
+ }
76
+ });
77
+ });
63
78
  describe('list', () => {
64
79
  describe('v1 ops', () => {
65
80
  test('respects `maxResults` parameter in pagination', async () => {
package/dist/service.d.ts CHANGED
@@ -8,6 +8,7 @@ import { ShiftSwapRequest } from './interfaces/swap-request.interface.js';
8
8
  import { ShiftDropRequest } from './interfaces/drop-request.interface.js';
9
9
  import { ToilAllowanceQueryParams } from './interfaces/query-params/index.js';
10
10
  import { LogbookEntry, LogbookQueryParameters } from './interfaces/logbook.interface.js';
11
+ import { Message } from './interfaces/message.interface.js';
11
12
  export type ServiceSpecification<CustomOp extends OpDef<unknown> = OpDef<any>> = {
12
13
  /** Operations allowed and usable for the endpoint */
13
14
  operations: Operation[];
@@ -156,6 +157,14 @@ export declare const SERVICES: {
156
157
  };
157
158
  };
158
159
  };
160
+ message: {
161
+ endpoint: "messages";
162
+ endpointVersion: "v1";
163
+ operations: ("list" | "listAll" | "listByPage")[];
164
+ customOperations: {
165
+ send: (ctx: OperationContext, message: EndpointEntityMap["v1"]["messages"]["createType"]) => RequestConfig<unknown, Message>;
166
+ };
167
+ };
159
168
  pin: {
160
169
  endpoint: "pins";
161
170
  endpointVersion: "v1";
package/dist/service.js CHANGED
@@ -1,4 +1,4 @@
1
- import { listAllOp, listAllV2Op, listOp, listV2Op, paramsFromOptions, } from './ops.js';
1
+ import { createOp, listAllOp, listAllV2Op, listOp, listV2Op, paramsFromOptions, } from './ops.js';
2
2
  /**
3
3
  * Map of all officially supported service specifications used to generate the
4
4
  * SDK client where each key is the service name and each value is the service
@@ -184,6 +184,14 @@ export const SERVICES = {
184
184
  },
185
185
  },
186
186
  },
187
+ message: {
188
+ endpoint: 'messages',
189
+ endpointVersion: 'v1',
190
+ operations: ['list', 'listAll', 'listByPage'],
191
+ customOperations: {
192
+ send: (ctx, message) => createOp(ctx, message),
193
+ },
194
+ },
187
195
  pin: {
188
196
  endpoint: 'pins',
189
197
  endpointVersion: 'v1',
package/dist/utils.d.ts CHANGED
@@ -23,7 +23,17 @@ export interface RequestOptions<T> {
23
23
  dryRun?: boolean;
24
24
  fields?: T extends Object ? (keyof T)[] : never;
25
25
  }
26
- export declare function assert(value: unknown, message?: string | Error): asserts value;
26
+ /** Ensures the provided value resolves to `true` before continuing
27
+ *
28
+ * If the value doesn't resolve to `true` then the provided `error` will be thrown
29
+ *
30
+ * Intended to be run in production and not removed on build
31
+ *
32
+ * @param assertion assertion to verify
33
+ * @param error error to throw. If `undefined` or a `string` then an {@link AssertionError}
34
+ * will be thrown instead
35
+ */
36
+ export declare function assert(assertion: unknown, error?: string | Error): asserts assertion;
27
37
  /** Creates and configures an Axios client for use in all calls to API endpoints
28
38
  * according to the provided {@see SDKConfig}
29
39
  */
package/dist/utils.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import axios, { isAxiosError } from 'axios';
2
2
  import axiosRetry, { isNetworkOrIdempotentRequestError } from 'axios-retry';
3
3
  import { RetryStrategy } from './interfaces/index.js';
4
- import { SDKError } from './models/index.js';
4
+ import { SDKError } from './error.js';
5
5
  import pkg from '../package.json' with { type: 'json' };
6
6
  const DEFAULT_RETRIES = 3;
7
7
  const DEFAULT_RETRY_DELAY = 2000;
@@ -19,12 +19,22 @@ const DEFAULT_RETRY_STRATEGY_OPTIONS = {
19
19
  class AssertionError extends Error {
20
20
  name = AssertionError.prototype.name;
21
21
  }
22
- export function assert(value, message) {
23
- if (value)
22
+ /** Ensures the provided value resolves to `true` before continuing
23
+ *
24
+ * If the value doesn't resolve to `true` then the provided `error` will be thrown
25
+ *
26
+ * Intended to be run in production and not removed on build
27
+ *
28
+ * @param assertion assertion to verify
29
+ * @param error error to throw. If `undefined` or a `string` then an {@link AssertionError}
30
+ * will be thrown instead
31
+ */
32
+ export function assert(assertion, error) {
33
+ if (assertion)
24
34
  return;
25
- if (!message || typeof message === 'string')
26
- throw new AssertionError(message ?? `Assertion failed - value = ${value}`);
27
- throw message;
35
+ if (error === undefined || typeof error === 'string')
36
+ throw new AssertionError(error ?? `Assertion failed - value = ${assertion}`);
37
+ throw error;
28
38
  }
29
39
  /** Converts a map of query parameter key/values into API compatible {@see URLSearchParams} */
30
40
  function toSearchParams(parameters) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rotacloud",
3
- "version": "2.1.3",
3
+ "version": "2.2.0",
4
4
  "description": "The RotaCloud SDK for the RotaCloud API",
5
5
  "type": "module",
6
6
  "engines": {
@@ -1,7 +1,7 @@
1
1
  import { test, expect, describe, vi } from 'vitest';
2
2
  import { Axios } from 'axios';
3
3
  import { createSdkClient, DEFAULT_CONFIG } from './client-builder.js';
4
- import { SDKConfig } from './main.js';
4
+ import { SDKConfig } from './interfaces/index.js';
5
5
  import pkg from '../package.json' with { type: 'json' };
6
6
 
7
7
  let mockAxiosClient: Axios;
package/src/endpoint.ts CHANGED
@@ -29,6 +29,7 @@ import {
29
29
  DayNoteV2QueryParameters,
30
30
  } from './interfaces/index.js';
31
31
  import { LogbookEntry, LogbookQueryParameters } from './interfaces/logbook.interface.js';
32
+ import { Message } from './interfaces/message.interface.js';
32
33
  import {
33
34
  AttendanceQueryParams,
34
35
  AvailabilityQueryParams,
@@ -58,7 +59,7 @@ export type EndpointVersion = 'v1' | 'v2';
58
59
  export type Endpoint<
59
60
  Entity,
60
61
  QueryParameters = undefined,
61
- CreateEntity extends keyof Entity | Partial<Entity> = any,
62
+ CreateEntity extends keyof Entity | object = any,
62
63
  // NOTE: introduced to work around TS inferring `RequirementsOf<Entity, CreateEntity>` incorrectly
63
64
  // TS resolves type to:
64
65
  // `RequirementsOf<Entity, "key 1"> | RequirementsOf<Entity "key 2">`
@@ -96,6 +97,14 @@ export interface EndpointEntityMap extends Record<EndpointVersion, Record<string
96
97
  leave_types: Endpoint<LeaveType>;
97
98
  leave: Endpoint<Leave, LeaveQueryParams, 'users' | 'type' | 'start_date' | 'end_date'>;
98
99
  locations: Endpoint<Location, LocationsQueryParams, 'name'>;
100
+ messages: Endpoint<
101
+ Message,
102
+ undefined,
103
+ Pick<Message, 'message' | 'subject'> & {
104
+ users: number[];
105
+ attachments?: Pick<Message['attachments'][number], 'key' | 'bucket' | 'name' | 'extension'>[];
106
+ }
107
+ >;
99
108
  pins: Endpoint<Pin>;
100
109
  roles: Endpoint<Role, RolesQueryParams, 'name'>;
101
110
  settings: Endpoint<Settings, SettingsQueryParams>;
@@ -18,3 +18,7 @@ export class SDKError extends Error {
18
18
  this.data = errorConfig.data;
19
19
  }
20
20
  }
21
+
22
+ export class ValidationError extends Error {
23
+ override name = 'ValidationError';
24
+ }
@@ -5,7 +5,7 @@ export interface Document {
5
5
  user: number;
6
6
  users: number[];
7
7
  folder_id: number | null;
8
- expires: number | null;
8
+ expires: string | null;
9
9
  public: boolean;
10
10
  created_at: number;
11
11
  created_by: number | null;
@@ -36,3 +36,4 @@ export * from './user.interface.js';
36
36
  export * from './users-clocked-in.interface.js';
37
37
  export * from './users-clocked-out.interface.js';
38
38
  export * from './document.interface.js';
39
+ export * from './message.interface.js';
@@ -0,0 +1,27 @@
1
+ export interface Message {
2
+ id: number;
3
+ /** Unix timestamp in seconds */
4
+ sent_at: number;
5
+ sent_by: number;
6
+ subject: string;
7
+ message: string;
8
+ /** User recipients of the message */
9
+ users: {
10
+ /** ID of user */
11
+ user: number;
12
+ sent: boolean;
13
+ opened: boolean;
14
+ /** Unix timestamp in seconds */
15
+ opened_at: boolean;
16
+ error: string | null;
17
+ }[];
18
+ attachments: {
19
+ name: string;
20
+ extension: string;
21
+ type: string;
22
+ /** rounded integer value */
23
+ size_kb: number;
24
+ bucket: string;
25
+ key: string;
26
+ }[];
27
+ }
package/src/main.ts CHANGED
@@ -3,5 +3,5 @@ import { SERVICES } from './service.js';
3
3
 
4
4
  export * from './interfaces/index.js';
5
5
  export * from './interfaces/query-params/index.js';
6
- export * from './models/index.js';
6
+ export * from './error.js';
7
7
  export const createRotaCloudClient = createSdkClient(SERVICES);
package/src/ops.test.ts CHANGED
@@ -25,7 +25,7 @@ describe('Operations', () => {
25
25
  const serviceV1 = {
26
26
  endpoint: 'settings',
27
27
  endpointVersion: 'v1',
28
- operations: ['get', 'list'],
28
+ operations: ['get', 'delete', 'list'],
29
29
  customOperations: {
30
30
  promiseOp: async () => 3,
31
31
  },
@@ -33,7 +33,7 @@ describe('Operations', () => {
33
33
  const serviceV2 = {
34
34
  endpoint: 'logbook',
35
35
  endpointVersion: 'v2',
36
- operations: ['get', 'list'],
36
+ operations: ['get', 'delete', 'list'],
37
37
  customOperations: {
38
38
  promiseOp: async () => 3,
39
39
  },
@@ -74,6 +74,24 @@ describe('Operations', () => {
74
74
  expect(promiseOpRes).toStrictEqual(await client.service.promiseOp());
75
75
  });
76
76
 
77
+ describe('validation', () => {
78
+ const invalidIds = ['id', { id: 1 }, undefined, null, Infinity, -Infinity, 2.1, NaN];
79
+
80
+ test('getOp rejects invalid IDs', () => {
81
+ for (const invalidId of invalidIds) {
82
+ expect(() => client.service.get(invalidId as number)).toThrowError();
83
+ expect(() => client.serviceV2.get(invalidId as number)).toThrowError();
84
+ }
85
+ });
86
+
87
+ test('deleteOp rejects invalid IDs', () => {
88
+ for (const invalidId of invalidIds) {
89
+ expect(() => client.service.delete(invalidId as number)).toThrowError();
90
+ expect(() => client.serviceV2.delete(invalidId as number)).toThrowError();
91
+ }
92
+ });
93
+ });
94
+
77
95
  describe('list', () => {
78
96
  describe('v1 ops', () => {
79
97
  test('respects `maxResults` parameter in pagination', async () => {
package/src/ops.ts CHANGED
@@ -2,7 +2,8 @@ import { Axios, AxiosRequestConfig, AxiosResponse } from 'axios';
2
2
  import { ServiceSpecification } from './service.js';
3
3
  import { RequestOptions, QueryParameterValue, RequirementsOf, assert } from './utils.js';
4
4
  import { Endpoint, EndpointVersion } from './endpoint.js';
5
- import { SDKConfig } from './main.js';
5
+ import { SDKConfig } from './interfaces/index.js';
6
+ import { ValidationError } from './error.js';
6
7
 
7
8
  /** Supported common operations */
8
9
  export type Operation =
@@ -163,6 +164,22 @@ interface PagedResponse<T> {
163
164
  };
164
165
  }
165
166
 
167
+ /** For validating the query parameter supplied to list ops
168
+ *
169
+ * @throws {ValidationError} if invalid
170
+ */
171
+ function validateQueryParameter(query: unknown) {
172
+ // undefined is an accepted type for query
173
+ if (query !== undefined && (typeof query !== 'object' || query === null)) {
174
+ throw new ValidationError('Invalid type for query', {
175
+ cause: {
176
+ type: typeof query,
177
+ value: query,
178
+ },
179
+ });
180
+ }
181
+ }
182
+
166
183
  /** Utility for creating a query params map needed by most API requests */
167
184
  export function paramsFromOptions<T>(opts: RequestOptions<T>): Record<string, QueryParameterValue> {
168
185
  return {
@@ -200,6 +217,22 @@ function* requestPaginated<T>(
200
217
 
201
218
  /** Operation for getting an entity */
202
219
  function getOp<T = undefined>(ctx: OperationContext, id: number): RequestConfig<unknown, T> {
220
+ if (typeof id !== 'number') {
221
+ throw new ValidationError('Invalid type for id', {
222
+ cause: {
223
+ type: typeof id,
224
+ },
225
+ });
226
+ }
227
+ if (!Number.isSafeInteger(id)) {
228
+ throw new ValidationError('Invalid value for id', {
229
+ cause: {
230
+ reason: 'Not a safe integer',
231
+ value: id,
232
+ },
233
+ });
234
+ }
235
+
203
236
  return {
204
237
  ...ctx.request,
205
238
  method: 'GET',
@@ -208,10 +241,19 @@ function getOp<T = undefined>(ctx: OperationContext, id: number): RequestConfig<
208
241
  }
209
242
 
210
243
  /** Operation for creating an entity */
211
- function createOp<T = unknown, NewEntity = unknown>(
244
+ export function createOp<T = unknown, NewEntity = unknown>(
212
245
  ctx: OperationContext,
213
246
  newEntity: NewEntity,
214
247
  ): RequestConfig<NewEntity, T> {
248
+ if (typeof newEntity !== 'object' || newEntity === null) {
249
+ throw new ValidationError('Invalid type for entity', {
250
+ cause: {
251
+ type: typeof newEntity,
252
+ value: newEntity,
253
+ },
254
+ });
255
+ }
256
+
215
257
  return {
216
258
  ...ctx.request,
217
259
  method: 'POST',
@@ -225,6 +267,23 @@ function updateV1Op<Return, Entity extends { id: number } & Partial<Return>>(
225
267
  ctx: OperationContext,
226
268
  entity: Entity,
227
269
  ): RequestConfig<Entity, Return> {
270
+ if (typeof entity !== 'object' || entity === null || !('id' in entity)) {
271
+ throw new ValidationError('Invalid type for entity', {
272
+ cause: {
273
+ type: typeof entity,
274
+ value: entity,
275
+ },
276
+ });
277
+ }
278
+ if (!Number.isSafeInteger(entity.id)) {
279
+ throw new ValidationError('Invalid value for entity id', {
280
+ cause: {
281
+ reason: 'Not a safe integer',
282
+ value: entity.id,
283
+ },
284
+ });
285
+ }
286
+
228
287
  return {
229
288
  ...ctx.request,
230
289
  method: 'POST',
@@ -238,6 +297,23 @@ function updateV2Op<Return, Entity extends { id: number } & Partial<Return>>(
238
297
  ctx: OperationContext,
239
298
  entity: Entity,
240
299
  ): RequestConfig<Entity, Return> {
300
+ if (typeof entity !== 'object' || entity === null || !('id' in entity)) {
301
+ throw new ValidationError('Invalid type for entity', {
302
+ cause: {
303
+ type: typeof entity,
304
+ value: entity,
305
+ },
306
+ });
307
+ }
308
+ if (!Number.isSafeInteger(entity.id)) {
309
+ throw new ValidationError('Invalid value for entity id', {
310
+ cause: {
311
+ reason: 'Not a safe integer',
312
+ value: entity.id,
313
+ },
314
+ });
315
+ }
316
+
241
317
  return {
242
318
  ...ctx.request,
243
319
  method: 'PUT',
@@ -251,6 +327,14 @@ async function updateBatchOp<Return, Entity extends { id: number } & Partial<Ret
251
327
  ctx: OperationContext,
252
328
  entities: Entity[],
253
329
  ): Promise<{ success: Return[]; failed: { id: number; error: string }[] }> {
330
+ if (!Array.isArray(entities)) {
331
+ throw new ValidationError('Invalid type for entity array', {
332
+ cause: {
333
+ type: typeof entities,
334
+ },
335
+ });
336
+ }
337
+
254
338
  const res = await ctx.client.request<{ code: number; data?: Return; error?: string }[]>({
255
339
  ...ctx.request,
256
340
  method: 'POST',
@@ -273,6 +357,22 @@ async function updateBatchOp<Return, Entity extends { id: number } & Partial<Ret
273
357
 
274
358
  /** Operation for deleting an entity */
275
359
  function deleteOp(ctx: OperationContext, id: number): RequestConfig<unknown, void> {
360
+ if (typeof id !== 'number') {
361
+ throw new ValidationError('Invalid type for id', {
362
+ cause: {
363
+ type: typeof id,
364
+ },
365
+ });
366
+ }
367
+ if (!Number.isSafeInteger(id)) {
368
+ throw new ValidationError('Invalid value for id', {
369
+ cause: {
370
+ reason: 'Not a safe integer',
371
+ value: id,
372
+ },
373
+ });
374
+ }
375
+
276
376
  return {
277
377
  ...ctx.request,
278
378
  method: 'DELETE',
@@ -282,6 +382,14 @@ function deleteOp(ctx: OperationContext, id: number): RequestConfig<unknown, voi
282
382
 
283
383
  /** Operation for deleting a list of entities */
284
384
  function deleteBatchOp(ctx: OperationContext, ids: number[]): RequestConfig<unknown, void> {
385
+ if (!Array.isArray(ids)) {
386
+ throw new ValidationError('Invalid type for id array', {
387
+ cause: {
388
+ type: typeof ids,
389
+ },
390
+ });
391
+ }
392
+
285
393
  return {
286
394
  ...ctx.request,
287
395
  method: 'DELETE',
@@ -299,6 +407,7 @@ export async function* listOp<T, Query>(
299
407
  // NOTE: offset is only supported in v1
300
408
  opts?: RequestOptions<T[]> & { offset?: number },
301
409
  ): AsyncGenerator<T> {
410
+ validateQueryParameter(query);
302
411
  const queriedRequest = {
303
412
  ...ctx.request,
304
413
  url: `${ctx.service.endpointVersion}/${ctx.service.endpoint}`,
@@ -334,6 +443,8 @@ export async function* listV2Op<T, Query>(
334
443
  query: Query,
335
444
  opts?: RequestOptions<T[]>,
336
445
  ): AsyncGenerator<T> {
446
+ validateQueryParameter(query);
447
+
337
448
  const queriedRequest = {
338
449
  ...ctx.request,
339
450
  url: `${ctx.service.endpointVersion}/${ctx.service.endpoint}`,
@@ -403,6 +514,8 @@ async function* listByPageOp<T, Query>(
403
514
  // NOTE: offset is only supported in v1
404
515
  opts?: RequestOptions<T[]> & { offset?: number },
405
516
  ): AsyncGenerator<AxiosResponse<T[]>> {
517
+ validateQueryParameter(query);
518
+
406
519
  const queriedRequest = {
407
520
  ...ctx.request,
408
521
  url: `${ctx.service.endpointVersion}/${ctx.service.endpoint}`,
@@ -436,6 +549,8 @@ async function* listByPageV2Op<T, Query>(
436
549
  query: Query,
437
550
  opts?: RequestOptions<T[]>,
438
551
  ): AsyncGenerator<AxiosResponse<PagedResponse<T>>> {
552
+ validateQueryParameter(query);
553
+
439
554
  const queriedRequest = {
440
555
  ...ctx.request,
441
556
  url: `${ctx.service.endpointVersion}/${ctx.service.endpoint}`,
package/src/service.ts CHANGED
@@ -19,6 +19,7 @@ import {
19
19
  Operation,
20
20
  OperationContext,
21
21
  RequestConfig,
22
+ createOp,
22
23
  listAllOp,
23
24
  listAllV2Op,
24
25
  listOp,
@@ -31,6 +32,7 @@ import { ShiftSwapRequest } from './interfaces/swap-request.interface.js';
31
32
  import { ShiftDropRequest } from './interfaces/drop-request.interface.js';
32
33
  import { ToilAllowanceQueryParams } from './interfaces/query-params/index.js';
33
34
  import { LogbookEntry, LogbookQueryParameters } from './interfaces/logbook.interface.js';
35
+ import { Message } from './interfaces/message.interface.js';
34
36
 
35
37
  export type ServiceSpecification<CustomOp extends OpDef<unknown> = OpDef<any>> = {
36
38
  /** Operations allowed and usable for the endpoint */
@@ -266,6 +268,14 @@ export const SERVICES = {
266
268
  },
267
269
  },
268
270
  },
271
+ message: {
272
+ endpoint: 'messages',
273
+ endpointVersion: 'v1',
274
+ operations: ['list', 'listAll', 'listByPage'],
275
+ customOperations: {
276
+ send: (ctx, message: EndpointEntityMap['v1']['messages']['createType']) => createOp<Message>(ctx, message),
277
+ },
278
+ },
269
279
  pin: {
270
280
  endpoint: 'pins',
271
281
  endpointVersion: 'v1',
package/src/utils.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import axios, { Axios, AxiosError, AxiosRequestConfig, isAxiosError } from 'axios';
2
2
  import axiosRetry, { isNetworkOrIdempotentRequestError } from 'axios-retry';
3
3
  import { RetryOptions, RetryStrategy, SDKConfig } from './interfaces/index.js';
4
- import { SDKError } from './models/index.js';
4
+ import { SDKError } from './error.js';
5
5
  import pkg from '../package.json' with { type: 'json' };
6
6
 
7
7
  /** Creates a `Partial<T>` where all properties specified by `K` are required
@@ -50,11 +50,21 @@ class AssertionError extends Error {
50
50
  override name = AssertionError.prototype.name;
51
51
  }
52
52
 
53
- export function assert(value: unknown, message?: string | Error): asserts value {
54
- if (value) return;
55
- if (!message || typeof message === 'string')
56
- throw new AssertionError(message ?? `Assertion failed - value = ${value}`);
57
- throw message;
53
+ /** Ensures the provided value resolves to `true` before continuing
54
+ *
55
+ * If the value doesn't resolve to `true` then the provided `error` will be thrown
56
+ *
57
+ * Intended to be run in production and not removed on build
58
+ *
59
+ * @param assertion assertion to verify
60
+ * @param error error to throw. If `undefined` or a `string` then an {@link AssertionError}
61
+ * will be thrown instead
62
+ */
63
+ export function assert(assertion: unknown, error?: string | Error): asserts assertion {
64
+ if (assertion) return;
65
+ if (error === undefined || typeof error === 'string')
66
+ throw new AssertionError(error ?? `Assertion failed - value = ${assertion}`);
67
+ throw error;
58
68
  }
59
69
 
60
70
  /** Converts a map of query parameter key/values into API compatible {@see URLSearchParams} */
@@ -1 +0,0 @@
1
- export * from './SDKError.model.js';
@@ -1 +0,0 @@
1
- export * from './SDKError.model.js';
@@ -1 +0,0 @@
1
- export * from './SDKError.model.js';