rotacloud 1.0.53 → 1.0.56

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.
Files changed (67) hide show
  1. package/.eslintrc.json +2 -2
  2. package/.github/workflows/check.yml +36 -0
  3. package/.github/workflows/publish.yml +10 -8
  4. package/dist/cjs/interfaces/index.js +5 -1
  5. package/dist/cjs/interfaces/query-params/index.js +5 -1
  6. package/dist/cjs/models/index.js +5 -1
  7. package/dist/cjs/rotacloud.js +5 -1
  8. package/dist/cjs/services/accounts.service.js +12 -8
  9. package/dist/cjs/services/attendance.service.d.ts +1 -1
  10. package/dist/cjs/services/attendance.service.js +13 -11
  11. package/dist/cjs/services/availability.service.js +11 -11
  12. package/dist/cjs/services/daily-budgets.service.js +12 -8
  13. package/dist/cjs/services/daily-revenue.service.js +12 -8
  14. package/dist/cjs/services/day-notes.service.js +12 -8
  15. package/dist/cjs/services/days-off.service.js +12 -8
  16. package/dist/cjs/services/groups.service.d.ts +1 -1
  17. package/dist/cjs/services/groups.service.js +12 -8
  18. package/dist/cjs/services/index.js +5 -1
  19. package/dist/cjs/services/leave-embargoes.service.d.ts +1 -1
  20. package/dist/cjs/services/leave-embargoes.service.js +12 -8
  21. package/dist/cjs/services/leave-request.service.d.ts +1 -1
  22. package/dist/cjs/services/leave-request.service.js +12 -8
  23. package/dist/cjs/services/leave.service.d.ts +1 -1
  24. package/dist/cjs/services/leave.service.js +18 -12
  25. package/dist/cjs/services/locations.service.d.ts +1 -1
  26. package/dist/cjs/services/locations.service.js +12 -8
  27. package/dist/cjs/services/roles.service.d.ts +1 -1
  28. package/dist/cjs/services/roles.service.js +12 -8
  29. package/dist/cjs/services/service.d.ts +9 -11
  30. package/dist/cjs/services/service.js +33 -55
  31. package/dist/cjs/services/shifts.service.d.ts +27 -5
  32. package/dist/cjs/services/shifts.service.js +40 -14
  33. package/dist/cjs/services/terminals-active.service.js +12 -8
  34. package/dist/cjs/services/terminals.service.d.ts +1 -1
  35. package/dist/cjs/services/terminals.service.js +12 -8
  36. package/dist/cjs/services/toil-accruals.service.d.ts +1 -1
  37. package/dist/cjs/services/toil-accruals.service.js +12 -8
  38. package/dist/cjs/services/toil-allowance.service.js +12 -8
  39. package/dist/cjs/services/users-clock-in.service.d.ts +2 -2
  40. package/dist/cjs/services/users-clock-in.service.js +12 -8
  41. package/dist/cjs/services/users.service.d.ts +1 -1
  42. package/dist/cjs/services/users.service.js +12 -8
  43. package/dist/cjs/version.js +1 -1
  44. package/dist/mjs/services/attendance.service.d.ts +1 -1
  45. package/dist/mjs/services/attendance.service.js +1 -3
  46. package/dist/mjs/services/availability.service.js +5 -7
  47. package/dist/mjs/services/groups.service.d.ts +1 -1
  48. package/dist/mjs/services/leave-embargoes.service.d.ts +1 -1
  49. package/dist/mjs/services/leave-request.service.d.ts +1 -1
  50. package/dist/mjs/services/leave.service.d.ts +1 -1
  51. package/dist/mjs/services/locations.service.d.ts +1 -1
  52. package/dist/mjs/services/roles.service.d.ts +1 -1
  53. package/dist/mjs/services/service.d.ts +9 -11
  54. package/dist/mjs/services/service.js +34 -57
  55. package/dist/mjs/services/shifts.service.d.ts +27 -5
  56. package/dist/mjs/services/shifts.service.js +28 -6
  57. package/dist/mjs/services/terminals.service.d.ts +1 -1
  58. package/dist/mjs/services/toil-accruals.service.d.ts +1 -1
  59. package/dist/mjs/services/users-clock-in.service.d.ts +2 -2
  60. package/dist/mjs/services/users.service.d.ts +1 -1
  61. package/dist/mjs/version.js +1 -1
  62. package/package.json +16 -16
  63. package/src/services/attendance.service.ts +1 -3
  64. package/src/services/availability.service.ts +2 -4
  65. package/src/services/service.ts +51 -82
  66. package/src/services/shifts.service.ts +45 -14
  67. package/src/version.ts +1 -1
@@ -48,10 +48,18 @@ export class Service {
48
48
  isLeaveRequest(endpoint) {
49
49
  return endpoint === '/leave_requests';
50
50
  }
51
- buildQueryStr(queryParams) {
52
- if (!queryParams)
53
- return '';
54
- const reducedParams = Object.entries(queryParams).reduce((params, [key, val]) => {
51
+ buildQueryParams(options, extraParams) {
52
+ const queryParams = {
53
+ expand: options?.expand,
54
+ fields: options?.fields,
55
+ limit: options?.limit,
56
+ offset: options?.offset,
57
+ dry_run: options?.dryRun,
58
+ ...extraParams,
59
+ // NOTE: Should not overridable so must come after spread of params
60
+ exclude_link_header: true,
61
+ };
62
+ const reducedParams = Object.entries(queryParams ?? {}).reduce((params, [key, val]) => {
55
63
  if (val !== undefined && val !== '') {
56
64
  if (Array.isArray(val))
57
65
  params.push(...val.map((item) => [`${key}[]`, String(item)]));
@@ -60,17 +68,9 @@ export class Service {
60
68
  }
61
69
  return params;
62
70
  }, []);
63
- return new URLSearchParams(reducedParams).toString();
64
- }
65
- parsePageLinkHeader(linkHeader) {
66
- const pageData = {};
67
- for (const link of linkHeader.split(',')) {
68
- const { rel, url } = link.match(/<(?<url>.*)>; rel="(?<rel>\w*)"/)?.groups ?? {};
69
- pageData[rel] = url;
70
- }
71
- return pageData;
71
+ return new URLSearchParams(reducedParams);
72
72
  }
73
- fetch(httpOptions, options) {
73
+ fetch(reqConfig, options) {
74
74
  const headers = {
75
75
  Authorization: `Bearer ${RotaCloud.config.apiKey}`,
76
76
  'SDK-Version': Version.version,
@@ -88,21 +88,13 @@ export class Service {
88
88
  }
89
89
  else {
90
90
  // need to convert user field in payload to a header for creating leave_requests when using an API key
91
- this.isLeaveRequest(httpOptions.url) ? (headers.User = `${httpOptions.data.user}`) : undefined;
91
+ this.isLeaveRequest(reqConfig.url) ? (headers.User = `${reqConfig.data.user}`) : undefined;
92
92
  }
93
- const reqObject = {
94
- ...httpOptions,
93
+ const finalReqConfig = {
94
+ ...reqConfig,
95
95
  baseURL: RotaCloud.config.baseUri,
96
96
  headers,
97
- params: {
98
- expand: options?.expand,
99
- fields: options?.fields,
100
- limit: options?.limit,
101
- offset: options?.offset,
102
- dry_run: options?.dryRun,
103
- ...httpOptions?.params,
104
- },
105
- paramsSerializer: this.buildQueryStr,
97
+ params: this.buildQueryParams(options, reqConfig.params),
106
98
  };
107
99
  if (RotaCloud.config.retry) {
108
100
  const retryConfig = typeof RotaCloud.config.retry === 'string'
@@ -118,44 +110,29 @@ export class Service {
118
110
  },
119
111
  });
120
112
  }
121
- return this.client.request(reqObject);
113
+ return this.client.request(finalReqConfig);
122
114
  }
123
- async *listFetch(reqObject, options) {
124
- let pageRequestObject = reqObject;
125
- let currentPageUrl = pageRequestObject.url;
126
- let pageRemaining = true;
127
- while (pageRemaining) {
128
- const res = await this.fetch(pageRequestObject, options);
129
- const pageLinkMap = this.parsePageLinkHeader(res.headers.link ?? '');
130
- pageRemaining = Boolean(pageLinkMap.next);
131
- // NOTE: query params including paging options are included in the "next" link
132
- pageRequestObject = { url: pageLinkMap.next };
133
- yield res;
134
- // Failsafe incase the page does not change
135
- if (currentPageUrl === pageRequestObject.url) {
136
- throw new Error('Next page link did not change');
137
- }
138
- currentPageUrl = pageRequestObject.url;
115
+ /** Iterates through every page for a potentially paginated request */
116
+ async *fetchPages(reqConfig, options) {
117
+ const fallbackLimit = 20;
118
+ const res = await this.fetch(reqConfig, options);
119
+ yield res;
120
+ const limit = Number(res.headers['x-limit']) || fallbackLimit;
121
+ const entityCount = Number(res.headers['x-total-count']) || 0;
122
+ const requestOffset = Number(res.headers['x-offset']) || 0;
123
+ for (let offset = requestOffset + limit; offset < entityCount; offset += limit) {
124
+ yield this.fetch(reqConfig, { ...options, offset });
139
125
  }
140
126
  }
141
- async *listResponses(reqObject, options) {
142
- for await (const res of this.listFetch(reqObject, options)) {
127
+ async *listResponses(reqConfig, options) {
128
+ for await (const res of this.fetchPages(reqConfig, options)) {
143
129
  yield* res.data;
144
130
  }
145
131
  }
146
- iterator(reqObject, options) {
147
- const iterator = this.listResponses(reqObject, options);
132
+ iterator(reqConfig, options) {
148
133
  return {
149
- [Symbol.asyncIterator]() {
150
- return {
151
- next() {
152
- return iterator.next();
153
- },
154
- };
155
- },
156
- byPage: () => {
157
- return this.listFetch(reqObject, options);
158
- },
134
+ [Symbol.asyncIterator]: () => this.listResponses(reqConfig, options),
135
+ byPage: () => this.fetchPages(reqConfig, options),
159
136
  };
160
137
  }
161
138
  }
@@ -3,7 +3,7 @@ import { ApiShift } from '../interfaces/index.js';
3
3
  import { Service, Options, RequirementsOf } from './index.js';
4
4
  import { Shift } from '../models/shift.model.js';
5
5
  import { ShiftsQueryParams } from '../interfaces/query-params/shifts-query-params.interface.js';
6
- declare type RequiredProps = 'end_time' | 'start_time' | 'location';
6
+ type RequiredProps = 'end_time' | 'start_time' | 'location';
7
7
  export declare class ShiftsService extends Service {
8
8
  private apiPath;
9
9
  create(data: RequirementsOf<ApiShift, RequiredProps>): Promise<Shift>;
@@ -19,11 +19,33 @@ export declare class ShiftsService extends Service {
19
19
  list(query: ShiftsQueryParams, options?: Options): AsyncGenerator<Shift, void, unknown>;
20
20
  listAll(query: ShiftsQueryParams, options?: Options): Promise<Shift[]>;
21
21
  listByPage(query: ShiftsQueryParams, options?: Options): AsyncGenerator<AxiosResponse<ApiShift[], any>, any, unknown>;
22
- update(id: number, data: Partial<ApiShift>): Promise<Shift>;
23
- update(id: number, data: Partial<ApiShift>, options: {
22
+ update(shift: RequirementsOf<ApiShift, 'id'>): Promise<Shift>;
23
+ update(shift: RequirementsOf<ApiShift, 'id'>, options: {
24
24
  rawResponse: true;
25
- } & Options): Promise<AxiosResponse<ApiShift, any>>;
26
- update(id: number, data: Partial<ApiShift>, options: Options): Promise<Shift>;
25
+ } & Options): Promise<AxiosResponse<ApiShift>>;
26
+ update(shift: RequirementsOf<ApiShift, 'id'>, options: Options): Promise<Shift>;
27
+ update(shifts: RequirementsOf<ApiShift, 'id'>[]): Promise<{
28
+ success: Shift[];
29
+ failed: {
30
+ id: number;
31
+ error: string;
32
+ }[];
33
+ }>;
34
+ update(shift: RequirementsOf<ApiShift, 'id'>, options: Options): Promise<Shift>;
35
+ update(shifts: RequirementsOf<ApiShift, 'id'>[], options: {
36
+ rawResponse: true;
37
+ } & Options): Promise<AxiosResponse<{
38
+ code: number;
39
+ data?: ApiShift;
40
+ error?: string;
41
+ }[]>>;
42
+ update(shifts: RequirementsOf<ApiShift, 'id'>[], options: Options): Promise<{
43
+ success: Shift[];
44
+ failed: {
45
+ id: number;
46
+ error: string;
47
+ }[];
48
+ }>;
27
49
  delete(ids: number | number[]): Promise<number>;
28
50
  delete(ids: number | number[], options: {
29
51
  rawResponse: true;
@@ -30,18 +30,40 @@ export class ShiftsService extends Service {
30
30
  listByPage(query, options) {
31
31
  return super.iterator({ url: this.apiPath, params: query }, options).byPage();
32
32
  }
33
- update(id, data, options) {
33
+ update(shifts, options) {
34
+ if (!Array.isArray(shifts)) {
35
+ return super
36
+ .fetch({
37
+ url: `${this.apiPath}/${shifts.id}`,
38
+ data: shifts,
39
+ method: 'POST',
40
+ })
41
+ .then((res) => (options?.rawResponse ? res : new Shift(res.data)));
42
+ }
34
43
  return super
35
44
  .fetch({
36
- url: `${this.apiPath}/${id}`,
37
- data,
45
+ url: this.apiPath,
46
+ data: shifts,
38
47
  method: 'POST',
39
48
  })
40
- .then((res) => Promise.resolve(options?.rawResponse ? res : new Shift(res.data)));
49
+ .then((res) => {
50
+ if (options?.rawResponse)
51
+ return res;
52
+ const success = [];
53
+ const failed = [];
54
+ for (let shiftIdx = 0; shiftIdx < res.data.length; shiftIdx += 1) {
55
+ const { data, error } = res.data[shiftIdx];
56
+ if (data)
57
+ success.push(new Shift(data));
58
+ if (error)
59
+ failed.push({ id: shifts[shiftIdx].id, error });
60
+ }
61
+ return { success, failed };
62
+ });
41
63
  }
42
64
  delete(ids, options) {
43
- const params = typeof ids !== 'number'
44
- ? { url: this.apiPath, data: ids, method: 'DELETE' }
65
+ const params = Array.isArray(ids)
66
+ ? { url: this.apiPath, data: { ids }, method: 'DELETE' }
45
67
  : { url: `${this.apiPath}/${ids}`, method: 'DELETE' };
46
68
  return super.fetch(params).then((res) => Promise.resolve(options?.rawResponse ? res : res.status));
47
69
  }
@@ -2,7 +2,7 @@ import { AxiosResponse } from 'axios';
2
2
  import { ApiTerminal } from '../interfaces/index.js';
3
3
  import { Service, Options, RequirementsOf } from './index.js';
4
4
  import { Terminal } from '../models/terminal.model.js';
5
- declare type RequiredProps = 'name' | 'timezone';
5
+ type RequiredProps = 'name' | 'timezone';
6
6
  declare class TerminalsService extends Service {
7
7
  private apiPath;
8
8
  create(data: RequirementsOf<ApiTerminal, RequiredProps>): Promise<Terminal>;
@@ -3,7 +3,7 @@ import { Options, RequirementsOf, Service } from './service';
3
3
  import { ToilAccrualsQueryParams } from '../interfaces/query-params/toil-accruals-query-params.interface';
4
4
  import { ToilAccrual } from '../models/toil-accrual.model';
5
5
  import { ApiToilAccrual } from '../interfaces/toil-accrual.interface';
6
- declare type RequiredProps = 'duration_hours' | 'leave_year' | 'user_id';
6
+ type RequiredProps = 'duration_hours' | 'leave_year' | 'user_id';
7
7
  export declare class ToilAccrualsService extends Service {
8
8
  private apiPath;
9
9
  create(data: RequirementsOf<ApiToilAccrual, RequiredProps>): Promise<ToilAccrual>;
@@ -26,8 +26,8 @@ declare class UserBreak {
26
26
  end_location: ApiTerminalLocation | null;
27
27
  constructor(apiUserBreak: ApiUserBreak);
28
28
  }
29
- declare type RequiredPropsClockIn = 'method';
30
- declare type RequiredPropsBreak = 'method' | 'action';
29
+ type RequiredPropsClockIn = 'method';
30
+ type RequiredPropsBreak = 'method' | 'action';
31
31
  declare class UsersClockInService extends Service {
32
32
  private apiPath;
33
33
  getClockedInUser(id: number): Promise<UserClockedIn>;
@@ -3,7 +3,7 @@ import { ApiUser } from '../interfaces/index.js';
3
3
  import { Service, Options, RequirementsOf } from './index.js';
4
4
  import { User } from '../models/user.model.js';
5
5
  import { UsersQueryParams } from '../interfaces/query-params/users-query-params.interface.js';
6
- declare type RequiredProps = 'first_name' | 'last_name';
6
+ type RequiredProps = 'first_name' | 'last_name';
7
7
  declare class UsersService extends Service {
8
8
  private apiPath;
9
9
  create(data: RequirementsOf<ApiUser, RequiredProps>): Promise<User>;
@@ -1 +1 @@
1
- export const Version = { version: '1.0.53' };
1
+ export const Version = { version: '1.0.56' };
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "rotacloud",
3
- "version": "1.0.53",
3
+ "version": "1.0.56",
4
4
  "description": "The RotaCloud SDK for the RotaCloud API",
5
5
  "engines": {
6
- "node": ">=14.17.0"
6
+ "node": ">=16.10.0"
7
7
  },
8
8
  "main": "./dist/cjs/rotacloud.js",
9
9
  "module": "./dist/mjs/rotacloud.js",
@@ -18,7 +18,8 @@
18
18
  "watch": "rm -rf dist/* && concurrently \"tsc -p tsconfig.json --watch\" \"tsc -p tsconfig-cjs.json --watch\" && ./fixup",
19
19
  "build": "rm -rf dist/* && tsc -p tsconfig.json && tsc -p tsconfig-cjs.json && ./fixup",
20
20
  "lint": "eslint src --ext .ts",
21
- "test": "echo \"Error: no test specified\" && exit 1"
21
+ "test": "echo \"Error: no test specified\" && exit 1",
22
+ "prepublishOnly": "npm run build"
22
23
  },
23
24
  "repository": {
24
25
  "type": "git",
@@ -31,22 +32,21 @@
31
32
  },
32
33
  "homepage": "https://github.com/rotacloud/rotacloud-node#readme",
33
34
  "devDependencies": {
34
- "@typescript-eslint/eslint-plugin": "^5.4.0",
35
- "@typescript-eslint/parser": "^5.4.0",
36
- "concurrently": "^7.2.2",
37
- "eslint": "^8.2.0",
35
+ "@typescript-eslint/eslint-plugin": "^6.2.1",
36
+ "@typescript-eslint/parser": "^6.3.0",
37
+ "concurrently": "^8.2.0",
38
+ "eslint": "^8.46.0",
38
39
  "eslint-config-airbnb": "^19.0.0",
39
40
  "eslint-config-airbnb-base": "^15.0.0",
40
- "eslint-config-airbnb-typescript": "^16.0.0",
41
- "eslint-config-prettier": "^8.3.0",
42
- "eslint-plugin-import": "^2.25.3",
43
- "eslint-plugin-prettier": "^4.0.0",
44
- "prettier": "2.4.1",
45
- "typescript": "^4.4.4"
41
+ "eslint-config-airbnb-typescript": "^17.1.0",
42
+ "eslint-config-prettier": "^9.0.0",
43
+ "eslint-plugin-import": "^2.28.0",
44
+ "prettier": "~3.0.1",
45
+ "typescript": "^5.1.6"
46
46
  },
47
47
  "dependencies": {
48
- "@types/node": "^16.11.6",
49
- "axios": "^0.23.0",
50
- "axios-retry": "^3.3.1"
48
+ "@types/node": "^16.11.7",
49
+ "axios": "^1.4.0",
50
+ "axios-retry": "^3.6.0"
51
51
  }
52
52
  }
@@ -26,9 +26,7 @@ export class AttendanceService extends Service {
26
26
  get(id: number, options: { rawResponse: true } & Options): Promise<AxiosResponse<ApiAttendance, any>>;
27
27
  get(id: number, options: Options): Promise<Attendance>;
28
28
  get(id: number, options?: Options) {
29
- return super.fetch<ApiAttendance>({ url: `${this.apiPath}/${id}` }, options).then((res) => {
30
- return Promise.resolve(options?.rawResponse ? res : new Attendance(res.data));
31
- });
29
+ return super.fetch<ApiAttendance>({ url: `${this.apiPath}/${id}` }, options).then((res) => Promise.resolve(options?.rawResponse ? res : new Attendance(res.data)));
32
30
  }
33
31
 
34
32
  async *list(query: AttendanceQueryParams, options?: Options) {
@@ -40,13 +40,11 @@ export class AvailabilityService extends Service {
40
40
  return this.update(
41
41
  {
42
42
  user,
43
- dates: dates.map((date) => {
44
- return {
43
+ dates: dates.map((date) => ({
45
44
  date,
46
45
  available: [],
47
46
  unavailable: [],
48
- };
49
- }),
47
+ })),
50
48
  },
51
49
  options
52
50
  );
@@ -1,4 +1,4 @@
1
- import axios, { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosRequestHeaders, AxiosResponse } from 'axios';
1
+ import axios, { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
2
2
  import axiosRetry from 'axios-retry';
3
3
  import { RotaCloud } from '../rotacloud.js';
4
4
  import { Version } from '../version.js';
@@ -27,6 +27,15 @@ export type RetryOptions =
27
27
  maxRetries: number;
28
28
  };
29
29
 
30
+ export interface Options {
31
+ rawResponse?: boolean;
32
+ expand?: string[];
33
+ fields?: string[];
34
+ limit?: number;
35
+ offset?: number;
36
+ dryRun?: boolean;
37
+ }
38
+
30
39
  const DEFAULT_RETRIES = 3;
31
40
  const DEFAULT_RETRY_DELAY = 2000;
32
41
 
@@ -42,22 +51,6 @@ const DEFAULT_RETRY_STRATEGY_OPTIONS: Record<RetryStrategy, RetryOptions> = {
42
51
  },
43
52
  };
44
53
 
45
- export interface Options {
46
- rawResponse?: boolean;
47
- expand?: string[];
48
- fields?: string[];
49
- limit?: number;
50
- offset?: number;
51
- dryRun?: boolean;
52
- }
53
-
54
- interface PagingObject {
55
- first: string;
56
- prev: string;
57
- next: string;
58
- last: string;
59
- }
60
-
61
54
  type ParameterPrimitive = string | boolean | number | null;
62
55
  type ParameterValue = ParameterPrimitive | ParameterPrimitive[] | undefined;
63
56
 
@@ -90,13 +83,22 @@ export abstract class Service<ApiResponse = any> {
90
83
  return new SDKError(sdkErrorParams);
91
84
  }
92
85
 
93
- public isLeaveRequest(endpoint?: string): boolean {
86
+ private isLeaveRequest(endpoint?: string): boolean {
94
87
  return endpoint === '/leave_requests';
95
88
  }
96
89
 
97
- private buildQueryStr(queryParams: Record<string, ParameterValue> | undefined) {
98
- if (!queryParams) return '';
99
- const reducedParams = Object.entries(queryParams).reduce((params, [key, val]) => {
90
+ private buildQueryParams(options?: Options, extraParams?: Record<string, ParameterValue>) {
91
+ const queryParams: Record<string, ParameterValue> = {
92
+ expand: options?.expand,
93
+ fields: options?.fields,
94
+ limit: options?.limit,
95
+ offset: options?.offset,
96
+ dry_run: options?.dryRun,
97
+ ...extraParams,
98
+ // NOTE: Should not overridable so must come after spread of params
99
+ exclude_link_header: true,
100
+ };
101
+ const reducedParams = Object.entries(queryParams ?? {}).reduce((params, [key, val]) => {
100
102
  if (val !== undefined && val !== '') {
101
103
  if (Array.isArray(val)) params.push(...val.map((item) => [`${key}[]`, String(item)]));
102
104
  else params.push([key, String(val)]);
@@ -104,21 +106,11 @@ export abstract class Service<ApiResponse = any> {
104
106
  return params;
105
107
  }, [] as string[][]);
106
108
 
107
- return new URLSearchParams(reducedParams).toString();
108
- }
109
-
110
- private parsePageLinkHeader(linkHeader: string): Partial<PagingObject> {
111
- const pageData = {};
112
- for (const link of linkHeader.split(',')) {
113
- const { rel, url } = link.match(/<(?<url>.*)>; rel="(?<rel>\w*)"/)?.groups ?? {};
114
- pageData[rel] = url;
115
- }
116
-
117
- return pageData;
109
+ return new URLSearchParams(reducedParams);
118
110
  }
119
111
 
120
- public fetch<T = ApiResponse>(httpOptions: AxiosRequestConfig, options?: Options): Promise<AxiosResponse<T>> {
121
- const headers: AxiosRequestHeaders = {
112
+ fetch<T = ApiResponse>(reqConfig: AxiosRequestConfig, options?: Options): Promise<AxiosResponse<T>> {
113
+ const headers: Record<string, string> = {
122
114
  Authorization: `Bearer ${RotaCloud.config.apiKey}`,
123
115
  'SDK-Version': Version.version,
124
116
  };
@@ -136,22 +128,14 @@ export abstract class Service<ApiResponse = any> {
136
128
  headers.Account = String(RotaCloud.config.accountId);
137
129
  } else {
138
130
  // need to convert user field in payload to a header for creating leave_requests when using an API key
139
- this.isLeaveRequest(httpOptions.url) ? (headers.User = `${httpOptions.data.user}`) : undefined;
131
+ this.isLeaveRequest(reqConfig.url) ? (headers.User = `${reqConfig.data.user}`) : undefined;
140
132
  }
141
133
 
142
- const reqObject: AxiosRequestConfig<T> = {
143
- ...httpOptions,
134
+ const finalReqConfig: AxiosRequestConfig<T> = {
135
+ ...reqConfig,
144
136
  baseURL: RotaCloud.config.baseUri,
145
137
  headers,
146
- params: {
147
- expand: options?.expand,
148
- fields: options?.fields,
149
- limit: options?.limit,
150
- offset: options?.offset,
151
- dry_run: options?.dryRun,
152
- ...httpOptions?.params,
153
- },
154
- paramsSerializer: this.buildQueryStr,
138
+ params: this.buildQueryParams(options, reqConfig.params),
155
139
  };
156
140
 
157
141
  if (RotaCloud.config.retry) {
@@ -171,52 +155,37 @@ export abstract class Service<ApiResponse = any> {
171
155
  });
172
156
  }
173
157
 
174
- return this.client.request<T>(reqObject);
158
+ return this.client.request<T>(finalReqConfig);
175
159
  }
176
160
 
177
- private async *listFetch<T = ApiResponse>(
178
- reqObject: AxiosRequestConfig<T[]>,
179
- options?: Options
180
- ): AsyncGenerator<AxiosResponse<T[], any>> {
181
- let pageRequestObject = reqObject;
182
- let currentPageUrl = pageRequestObject.url;
183
-
184
- let pageRemaining = true;
185
- while (pageRemaining) {
186
- const res = await this.fetch<T[]>(pageRequestObject, options);
187
- const pageLinkMap = this.parsePageLinkHeader(res.headers.link ?? '');
188
- pageRemaining = Boolean(pageLinkMap.next);
189
- // NOTE: query params including paging options are included in the "next" link
190
- pageRequestObject = { url: pageLinkMap.next };
191
- yield res;
192
-
193
- // Failsafe incase the page does not change
194
- if (currentPageUrl === pageRequestObject.url) {
195
- throw new Error('Next page link did not change');
196
- }
197
- currentPageUrl = pageRequestObject.url;
161
+ /** Iterates through every page for a potentially paginated request */
162
+ private async *fetchPages<T>(
163
+ reqConfig: AxiosRequestConfig<T[]>,
164
+ options: Options | undefined
165
+ ): AsyncGenerator<AxiosResponse<T[]>> {
166
+ const fallbackLimit = 20;
167
+ const res = await this.fetch<T[]>(reqConfig, options);
168
+ yield res;
169
+
170
+ const limit = Number(res.headers['x-limit']) || fallbackLimit;
171
+ const entityCount = Number(res.headers['x-total-count']) || 0;
172
+ const requestOffset = Number(res.headers['x-offset']) || 0;
173
+
174
+ for (let offset = requestOffset + limit; offset < entityCount; offset += limit) {
175
+ yield this.fetch<T[]>(reqConfig, { ...options, offset });
198
176
  }
199
177
  }
200
178
 
201
- private async *listResponses<T = ApiResponse>(reqObject: AxiosRequestConfig<T[]>, options?: Options) {
202
- for await (const res of this.listFetch<T>(reqObject, options)) {
179
+ private async *listResponses<T = ApiResponse>(reqConfig: AxiosRequestConfig<T[]>, options?: Options) {
180
+ for await (const res of this.fetchPages<T>(reqConfig, options)) {
203
181
  yield* res.data;
204
182
  }
205
183
  }
206
184
 
207
- public iterator<T = ApiResponse>(reqObject: AxiosRequestConfig<T[]>, options?: Options) {
208
- const iterator = this.listResponses<T>(reqObject, options);
185
+ iterator<T = ApiResponse>(reqConfig: AxiosRequestConfig<T[]>, options?: Options) {
209
186
  return {
210
- [Symbol.asyncIterator]() {
211
- return {
212
- next() {
213
- return iterator.next();
214
- },
215
- };
216
- },
217
- byPage: () => {
218
- return this.listFetch<T>(reqObject, options);
219
- },
187
+ [Symbol.asyncIterator]: () => this.listResponses<T>(reqConfig, options),
188
+ byPage: () => this.fetchPages<T>(reqConfig, options),
220
189
  };
221
190
  }
222
191
  }
@@ -51,31 +51,62 @@ export class ShiftsService extends Service {
51
51
  return super.iterator<ApiShift>({ url: this.apiPath, params: query }, options).byPage();
52
52
  }
53
53
 
54
- update(id: number, data: Partial<ApiShift>): Promise<Shift>;
54
+ update(shift: RequirementsOf<ApiShift, 'id'>): Promise<Shift>;
55
55
  update(
56
- id: number,
57
- data: Partial<ApiShift>,
56
+ shift: RequirementsOf<ApiShift, 'id'>,
58
57
  options: { rawResponse: true } & Options
59
- ): Promise<AxiosResponse<ApiShift, any>>;
60
- update(id: number, data: Partial<ApiShift>, options: Options): Promise<Shift>;
61
- update(id: number, data: Partial<ApiShift>, options?: Options) {
58
+ ): Promise<AxiosResponse<ApiShift>>;
59
+ update(shift: RequirementsOf<ApiShift, 'id'>, options: Options): Promise<Shift>;
60
+ update(
61
+ shifts: RequirementsOf<ApiShift, 'id'>[]
62
+ ): Promise<{ success: Shift[]; failed: { id: number; error: string }[] }>;
63
+ update(shift: RequirementsOf<ApiShift, 'id'>, options: Options): Promise<Shift>;
64
+ update(
65
+ shifts: RequirementsOf<ApiShift, 'id'>[],
66
+ options: { rawResponse: true } & Options
67
+ ): Promise<AxiosResponse<{ code: number; data?: ApiShift; error?: string }[]>>;
68
+ update(
69
+ shifts: RequirementsOf<ApiShift, 'id'>[],
70
+ options: Options
71
+ ): Promise<{ success: Shift[]; failed: { id: number; error: string }[] }>;
72
+ update(shifts: RequirementsOf<ApiShift, 'id'> | RequirementsOf<ApiShift, 'id'>[], options?: Options) {
73
+ if (!Array.isArray(shifts)) {
74
+ return super
75
+ .fetch<ApiShift>({
76
+ url: `${this.apiPath}/${shifts.id}`,
77
+ data: shifts,
78
+ method: 'POST',
79
+ })
80
+ .then((res) => (options?.rawResponse ? res : new Shift(res.data)));
81
+ }
82
+
62
83
  return super
63
- .fetch<ApiShift>({
64
- url: `${this.apiPath}/${id}`,
65
- data,
84
+ .fetch<{ code: number; data?: ApiShift; error?: string }[]>({
85
+ url: this.apiPath,
86
+ data: shifts,
66
87
  method: 'POST',
67
88
  })
68
- .then((res) => Promise.resolve(options?.rawResponse ? res : new Shift(res.data)));
89
+ .then((res) => {
90
+ if (options?.rawResponse) return res;
91
+
92
+ const success: Shift[] = [];
93
+ const failed: { id: number; error: string }[] = [];
94
+ for (let shiftIdx = 0; shiftIdx < res.data.length; shiftIdx += 1) {
95
+ const { data, error } = res.data[shiftIdx];
96
+ if (data) success.push(new Shift(data));
97
+ if (error) failed.push({ id: shifts[shiftIdx].id, error });
98
+ }
99
+ return { success, failed };
100
+ });
69
101
  }
70
102
 
71
103
  delete(ids: number | number[]): Promise<number>;
72
104
  delete(ids: number | number[], options: { rawResponse: true } & Options): Promise<AxiosResponse<any, any>>;
73
105
  delete(ids: number | number[], options: Options): Promise<number>;
74
106
  delete(ids: number | number[], options?: Options) {
75
- const params: AxiosRequestConfig =
76
- typeof ids !== 'number'
77
- ? { url: this.apiPath, data: ids, method: 'DELETE' }
78
- : { url: `${this.apiPath}/${ids}`, method: 'DELETE' };
107
+ const params: AxiosRequestConfig = Array.isArray(ids)
108
+ ? { url: this.apiPath, data: { ids }, method: 'DELETE' }
109
+ : { url: `${this.apiPath}/${ids}`, method: 'DELETE' };
79
110
 
80
111
  return super.fetch<ApiShift>(params).then((res) => Promise.resolve(options?.rawResponse ? res : res.status));
81
112
  }
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const Version = { version: '1.0.53' };
1
+ export const Version = { version: '1.0.56' };