rotacloud 2.0.2 → 2.1.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/client-builder.d.ts +9 -1
- package/dist/client-builder.js +14 -0
- package/dist/endpoint.d.ts +8 -4
- package/dist/interfaces/index.d.ts +1 -2
- package/dist/interfaces/index.js +1 -2
- package/dist/interfaces/logbook.interface.d.ts +29 -0
- package/dist/interfaces/query-params/index.d.ts +0 -1
- package/dist/interfaces/query-params/index.js +0 -1
- package/dist/main.d.ts +16 -0
- package/dist/ops.js +16 -7
- package/dist/ops.test.js +123 -50
- package/dist/service.d.ts +27 -0
- package/dist/service.js +39 -1
- package/dist/utils.js +1 -1
- package/package.json +4 -4
- package/src/client-builder.ts +28 -1
- package/src/endpoint.ts +18 -3
- package/src/interfaces/index.ts +1 -2
- package/src/interfaces/logbook.interface.ts +31 -0
- package/src/interfaces/query-params/index.ts +0 -1
- package/src/ops.test.ts +133 -51
- package/src/ops.ts +20 -7
- package/src/service.ts +71 -3
- package/src/utils.ts +1 -1
- package/dist/interfaces/logbook-category.interface.d.ts +0 -5
- package/dist/interfaces/logbook-event.interface.d.ts +0 -16
- package/dist/interfaces/logbook-event.interface.js +0 -1
- package/dist/interfaces/query-params/logbook-events-query-params.interface.d.ts +0 -3
- package/dist/interfaces/query-params/logbook-events-query-params.interface.js +0 -1
- package/src/interfaces/logbook-category.interface.ts +0 -5
- package/src/interfaces/logbook-event.interface.ts +0 -16
- package/src/interfaces/query-params/logbook-events-query-params.interface.ts +0 -3
- /package/dist/interfaces/{logbook-category.interface.js → logbook.interface.js} +0 -0
package/dist/client-builder.d.ts
CHANGED
|
@@ -13,7 +13,15 @@ type ServiceOps<Spec extends ServiceSpecification> = {
|
|
|
13
13
|
type ServiceCustomOps<Spec extends ServiceSpecification> = {
|
|
14
14
|
[Key in keyof Spec['customOperations']]: Spec['customOperations'][Key] extends OpDef<any, any> ? ReturnType<typeof buildOp<Spec['customOperations'][Key]>> : never;
|
|
15
15
|
};
|
|
16
|
-
type
|
|
16
|
+
/** Mapped index type of all sub services defined on a provided {@link ServiceSpecification] */
|
|
17
|
+
type ServiceSubServices<Spec extends ServiceSpecification> = {
|
|
18
|
+
[Key in keyof Spec['subService']]: Spec['subService'][Key] extends ServiceSpecification ? Service<Spec['subService'][Key]> : never;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Service constructed from a provided {@link ServiceSpecification} consisting of
|
|
22
|
+
* built operation and custom operation methods and sub services
|
|
23
|
+
*/
|
|
24
|
+
type Service<Spec extends ServiceSpecification> = Omit<ServiceOps<Spec>, keyof ServiceCustomOps<Spec>> & ServiceCustomOps<Spec> & ServiceSubServices<Spec>;
|
|
17
25
|
export type SdkClient<T extends Record<string, ServiceSpecification>> = {
|
|
18
26
|
[ServiceName in keyof T]: Service<T[ServiceName]>;
|
|
19
27
|
} & {
|
package/dist/client-builder.js
CHANGED
|
@@ -19,6 +19,20 @@ function serviceForContext(opContext) {
|
|
|
19
19
|
for (const [customOpName, customOpFunc] of Object.entries(opContext.service.customOperations ?? {})) {
|
|
20
20
|
service[customOpName] = buildOp(opContext, customOpFunc);
|
|
21
21
|
}
|
|
22
|
+
for (const [subServiceName, subServiceSpec] of Object.entries(opContext.service.subService ?? {})) {
|
|
23
|
+
service[subServiceName] = serviceForContext({
|
|
24
|
+
get client() {
|
|
25
|
+
return opContext.client;
|
|
26
|
+
},
|
|
27
|
+
get request() {
|
|
28
|
+
return opContext.request;
|
|
29
|
+
},
|
|
30
|
+
get sdkConfig() {
|
|
31
|
+
return opContext.sdkConfig;
|
|
32
|
+
},
|
|
33
|
+
service: subServiceSpec,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
22
36
|
return service;
|
|
23
37
|
}
|
|
24
38
|
/** Builds the entire SDK client ready to be exported by the library and used by an end
|
package/dist/endpoint.d.ts
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
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 } from './interfaces/index.js';
|
|
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 } from './interfaces/index.js';
|
|
2
|
+
import { LogbookEntry, LogbookQueryParameters } from './interfaces/logbook.interface.js';
|
|
2
3
|
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';
|
|
3
4
|
import { RequirementsOf } from './utils.js';
|
|
4
5
|
/** Endpoint versions supported by the API */
|
|
5
6
|
export type EndpointVersion = 'v1' | 'v2';
|
|
6
7
|
/** Associated types for a given API endpoint */
|
|
7
|
-
export type Endpoint<Entity, QueryParameters = undefined, RequiredFields extends keyof Entity =
|
|
8
|
+
export type Endpoint<Entity, QueryParameters = undefined, CreateEntity extends keyof Entity | Partial<Entity> = any, RequiredFields extends keyof Entity = CreateEntity extends keyof Entity ? CreateEntity : never> = {
|
|
8
9
|
/** The type returned by an endpoint */
|
|
9
10
|
type: Entity;
|
|
10
11
|
/** The query parameters for endpoints that support listing */
|
|
11
12
|
queryParameters: QueryParameters;
|
|
12
13
|
/** The entity type required for endpoints that support creation */
|
|
13
|
-
createType: RequirementsOf<Entity, RequiredFields
|
|
14
|
+
createType: CreateEntity extends keyof Entity ? RequirementsOf<Entity, RequiredFields> : CreateEntity;
|
|
14
15
|
};
|
|
15
16
|
/** Mapping between a endpoint URL and it's associated entity type
|
|
16
17
|
*
|
|
@@ -47,5 +48,8 @@ export interface EndpointEntityMap extends Record<EndpointVersion, Record<string
|
|
|
47
48
|
users: Endpoint<User, UsersQueryParams, 'first_name' | 'last_name'>;
|
|
48
49
|
};
|
|
49
50
|
/** Type mappings for v2 endpoints */
|
|
50
|
-
v2: {
|
|
51
|
+
v2: {
|
|
52
|
+
logbook: Endpoint<LogbookEntry, LogbookQueryParameters, 'name' | 'description' | 'date' | 'userId'>;
|
|
53
|
+
'logbook/categories': Endpoint<LogbookCategory, undefined, Pick<LogbookCategory, 'name'>>;
|
|
54
|
+
};
|
|
51
55
|
}
|
|
@@ -20,8 +20,7 @@ export * from './leave-type.interface.js';
|
|
|
20
20
|
export * from './leave.interface.js';
|
|
21
21
|
export * from './location-coordinate.interface.js';
|
|
22
22
|
export * from './location.interface.js';
|
|
23
|
-
export * from './logbook
|
|
24
|
-
export * from './logbook-event.interface.js';
|
|
23
|
+
export * from './logbook.interface.js';
|
|
25
24
|
export * from './pay-period.interface.js';
|
|
26
25
|
export * from './pin.interface.js';
|
|
27
26
|
export * from './role-rate.interface.js';
|
package/dist/interfaces/index.js
CHANGED
|
@@ -20,8 +20,7 @@ export * from './leave-type.interface.js';
|
|
|
20
20
|
export * from './leave.interface.js';
|
|
21
21
|
export * from './location-coordinate.interface.js';
|
|
22
22
|
export * from './location.interface.js';
|
|
23
|
-
export * from './logbook
|
|
24
|
-
export * from './logbook-event.interface.js';
|
|
23
|
+
export * from './logbook.interface.js';
|
|
25
24
|
export * from './pay-period.interface.js';
|
|
26
25
|
export * from './pin.interface.js';
|
|
27
26
|
export * from './role-rate.interface.js';
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface LogbookCategory {
|
|
2
|
+
id: number;
|
|
3
|
+
name: string;
|
|
4
|
+
deleted: boolean;
|
|
5
|
+
/** Date and time in ISO 8601 format */
|
|
6
|
+
createdAt: string;
|
|
7
|
+
createdBy: number;
|
|
8
|
+
}
|
|
9
|
+
export interface LogbookEntry {
|
|
10
|
+
id: number;
|
|
11
|
+
name: string;
|
|
12
|
+
description: string;
|
|
13
|
+
categoryId: number;
|
|
14
|
+
/** Date of entry in ISO 8601 format */
|
|
15
|
+
date: string;
|
|
16
|
+
time: string | null;
|
|
17
|
+
/** Date and time in ISO 8601 format */
|
|
18
|
+
createdAt: string | null;
|
|
19
|
+
createdBy: number;
|
|
20
|
+
/** Date and time in ISO 8601 format */
|
|
21
|
+
updatedAt: string | null;
|
|
22
|
+
updatedBy: number | null;
|
|
23
|
+
userId: number;
|
|
24
|
+
}
|
|
25
|
+
export interface LogbookQueryParameters {
|
|
26
|
+
userId: number;
|
|
27
|
+
/** Date in ISO 8601 format */
|
|
28
|
+
date?: string;
|
|
29
|
+
}
|
|
@@ -13,7 +13,6 @@ export * from './leave-embargoes-query-params.interface.js';
|
|
|
13
13
|
export * from './leave-query-params.interface.js';
|
|
14
14
|
export * from './leave-requests-query-params.interface.js';
|
|
15
15
|
export * from './locations-query-params.interface.js';
|
|
16
|
-
export * from './logbook-events-query-params.interface.js';
|
|
17
16
|
export * from './pay-periods-query-params.interface.js';
|
|
18
17
|
export * from './roles-query-params.interface.js';
|
|
19
18
|
export * from './settings-query-params.interface.js';
|
|
@@ -13,7 +13,6 @@ export * from './leave-embargoes-query-params.interface.js';
|
|
|
13
13
|
export * from './leave-query-params.interface.js';
|
|
14
14
|
export * from './leave-requests-query-params.interface.js';
|
|
15
15
|
export * from './locations-query-params.interface.js';
|
|
16
|
-
export * from './logbook-events-query-params.interface.js';
|
|
17
16
|
export * from './pay-periods-query-params.interface.js';
|
|
18
17
|
export * from './roles-query-params.interface.js';
|
|
19
18
|
export * from './settings-query-params.interface.js';
|
package/dist/main.d.ts
CHANGED
|
@@ -92,6 +92,22 @@ export declare const createRotaCloudClient: (config: import("./interfaces/sdk-co
|
|
|
92
92
|
endpointVersion: "v1";
|
|
93
93
|
operations: ("get" | "delete" | "list" | "listAll" | "create" | "update")[];
|
|
94
94
|
};
|
|
95
|
+
logbook: {
|
|
96
|
+
endpoint: "logbook";
|
|
97
|
+
endpointVersion: "v2";
|
|
98
|
+
operations: ("get" | "delete" | "create" | "update")[];
|
|
99
|
+
customOperations: {
|
|
100
|
+
list: (ctx: import("./ops.js").OperationContext, query: import("./interfaces/logbook.interface.js").LogbookQueryParameters, opts: import("./utils.js").RequestOptions<unknown> | undefined) => AsyncGenerator<import("./interfaces/logbook.interface.js").LogbookEntry, any, any>;
|
|
101
|
+
listAll: (ctx: import("./ops.js").OperationContext, query: import("./interfaces/logbook.interface.js").LogbookQueryParameters, opts: import("./utils.js").RequestOptions<unknown> | undefined) => Promise<import("./interfaces/logbook.interface.js").LogbookEntry[]>;
|
|
102
|
+
};
|
|
103
|
+
subService: {
|
|
104
|
+
category: {
|
|
105
|
+
endpoint: "logbook/categories";
|
|
106
|
+
endpointVersion: "v2";
|
|
107
|
+
operations: ("get" | "delete" | "list" | "listAll" | "create" | "update")[];
|
|
108
|
+
};
|
|
109
|
+
};
|
|
110
|
+
};
|
|
95
111
|
pin: {
|
|
96
112
|
endpoint: "pins";
|
|
97
113
|
endpointVersion: "v1";
|
package/dist/ops.js
CHANGED
|
@@ -45,8 +45,8 @@ function createOp(ctx, newEntity) {
|
|
|
45
45
|
data: newEntity,
|
|
46
46
|
};
|
|
47
47
|
}
|
|
48
|
-
/** Operation for updating an entity */
|
|
49
|
-
function
|
|
48
|
+
/** Operation for updating an entity for v1 endpoints */
|
|
49
|
+
function updateV1Op(ctx, entity) {
|
|
50
50
|
return {
|
|
51
51
|
...ctx.request,
|
|
52
52
|
method: 'POST',
|
|
@@ -54,6 +54,15 @@ function updateOp(ctx, entity) {
|
|
|
54
54
|
data: entity,
|
|
55
55
|
};
|
|
56
56
|
}
|
|
57
|
+
/** Operation for updating an entity for v2 endpoints */
|
|
58
|
+
function updateV2Op(ctx, entity) {
|
|
59
|
+
return {
|
|
60
|
+
...ctx.request,
|
|
61
|
+
method: 'PUT',
|
|
62
|
+
url: `${ctx.service.endpointVersion}/${ctx.service.endpoint}/${entity.id}`,
|
|
63
|
+
data: entity,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
57
66
|
/** Operation for deleting a list of entities */
|
|
58
67
|
async function updateBatchOp(ctx, entities) {
|
|
59
68
|
const res = await ctx.client.request({
|
|
@@ -86,7 +95,7 @@ function deleteBatchOp(ctx, ids) {
|
|
|
86
95
|
...ctx.request,
|
|
87
96
|
method: 'DELETE',
|
|
88
97
|
url: `${ctx.service.endpointVersion}/${ctx.service.endpoint}`,
|
|
89
|
-
data: ids,
|
|
98
|
+
data: { ids },
|
|
90
99
|
};
|
|
91
100
|
}
|
|
92
101
|
/** Operation for listing all entities on a v1 endpoint for a given query by
|
|
@@ -228,7 +237,7 @@ async function* listByPageV2Op(ctx, query, opts) {
|
|
|
228
237
|
yield res;
|
|
229
238
|
if (entityCount >= maxEntities)
|
|
230
239
|
return;
|
|
231
|
-
let nextPage = res.data.pagination.next;
|
|
240
|
+
let nextPage = res.data.pagination.next ?? undefined;
|
|
232
241
|
while (nextPage !== undefined) {
|
|
233
242
|
const pagedRes = await ctx.client.request({
|
|
234
243
|
...queriedRequest,
|
|
@@ -237,7 +246,7 @@ async function* listByPageV2Op(ctx, query, opts) {
|
|
|
237
246
|
cursor: nextPage,
|
|
238
247
|
},
|
|
239
248
|
});
|
|
240
|
-
nextPage = pagedRes.data.pagination.next;
|
|
249
|
+
nextPage = pagedRes.data.pagination.next ?? undefined;
|
|
241
250
|
yield pagedRes;
|
|
242
251
|
entityCount += pagedRes.data.data.length;
|
|
243
252
|
if (entityCount >= maxEntities)
|
|
@@ -286,7 +295,7 @@ export function getOpMap() {
|
|
|
286
295
|
listAll: (listAllOp),
|
|
287
296
|
listByPage: (listByPageOp),
|
|
288
297
|
create: (createOp),
|
|
289
|
-
update: (
|
|
298
|
+
update: (updateV1Op),
|
|
290
299
|
updateBatch: (updateBatchOp),
|
|
291
300
|
},
|
|
292
301
|
v2: {
|
|
@@ -297,7 +306,7 @@ export function getOpMap() {
|
|
|
297
306
|
listAll: (listAllV2Op),
|
|
298
307
|
listByPage: (listByPageV2Op),
|
|
299
308
|
create: (createOp),
|
|
300
|
-
update: (
|
|
309
|
+
update: (updateV2Op),
|
|
301
310
|
updateBatch: (updateBatchOp),
|
|
302
311
|
},
|
|
303
312
|
};
|
package/dist/ops.test.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, expect, test, vi } from 'vitest';
|
|
1
|
+
import { afterEach, describe, expect, test, vi } from 'vitest';
|
|
2
2
|
import { createSdkClient } from './client-builder.js';
|
|
3
3
|
import { getOpMap } from './ops.js';
|
|
4
4
|
let mockAxiosClient;
|
|
@@ -18,7 +18,7 @@ vi.mock(import('axios'), async (importOriginal) => {
|
|
|
18
18
|
};
|
|
19
19
|
});
|
|
20
20
|
describe('Operations', () => {
|
|
21
|
-
const
|
|
21
|
+
const serviceV1 = {
|
|
22
22
|
endpoint: 'settings',
|
|
23
23
|
endpointVersion: 'v1',
|
|
24
24
|
operations: ['get', 'list'],
|
|
@@ -26,16 +26,28 @@ describe('Operations', () => {
|
|
|
26
26
|
promiseOp: async () => 3,
|
|
27
27
|
},
|
|
28
28
|
};
|
|
29
|
+
const serviceV2 = {
|
|
30
|
+
endpoint: 'logbook',
|
|
31
|
+
endpointVersion: 'v2',
|
|
32
|
+
operations: ['get', 'list'],
|
|
33
|
+
customOperations: {
|
|
34
|
+
promiseOp: async () => 3,
|
|
35
|
+
},
|
|
36
|
+
};
|
|
29
37
|
const clientBuilder = createSdkClient({
|
|
30
|
-
service,
|
|
38
|
+
service: serviceV1,
|
|
39
|
+
serviceV2,
|
|
31
40
|
});
|
|
32
41
|
const client = clientBuilder({ basicAuth: '' });
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
vi.restoreAllMocks();
|
|
44
|
+
});
|
|
33
45
|
test('operations returning a request config trigger a request call', async () => {
|
|
34
46
|
const basicOp = getOpMap().v1.get;
|
|
35
47
|
const basicOpRes = basicOp({
|
|
36
48
|
client: mockAxiosClient,
|
|
37
49
|
request: {},
|
|
38
|
-
service,
|
|
50
|
+
service: serviceV1,
|
|
39
51
|
sdkConfig: { apiKey: '' },
|
|
40
52
|
}, 1);
|
|
41
53
|
expect(basicOpRes).toStrictEqual(expect.objectContaining({
|
|
@@ -45,57 +57,118 @@ describe('Operations', () => {
|
|
|
45
57
|
expect(mockAxiosClient.request).toHaveBeenCalledWith(expect.objectContaining(basicOpRes));
|
|
46
58
|
});
|
|
47
59
|
test('operations returning a promise are returned as is', async () => {
|
|
48
|
-
const promiseOpRes = await
|
|
60
|
+
const promiseOpRes = await serviceV1.customOperations.promiseOp();
|
|
49
61
|
expect(promiseOpRes).toStrictEqual(await client.service.promiseOp());
|
|
50
62
|
});
|
|
51
|
-
describe('list
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
63
|
+
describe('list', () => {
|
|
64
|
+
describe('v1 ops', () => {
|
|
65
|
+
test('respects `maxResults` parameter in pagination', async () => {
|
|
66
|
+
vi.spyOn(mockAxiosClient, 'request').mockResolvedValue({ data: [] });
|
|
67
|
+
await client.service.list({}, { maxResults: 2 }).next();
|
|
68
|
+
expect(mockAxiosClient.request).toHaveBeenCalledWith(expect.objectContaining({
|
|
69
|
+
url: expect.any(String),
|
|
70
|
+
params: {
|
|
71
|
+
limit: 2,
|
|
72
|
+
},
|
|
73
|
+
}));
|
|
74
|
+
});
|
|
75
|
+
test('automatically paginates', async () => {
|
|
76
|
+
const pageTotal = 3;
|
|
77
|
+
let pageCount = 0;
|
|
78
|
+
vi.spyOn(mockAxiosClient, 'request').mockResolvedValue({
|
|
79
|
+
headers: {
|
|
80
|
+
'x-limit': 1,
|
|
81
|
+
'x-total-count': pageTotal,
|
|
82
|
+
// eslint-disable-next-line no-plusplus
|
|
83
|
+
'x-offset': pageCount++,
|
|
84
|
+
},
|
|
85
|
+
data: [],
|
|
86
|
+
});
|
|
87
|
+
for await (const res of client.service.list({})) {
|
|
88
|
+
res;
|
|
89
|
+
}
|
|
90
|
+
expect(mockAxiosClient.request).toHaveBeenCalledTimes(pageTotal);
|
|
91
|
+
});
|
|
92
|
+
test('stops automatic pagination after maxResults reached', async () => {
|
|
93
|
+
const pageLimit = 2;
|
|
94
|
+
const pageTotal = 6;
|
|
95
|
+
let pageCount = 0;
|
|
96
|
+
vi.spyOn(mockAxiosClient, 'request').mockResolvedValue({
|
|
97
|
+
headers: {
|
|
98
|
+
'x-limit': pageLimit,
|
|
99
|
+
'x-total-count': pageTotal,
|
|
100
|
+
// eslint-disable-next-line no-plusplus
|
|
101
|
+
'x-offset': pageCount++,
|
|
102
|
+
},
|
|
103
|
+
data: new Array(pageLimit).fill('entity'),
|
|
104
|
+
});
|
|
105
|
+
let resultCount = 0;
|
|
106
|
+
for await (const res of client.service.list({}, { maxResults: 3 })) {
|
|
107
|
+
resultCount += 1;
|
|
108
|
+
res;
|
|
109
|
+
}
|
|
110
|
+
expect(resultCount).toBe(3);
|
|
111
|
+
expect(mockAxiosClient.request).toHaveBeenCalledTimes(2);
|
|
73
112
|
});
|
|
74
|
-
for await (const res of client.service.list({})) {
|
|
75
|
-
res;
|
|
76
|
-
}
|
|
77
|
-
expect(mockAxiosClient.request).toHaveBeenCalledTimes(pageTotal);
|
|
78
113
|
});
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
114
|
+
describe('v2 ops', () => {
|
|
115
|
+
test('respects `maxResults` parameter in pagination', async () => {
|
|
116
|
+
vi.spyOn(mockAxiosClient, 'request').mockResolvedValue({
|
|
117
|
+
data: { data: [], pagination: { next: null, count: 0 } },
|
|
118
|
+
});
|
|
119
|
+
await client.serviceV2.list({ userId: 0 }, { maxResults: 2 }).next();
|
|
120
|
+
expect(mockAxiosClient.request).toHaveBeenCalledWith(expect.objectContaining({
|
|
121
|
+
url: expect.any(String),
|
|
122
|
+
params: expect.objectContaining({
|
|
123
|
+
limit: 2,
|
|
124
|
+
}),
|
|
125
|
+
}));
|
|
126
|
+
});
|
|
127
|
+
test('automatically paginates', async () => {
|
|
128
|
+
const pageTotal = 3;
|
|
129
|
+
const pageLimit = 2;
|
|
130
|
+
let pageCount = 0;
|
|
131
|
+
vi.spyOn(mockAxiosClient, 'request').mockImplementation(async () => {
|
|
132
|
+
pageCount += 1;
|
|
133
|
+
return {
|
|
134
|
+
data: {
|
|
135
|
+
data: new Array(pageLimit).fill('entity'),
|
|
136
|
+
pagination: {
|
|
137
|
+
next: pageCount < pageTotal ? String(pageCount) : null,
|
|
138
|
+
count: pageTotal,
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
});
|
|
143
|
+
for await (const res of client.serviceV2.list({ userId: 0 })) {
|
|
144
|
+
res;
|
|
145
|
+
}
|
|
146
|
+
expect(mockAxiosClient.request).toHaveBeenCalledTimes(pageTotal);
|
|
147
|
+
});
|
|
148
|
+
test('stops automatic pagination after maxResults reached', async () => {
|
|
149
|
+
const pageLimit = 2;
|
|
150
|
+
const pageTotal = 6;
|
|
151
|
+
let pageCount = 0;
|
|
152
|
+
vi.spyOn(mockAxiosClient, 'request').mockImplementation(() => {
|
|
153
|
+
pageCount += pageLimit;
|
|
154
|
+
return Promise.resolve({
|
|
155
|
+
data: {
|
|
156
|
+
data: new Array(pageLimit).fill('entity'),
|
|
157
|
+
pagination: {
|
|
158
|
+
next: pageCount < pageTotal ? String(pageCount) : null,
|
|
159
|
+
count: pageTotal,
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
let resultCount = 0;
|
|
165
|
+
for await (const res of client.serviceV2.list({ userId: 0 }, { maxResults: 3 })) {
|
|
166
|
+
resultCount += 1;
|
|
167
|
+
res;
|
|
168
|
+
}
|
|
169
|
+
expect(resultCount).toBe(3);
|
|
170
|
+
expect(mockAxiosClient.request).toHaveBeenCalledTimes(2);
|
|
91
171
|
});
|
|
92
|
-
let resultCount = 0;
|
|
93
|
-
for await (const res of client.service.list({}, { maxResults: 3 })) {
|
|
94
|
-
resultCount += 1;
|
|
95
|
-
res;
|
|
96
|
-
}
|
|
97
|
-
expect(resultCount).toBe(3);
|
|
98
|
-
expect(mockAxiosClient.request).toHaveBeenCalledTimes(2);
|
|
99
172
|
});
|
|
100
173
|
});
|
|
101
174
|
});
|
package/dist/service.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { RequirementsOf, RequestOptions } from './utils.js';
|
|
|
7
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
|
+
import { LogbookEntry, LogbookQueryParameters } from './interfaces/logbook.interface.js';
|
|
10
11
|
export type ServiceSpecification<CustomOp extends OpDef<unknown> = OpDef<any>> = {
|
|
11
12
|
/** Operations allowed and usable for the endpoint */
|
|
12
13
|
operations: Operation[];
|
|
@@ -15,6 +16,7 @@ export type ServiceSpecification<CustomOp extends OpDef<unknown> = OpDef<any>> =
|
|
|
15
16
|
* Can be used to override operations listed in {@see ServiceSpecification['operations']}
|
|
16
17
|
*/
|
|
17
18
|
customOperations?: Record<string, CustomOp>;
|
|
19
|
+
subService?: Record<string, ServiceSpecification<CustomOp>>;
|
|
18
20
|
} & ({
|
|
19
21
|
/** URL of the endpoint */
|
|
20
22
|
endpoint: keyof EndpointEntityMap['v1'];
|
|
@@ -25,6 +27,15 @@ export type ServiceSpecification<CustomOp extends OpDef<unknown> = OpDef<any>> =
|
|
|
25
27
|
endpoint: keyof EndpointEntityMap['v2'];
|
|
26
28
|
/** API version of the endpoint */
|
|
27
29
|
endpointVersion: 'v2';
|
|
30
|
+
} | {
|
|
31
|
+
endpoint: string;
|
|
32
|
+
endpointVersion: 'v1' | 'v2';
|
|
33
|
+
/**
|
|
34
|
+
* Marks the endpoint as one not defined in the {@link EndpointEntityMap}
|
|
35
|
+
*
|
|
36
|
+
* Intended for defining customOperations
|
|
37
|
+
* */
|
|
38
|
+
custom: true;
|
|
28
39
|
});
|
|
29
40
|
/**
|
|
30
41
|
* Map of all officially supported service specifications used to generate the
|
|
@@ -122,6 +133,22 @@ export declare const SERVICES: {
|
|
|
122
133
|
endpointVersion: "v1";
|
|
123
134
|
operations: ("get" | "delete" | "list" | "listAll" | "create" | "update")[];
|
|
124
135
|
};
|
|
136
|
+
logbook: {
|
|
137
|
+
endpoint: "logbook";
|
|
138
|
+
endpointVersion: "v2";
|
|
139
|
+
operations: ("get" | "delete" | "create" | "update")[];
|
|
140
|
+
customOperations: {
|
|
141
|
+
list: (ctx: OperationContext, query: LogbookQueryParameters, opts: RequestOptions<unknown> | undefined) => AsyncGenerator<LogbookEntry, any, any>;
|
|
142
|
+
listAll: (ctx: OperationContext, query: LogbookQueryParameters, opts: RequestOptions<unknown> | undefined) => Promise<LogbookEntry[]>;
|
|
143
|
+
};
|
|
144
|
+
subService: {
|
|
145
|
+
category: {
|
|
146
|
+
endpoint: "logbook/categories";
|
|
147
|
+
endpointVersion: "v2";
|
|
148
|
+
operations: ("get" | "delete" | "list" | "listAll" | "create" | "update")[];
|
|
149
|
+
};
|
|
150
|
+
};
|
|
151
|
+
};
|
|
125
152
|
pin: {
|
|
126
153
|
endpoint: "pins";
|
|
127
154
|
endpointVersion: "v1";
|
package/dist/service.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { listAllOp, listOp, paramsFromOptions } from './ops.js';
|
|
1
|
+
import { 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
|
|
@@ -141,6 +141,42 @@ export const SERVICES = {
|
|
|
141
141
|
endpointVersion: 'v1',
|
|
142
142
|
operations: ['create', 'get', 'list', 'listAll', 'update', 'delete'],
|
|
143
143
|
},
|
|
144
|
+
logbook: {
|
|
145
|
+
endpoint: 'logbook',
|
|
146
|
+
endpointVersion: 'v2',
|
|
147
|
+
operations: ['create', 'get', 'delete', 'update'],
|
|
148
|
+
customOperations: {
|
|
149
|
+
list: (ctx, query, opts) =>
|
|
150
|
+
// Maps the "userId" query parameter into the endpoint URL
|
|
151
|
+
listV2Op({
|
|
152
|
+
...ctx,
|
|
153
|
+
service: {
|
|
154
|
+
...ctx.service,
|
|
155
|
+
endpoint: `${ctx.service.endpoint}/user/${query.userId}`,
|
|
156
|
+
endpointVersion: 'v2',
|
|
157
|
+
custom: true,
|
|
158
|
+
},
|
|
159
|
+
}, { date: query.date }, opts),
|
|
160
|
+
listAll: (ctx, query, opts) =>
|
|
161
|
+
// Maps the "userId" query parameter into the endpoint URL
|
|
162
|
+
listAllV2Op({
|
|
163
|
+
...ctx,
|
|
164
|
+
service: {
|
|
165
|
+
...ctx.service,
|
|
166
|
+
endpoint: `${ctx.service.endpoint}/user/${query.userId}`,
|
|
167
|
+
endpointVersion: 'v2',
|
|
168
|
+
custom: true,
|
|
169
|
+
},
|
|
170
|
+
}, { date: query.date }, opts),
|
|
171
|
+
},
|
|
172
|
+
subService: {
|
|
173
|
+
category: {
|
|
174
|
+
endpoint: 'logbook/categories',
|
|
175
|
+
endpointVersion: 'v2',
|
|
176
|
+
operations: ['get', 'create', 'update', 'delete', 'list', 'listAll'],
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
},
|
|
144
180
|
pin: {
|
|
145
181
|
endpoint: 'pins',
|
|
146
182
|
endpointVersion: 'v1',
|
|
@@ -264,6 +300,7 @@ export const SERVICES = {
|
|
|
264
300
|
...ctx.service,
|
|
265
301
|
endpoint: `${ctx.service.endpoint}/${query.year}`,
|
|
266
302
|
endpointVersion: 'v1',
|
|
303
|
+
custom: true,
|
|
267
304
|
},
|
|
268
305
|
}, { users: query.users }, opts),
|
|
269
306
|
listAll: (ctx, query, opts) =>
|
|
@@ -274,6 +311,7 @@ export const SERVICES = {
|
|
|
274
311
|
...ctx.service,
|
|
275
312
|
endpoint: `${ctx.service.endpoint}/${query.year}`,
|
|
276
313
|
endpointVersion: 'v1',
|
|
314
|
+
custom: true,
|
|
277
315
|
},
|
|
278
316
|
}, { users: query.users }, opts),
|
|
279
317
|
},
|
package/dist/utils.js
CHANGED
|
@@ -42,7 +42,7 @@ function toSearchParams(parameters) {
|
|
|
42
42
|
}
|
|
43
43
|
function parseClientError(error) {
|
|
44
44
|
const axiosErrorLocation = error.response || error.request;
|
|
45
|
-
const apiErrorMessage = axiosErrorLocation.data?.
|
|
45
|
+
const apiErrorMessage = axiosErrorLocation.data?.message;
|
|
46
46
|
let url;
|
|
47
47
|
try {
|
|
48
48
|
url = new URL(error.config?.url ?? '', error.config?.baseURL);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rotacloud",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "The RotaCloud SDK for the RotaCloud API",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -40,9 +40,9 @@
|
|
|
40
40
|
"eslint-config-airbnb-typescript": "^17.1.0",
|
|
41
41
|
"eslint-config-prettier": "^9.0.0",
|
|
42
42
|
"eslint-plugin-import": "^2.31.0",
|
|
43
|
-
"prettier": "~3.
|
|
44
|
-
"typescript": "^5.
|
|
45
|
-
"vitest": "^
|
|
43
|
+
"prettier": "~3.5.0",
|
|
44
|
+
"typescript": "^5.7.3",
|
|
45
|
+
"vitest": "^3.0.5"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"axios": "^1.7.7",
|
package/src/client-builder.ts
CHANGED
|
@@ -26,8 +26,21 @@ type ServiceCustomOps<Spec extends ServiceSpecification> = {
|
|
|
26
26
|
? ReturnType<typeof buildOp<Spec['customOperations'][Key]>>
|
|
27
27
|
: never;
|
|
28
28
|
};
|
|
29
|
+
|
|
30
|
+
/** Mapped index type of all sub services defined on a provided {@link ServiceSpecification] */
|
|
31
|
+
type ServiceSubServices<Spec extends ServiceSpecification> = {
|
|
32
|
+
[Key in keyof Spec['subService']]: Spec['subService'][Key] extends ServiceSpecification
|
|
33
|
+
? Service<Spec['subService'][Key]>
|
|
34
|
+
: never;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Service constructed from a provided {@link ServiceSpecification} consisting of
|
|
39
|
+
* built operation and custom operation methods and sub services
|
|
40
|
+
*/
|
|
29
41
|
type Service<Spec extends ServiceSpecification> = Omit<ServiceOps<Spec>, keyof ServiceCustomOps<Spec>> &
|
|
30
|
-
ServiceCustomOps<Spec
|
|
42
|
+
ServiceCustomOps<Spec> &
|
|
43
|
+
ServiceSubServices<Spec>;
|
|
31
44
|
|
|
32
45
|
export type SdkClient<T extends Record<string, ServiceSpecification>> = {
|
|
33
46
|
[ServiceName in keyof T]: Service<T[ServiceName]>;
|
|
@@ -59,6 +72,20 @@ function serviceForContext<Spec extends ServiceSpecification>(opContext: Operati
|
|
|
59
72
|
for (const [customOpName, customOpFunc] of Object.entries(opContext.service.customOperations ?? {})) {
|
|
60
73
|
service[customOpName] = buildOp(opContext, customOpFunc);
|
|
61
74
|
}
|
|
75
|
+
for (const [subServiceName, subServiceSpec] of Object.entries(opContext.service.subService ?? {})) {
|
|
76
|
+
service[subServiceName] = serviceForContext({
|
|
77
|
+
get client() {
|
|
78
|
+
return opContext.client;
|
|
79
|
+
},
|
|
80
|
+
get request() {
|
|
81
|
+
return opContext.request;
|
|
82
|
+
},
|
|
83
|
+
get sdkConfig() {
|
|
84
|
+
return opContext.sdkConfig;
|
|
85
|
+
},
|
|
86
|
+
service: subServiceSpec,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
62
89
|
|
|
63
90
|
return service as Service<Spec>;
|
|
64
91
|
}
|
package/src/endpoint.ts
CHANGED
|
@@ -24,7 +24,9 @@ import {
|
|
|
24
24
|
UserClockedIn,
|
|
25
25
|
User,
|
|
26
26
|
Settings,
|
|
27
|
+
LogbookCategory,
|
|
27
28
|
} from './interfaces/index.js';
|
|
29
|
+
import { LogbookEntry, LogbookQueryParameters } from './interfaces/logbook.interface.js';
|
|
28
30
|
import {
|
|
29
31
|
AttendanceQueryParams,
|
|
30
32
|
AvailabilityQueryParams,
|
|
@@ -51,13 +53,23 @@ import { RequirementsOf } from './utils.js';
|
|
|
51
53
|
/** Endpoint versions supported by the API */
|
|
52
54
|
export type EndpointVersion = 'v1' | 'v2';
|
|
53
55
|
/** Associated types for a given API endpoint */
|
|
54
|
-
export type Endpoint<
|
|
56
|
+
export type Endpoint<
|
|
57
|
+
Entity,
|
|
58
|
+
QueryParameters = undefined,
|
|
59
|
+
CreateEntity extends keyof Entity | Partial<Entity> = any,
|
|
60
|
+
// NOTE: introduced to work around TS inferring `RequirementsOf<Entity, CreateEntity>` incorrectly
|
|
61
|
+
// TS resolves type to:
|
|
62
|
+
// `RequirementsOf<Entity, "key 1"> | RequirementsOf<Entity "key 2">`
|
|
63
|
+
// instead of:
|
|
64
|
+
// `RequirementsOf<Entity, "key 1" | "key 2">`
|
|
65
|
+
RequiredFields extends keyof Entity = CreateEntity extends keyof Entity ? CreateEntity : never,
|
|
66
|
+
> = {
|
|
55
67
|
/** The type returned by an endpoint */
|
|
56
68
|
type: Entity;
|
|
57
69
|
/** The query parameters for endpoints that support listing */
|
|
58
70
|
queryParameters: QueryParameters;
|
|
59
71
|
/** The entity type required for endpoints that support creation */
|
|
60
|
-
createType: RequirementsOf<Entity, RequiredFields
|
|
72
|
+
createType: CreateEntity extends keyof Entity ? RequirementsOf<Entity, RequiredFields> : CreateEntity;
|
|
61
73
|
};
|
|
62
74
|
|
|
63
75
|
/** Mapping between a endpoint URL and it's associated entity type
|
|
@@ -95,5 +107,8 @@ export interface EndpointEntityMap extends Record<EndpointVersion, Record<string
|
|
|
95
107
|
users: Endpoint<User, UsersQueryParams, 'first_name' | 'last_name'>;
|
|
96
108
|
};
|
|
97
109
|
/** Type mappings for v2 endpoints */
|
|
98
|
-
v2: {
|
|
110
|
+
v2: {
|
|
111
|
+
logbook: Endpoint<LogbookEntry, LogbookQueryParameters, 'name' | 'description' | 'date' | 'userId'>;
|
|
112
|
+
'logbook/categories': Endpoint<LogbookCategory, undefined, Pick<LogbookCategory, 'name'>>;
|
|
113
|
+
};
|
|
99
114
|
}
|
package/src/interfaces/index.ts
CHANGED
|
@@ -20,8 +20,7 @@ export * from './leave-type.interface.js';
|
|
|
20
20
|
export * from './leave.interface.js';
|
|
21
21
|
export * from './location-coordinate.interface.js';
|
|
22
22
|
export * from './location.interface.js';
|
|
23
|
-
export * from './logbook
|
|
24
|
-
export * from './logbook-event.interface.js';
|
|
23
|
+
export * from './logbook.interface.js';
|
|
25
24
|
export * from './pay-period.interface.js';
|
|
26
25
|
export * from './pin.interface.js';
|
|
27
26
|
export * from './role-rate.interface.js';
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export interface LogbookCategory {
|
|
2
|
+
id: number;
|
|
3
|
+
name: string;
|
|
4
|
+
deleted: boolean;
|
|
5
|
+
/** Date and time in ISO 8601 format */
|
|
6
|
+
createdAt: string;
|
|
7
|
+
createdBy: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface LogbookEntry {
|
|
11
|
+
id: number;
|
|
12
|
+
name: string;
|
|
13
|
+
description: string;
|
|
14
|
+
categoryId: number;
|
|
15
|
+
/** Date of entry in ISO 8601 format */
|
|
16
|
+
date: string;
|
|
17
|
+
time: string | null;
|
|
18
|
+
/** Date and time in ISO 8601 format */
|
|
19
|
+
createdAt: string | null;
|
|
20
|
+
createdBy: number;
|
|
21
|
+
/** Date and time in ISO 8601 format */
|
|
22
|
+
updatedAt: string | null;
|
|
23
|
+
updatedBy: number | null;
|
|
24
|
+
userId: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface LogbookQueryParameters {
|
|
28
|
+
userId: number;
|
|
29
|
+
/** Date in ISO 8601 format */
|
|
30
|
+
date?: string;
|
|
31
|
+
}
|
|
@@ -13,7 +13,6 @@ export * from './leave-embargoes-query-params.interface.js';
|
|
|
13
13
|
export * from './leave-query-params.interface.js';
|
|
14
14
|
export * from './leave-requests-query-params.interface.js';
|
|
15
15
|
export * from './locations-query-params.interface.js';
|
|
16
|
-
export * from './logbook-events-query-params.interface.js';
|
|
17
16
|
export * from './pay-periods-query-params.interface.js';
|
|
18
17
|
export * from './roles-query-params.interface.js';
|
|
19
18
|
export * from './settings-query-params.interface.js';
|
package/src/ops.test.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, expect, test, vi } from 'vitest';
|
|
1
|
+
import { afterEach, describe, expect, test, vi } from 'vitest';
|
|
2
2
|
import { Axios } from 'axios';
|
|
3
3
|
import { createSdkClient } from './client-builder.js';
|
|
4
4
|
import { getOpMap } from './ops.js';
|
|
@@ -22,7 +22,7 @@ vi.mock(import('axios'), async (importOriginal) => {
|
|
|
22
22
|
});
|
|
23
23
|
|
|
24
24
|
describe('Operations', () => {
|
|
25
|
-
const
|
|
25
|
+
const serviceV1 = {
|
|
26
26
|
endpoint: 'settings',
|
|
27
27
|
endpointVersion: 'v1',
|
|
28
28
|
operations: ['get', 'list'],
|
|
@@ -30,18 +30,31 @@ describe('Operations', () => {
|
|
|
30
30
|
promiseOp: async () => 3,
|
|
31
31
|
},
|
|
32
32
|
} satisfies ServiceSpecification;
|
|
33
|
+
const serviceV2 = {
|
|
34
|
+
endpoint: 'logbook',
|
|
35
|
+
endpointVersion: 'v2',
|
|
36
|
+
operations: ['get', 'list'],
|
|
37
|
+
customOperations: {
|
|
38
|
+
promiseOp: async () => 3,
|
|
39
|
+
},
|
|
40
|
+
} satisfies ServiceSpecification;
|
|
33
41
|
const clientBuilder = createSdkClient({
|
|
34
|
-
service,
|
|
42
|
+
service: serviceV1,
|
|
43
|
+
serviceV2,
|
|
35
44
|
});
|
|
36
45
|
const client = clientBuilder({ basicAuth: '' });
|
|
37
46
|
|
|
47
|
+
afterEach(() => {
|
|
48
|
+
vi.restoreAllMocks();
|
|
49
|
+
});
|
|
50
|
+
|
|
38
51
|
test('operations returning a request config trigger a request call', async () => {
|
|
39
52
|
const basicOp = getOpMap().v1.get;
|
|
40
53
|
const basicOpRes = basicOp(
|
|
41
54
|
{
|
|
42
55
|
client: mockAxiosClient,
|
|
43
56
|
request: {},
|
|
44
|
-
service,
|
|
57
|
+
service: serviceV1,
|
|
45
58
|
sdkConfig: { apiKey: '' },
|
|
46
59
|
},
|
|
47
60
|
1,
|
|
@@ -57,65 +70,134 @@ describe('Operations', () => {
|
|
|
57
70
|
});
|
|
58
71
|
|
|
59
72
|
test('operations returning a promise are returned as is', async () => {
|
|
60
|
-
const promiseOpRes = await
|
|
73
|
+
const promiseOpRes = await serviceV1.customOperations.promiseOp();
|
|
61
74
|
expect(promiseOpRes).toStrictEqual(await client.service.promiseOp());
|
|
62
75
|
});
|
|
63
76
|
|
|
64
|
-
describe('list
|
|
65
|
-
|
|
66
|
-
|
|
77
|
+
describe('list', () => {
|
|
78
|
+
describe('v1 ops', () => {
|
|
79
|
+
test('respects `maxResults` parameter in pagination', async () => {
|
|
80
|
+
vi.spyOn(mockAxiosClient, 'request').mockResolvedValue({ data: [] });
|
|
67
81
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
82
|
+
await client.service.list({}, { maxResults: 2 }).next();
|
|
83
|
+
expect(mockAxiosClient.request).toHaveBeenCalledWith(
|
|
84
|
+
expect.objectContaining({
|
|
85
|
+
url: expect.any(String),
|
|
86
|
+
params: {
|
|
87
|
+
limit: 2,
|
|
88
|
+
},
|
|
89
|
+
}),
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('automatically paginates', async () => {
|
|
94
|
+
const pageTotal = 3;
|
|
95
|
+
let pageCount = 0;
|
|
96
|
+
vi.spyOn(mockAxiosClient, 'request').mockResolvedValue({
|
|
97
|
+
headers: {
|
|
98
|
+
'x-limit': 1,
|
|
99
|
+
'x-total-count': pageTotal,
|
|
100
|
+
// eslint-disable-next-line no-plusplus
|
|
101
|
+
'x-offset': pageCount++,
|
|
74
102
|
},
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
});
|
|
103
|
+
data: [],
|
|
104
|
+
});
|
|
78
105
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
headers: {
|
|
84
|
-
'x-limit': 1,
|
|
85
|
-
'x-total-count': pageTotal,
|
|
86
|
-
// eslint-disable-next-line no-plusplus
|
|
87
|
-
'x-offset': pageCount++,
|
|
88
|
-
},
|
|
89
|
-
data: [],
|
|
106
|
+
for await (const res of client.service.list({})) {
|
|
107
|
+
res;
|
|
108
|
+
}
|
|
109
|
+
expect(mockAxiosClient.request).toHaveBeenCalledTimes(pageTotal);
|
|
90
110
|
});
|
|
91
111
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
112
|
+
test('stops automatic pagination after maxResults reached', async () => {
|
|
113
|
+
const pageLimit = 2;
|
|
114
|
+
const pageTotal = 6;
|
|
115
|
+
let pageCount = 0;
|
|
116
|
+
vi.spyOn(mockAxiosClient, 'request').mockResolvedValue({
|
|
117
|
+
headers: {
|
|
118
|
+
'x-limit': pageLimit,
|
|
119
|
+
'x-total-count': pageTotal,
|
|
120
|
+
// eslint-disable-next-line no-plusplus
|
|
121
|
+
'x-offset': pageCount++,
|
|
122
|
+
},
|
|
123
|
+
data: new Array(pageLimit).fill('entity'),
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
let resultCount = 0;
|
|
127
|
+
for await (const res of client.service.list({}, { maxResults: 3 })) {
|
|
128
|
+
resultCount += 1;
|
|
129
|
+
res;
|
|
130
|
+
}
|
|
131
|
+
expect(resultCount).toBe(3);
|
|
132
|
+
expect(mockAxiosClient.request).toHaveBeenCalledTimes(2);
|
|
133
|
+
});
|
|
96
134
|
});
|
|
97
135
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
136
|
+
describe('v2 ops', () => {
|
|
137
|
+
test('respects `maxResults` parameter in pagination', async () => {
|
|
138
|
+
vi.spyOn(mockAxiosClient, 'request').mockResolvedValue({
|
|
139
|
+
data: { data: [], pagination: { next: null, count: 0 } },
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
await client.serviceV2.list({ userId: 0 }, { maxResults: 2 }).next();
|
|
143
|
+
expect(mockAxiosClient.request).toHaveBeenCalledWith(
|
|
144
|
+
expect.objectContaining({
|
|
145
|
+
url: expect.any(String),
|
|
146
|
+
params: expect.objectContaining({
|
|
147
|
+
limit: 2,
|
|
148
|
+
}),
|
|
149
|
+
}),
|
|
150
|
+
);
|
|
110
151
|
});
|
|
111
152
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
153
|
+
test('automatically paginates', async () => {
|
|
154
|
+
const pageTotal = 3;
|
|
155
|
+
const pageLimit = 2;
|
|
156
|
+
let pageCount = 0;
|
|
157
|
+
vi.spyOn(mockAxiosClient, 'request').mockImplementation(async () => {
|
|
158
|
+
pageCount += 1;
|
|
159
|
+
return {
|
|
160
|
+
data: {
|
|
161
|
+
data: new Array(pageLimit).fill('entity'),
|
|
162
|
+
pagination: {
|
|
163
|
+
next: pageCount < pageTotal ? String(pageCount) : null,
|
|
164
|
+
count: pageTotal,
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
for await (const res of client.serviceV2.list({ userId: 0 })) {
|
|
171
|
+
res;
|
|
172
|
+
}
|
|
173
|
+
expect(mockAxiosClient.request).toHaveBeenCalledTimes(pageTotal);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test('stops automatic pagination after maxResults reached', async () => {
|
|
177
|
+
const pageLimit = 2;
|
|
178
|
+
const pageTotal = 6;
|
|
179
|
+
let pageCount = 0;
|
|
180
|
+
vi.spyOn(mockAxiosClient, 'request').mockImplementation(() => {
|
|
181
|
+
pageCount += pageLimit;
|
|
182
|
+
return Promise.resolve({
|
|
183
|
+
data: {
|
|
184
|
+
data: new Array(pageLimit).fill('entity'),
|
|
185
|
+
pagination: {
|
|
186
|
+
next: pageCount < pageTotal ? String(pageCount) : null,
|
|
187
|
+
count: pageTotal,
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
let resultCount = 0;
|
|
194
|
+
for await (const res of client.serviceV2.list({ userId: 0 }, { maxResults: 3 })) {
|
|
195
|
+
resultCount += 1;
|
|
196
|
+
res;
|
|
197
|
+
}
|
|
198
|
+
expect(resultCount).toBe(3);
|
|
199
|
+
expect(mockAxiosClient.request).toHaveBeenCalledTimes(2);
|
|
200
|
+
});
|
|
119
201
|
});
|
|
120
202
|
});
|
|
121
203
|
});
|
package/src/ops.ts
CHANGED
|
@@ -220,8 +220,8 @@ function createOp<T = unknown, NewEntity = unknown>(
|
|
|
220
220
|
};
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
-
/** Operation for updating an entity */
|
|
224
|
-
function
|
|
223
|
+
/** Operation for updating an entity for v1 endpoints */
|
|
224
|
+
function updateV1Op<Return, Entity extends { id: number } & Partial<Return>>(
|
|
225
225
|
ctx: OperationContext,
|
|
226
226
|
entity: Entity,
|
|
227
227
|
): RequestConfig<Entity, Return> {
|
|
@@ -233,6 +233,19 @@ function updateOp<Return, Entity extends { id: number } & Partial<Return>>(
|
|
|
233
233
|
};
|
|
234
234
|
}
|
|
235
235
|
|
|
236
|
+
/** Operation for updating an entity for v2 endpoints */
|
|
237
|
+
function updateV2Op<Return, Entity extends { id: number } & Partial<Return>>(
|
|
238
|
+
ctx: OperationContext,
|
|
239
|
+
entity: Entity,
|
|
240
|
+
): RequestConfig<Entity, Return> {
|
|
241
|
+
return {
|
|
242
|
+
...ctx.request,
|
|
243
|
+
method: 'PUT',
|
|
244
|
+
url: `${ctx.service.endpointVersion}/${ctx.service.endpoint}/${entity.id}`,
|
|
245
|
+
data: entity,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
236
249
|
/** Operation for deleting a list of entities */
|
|
237
250
|
async function updateBatchOp<Return, Entity extends { id: number } & Partial<Return>>(
|
|
238
251
|
ctx: OperationContext,
|
|
@@ -273,7 +286,7 @@ function deleteBatchOp(ctx: OperationContext, ids: number[]): RequestConfig<unkn
|
|
|
273
286
|
...ctx.request,
|
|
274
287
|
method: 'DELETE',
|
|
275
288
|
url: `${ctx.service.endpointVersion}/${ctx.service.endpoint}`,
|
|
276
|
-
data: ids,
|
|
289
|
+
data: { ids },
|
|
277
290
|
};
|
|
278
291
|
}
|
|
279
292
|
|
|
@@ -439,7 +452,7 @@ async function* listByPageV2Op<T, Query>(
|
|
|
439
452
|
yield res;
|
|
440
453
|
if (entityCount >= maxEntities) return;
|
|
441
454
|
|
|
442
|
-
let nextPage = res.data.pagination.next;
|
|
455
|
+
let nextPage = res.data.pagination.next ?? undefined;
|
|
443
456
|
while (nextPage !== undefined) {
|
|
444
457
|
const pagedRes = await ctx.client.request<PagedResponse<T>>({
|
|
445
458
|
...queriedRequest,
|
|
@@ -448,7 +461,7 @@ async function* listByPageV2Op<T, Query>(
|
|
|
448
461
|
cursor: nextPage,
|
|
449
462
|
},
|
|
450
463
|
});
|
|
451
|
-
nextPage = pagedRes.data.pagination.next;
|
|
464
|
+
nextPage = pagedRes.data.pagination.next ?? undefined;
|
|
452
465
|
yield pagedRes;
|
|
453
466
|
entityCount += pagedRes.data.data.length;
|
|
454
467
|
if (entityCount >= maxEntities) return;
|
|
@@ -515,7 +528,7 @@ export function getOpMap<E extends Endpoint<any, any>, T extends E['type'] = E['
|
|
|
515
528
|
listAll: listAllOp<T, E['queryParameters']>,
|
|
516
529
|
listByPage: listByPageOp<T, E['queryParameters']>,
|
|
517
530
|
create: createOp<T, E['createType']>,
|
|
518
|
-
update:
|
|
531
|
+
update: updateV1Op<T, T extends { id: number } ? RequirementsOf<T, 'id'> : never>,
|
|
519
532
|
updateBatch: updateBatchOp<T, T extends { id: number } ? RequirementsOf<T, 'id'> : never>,
|
|
520
533
|
},
|
|
521
534
|
v2: {
|
|
@@ -526,7 +539,7 @@ export function getOpMap<E extends Endpoint<any, any>, T extends E['type'] = E['
|
|
|
526
539
|
listAll: listAllV2Op<T, E['queryParameters']>,
|
|
527
540
|
listByPage: listByPageV2Op<T, E['queryParameters']>,
|
|
528
541
|
create: createOp<T, E['createType']>,
|
|
529
|
-
update:
|
|
542
|
+
update: updateV2Op<T, T extends { id: number } ? RequirementsOf<T, 'id'> : never>,
|
|
530
543
|
updateBatch: updateBatchOp<T, T extends { id: number } ? RequirementsOf<T, 'id'> : never>,
|
|
531
544
|
},
|
|
532
545
|
} satisfies Record<EndpointVersion, Record<Operation, OpDef<any, any>>>;
|
package/src/service.ts
CHANGED
|
@@ -14,12 +14,23 @@ import {
|
|
|
14
14
|
UserClockedOut,
|
|
15
15
|
} from './interfaces/index.js';
|
|
16
16
|
import { LaunchTerminal } from './interfaces/launch-terminal.interface.js';
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
OpDef,
|
|
19
|
+
Operation,
|
|
20
|
+
OperationContext,
|
|
21
|
+
RequestConfig,
|
|
22
|
+
listAllOp,
|
|
23
|
+
listAllV2Op,
|
|
24
|
+
listOp,
|
|
25
|
+
listV2Op,
|
|
26
|
+
paramsFromOptions,
|
|
27
|
+
} from './ops.js';
|
|
18
28
|
import { UserBreakRequest, UserClockIn, UserClockOut } from './interfaces/user-clock-in.interface.js';
|
|
19
29
|
import { RequirementsOf, RequestOptions } from './utils.js';
|
|
20
30
|
import { ShiftSwapRequest } from './interfaces/swap-request.interface.js';
|
|
21
31
|
import { ShiftDropRequest } from './interfaces/drop-request.interface.js';
|
|
22
32
|
import { ToilAllowanceQueryParams } from './interfaces/query-params/index.js';
|
|
33
|
+
import { LogbookEntry, LogbookQueryParameters } from './interfaces/logbook.interface.js';
|
|
23
34
|
|
|
24
35
|
export type ServiceSpecification<CustomOp extends OpDef<unknown> = OpDef<any>> = {
|
|
25
36
|
/** Operations allowed and usable for the endpoint */
|
|
@@ -29,6 +40,7 @@ export type ServiceSpecification<CustomOp extends OpDef<unknown> = OpDef<any>> =
|
|
|
29
40
|
* Can be used to override operations listed in {@see ServiceSpecification['operations']}
|
|
30
41
|
*/
|
|
31
42
|
customOperations?: Record<string, CustomOp>;
|
|
43
|
+
subService?: Record<string, ServiceSpecification<CustomOp>>;
|
|
32
44
|
} & (
|
|
33
45
|
| {
|
|
34
46
|
/** URL of the endpoint */
|
|
@@ -42,6 +54,16 @@ export type ServiceSpecification<CustomOp extends OpDef<unknown> = OpDef<any>> =
|
|
|
42
54
|
/** API version of the endpoint */
|
|
43
55
|
endpointVersion: 'v2';
|
|
44
56
|
}
|
|
57
|
+
| {
|
|
58
|
+
endpoint: string;
|
|
59
|
+
endpointVersion: 'v1' | 'v2';
|
|
60
|
+
/**
|
|
61
|
+
* Marks the endpoint as one not defined in the {@link EndpointEntityMap}
|
|
62
|
+
*
|
|
63
|
+
* Intended for defining customOperations
|
|
64
|
+
* */
|
|
65
|
+
custom: true;
|
|
66
|
+
}
|
|
45
67
|
);
|
|
46
68
|
|
|
47
69
|
/**
|
|
@@ -193,6 +215,50 @@ export const SERVICES = {
|
|
|
193
215
|
endpointVersion: 'v1',
|
|
194
216
|
operations: ['create', 'get', 'list', 'listAll', 'update', 'delete'],
|
|
195
217
|
},
|
|
218
|
+
logbook: {
|
|
219
|
+
endpoint: 'logbook',
|
|
220
|
+
endpointVersion: 'v2',
|
|
221
|
+
operations: ['create', 'get', 'delete', 'update'],
|
|
222
|
+
customOperations: {
|
|
223
|
+
list: (ctx, query: LogbookQueryParameters, opts) =>
|
|
224
|
+
// Maps the "userId" query parameter into the endpoint URL
|
|
225
|
+
listV2Op<LogbookEntry, Omit<typeof query, 'userId'>>(
|
|
226
|
+
{
|
|
227
|
+
...ctx,
|
|
228
|
+
service: {
|
|
229
|
+
...ctx.service,
|
|
230
|
+
endpoint: `${ctx.service.endpoint}/user/${query.userId}`,
|
|
231
|
+
endpointVersion: 'v2',
|
|
232
|
+
custom: true,
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
{ date: query.date },
|
|
236
|
+
opts,
|
|
237
|
+
),
|
|
238
|
+
listAll: (ctx, query: LogbookQueryParameters, opts) =>
|
|
239
|
+
// Maps the "userId" query parameter into the endpoint URL
|
|
240
|
+
listAllV2Op<LogbookEntry, Omit<typeof query, 'userId'>>(
|
|
241
|
+
{
|
|
242
|
+
...ctx,
|
|
243
|
+
service: {
|
|
244
|
+
...ctx.service,
|
|
245
|
+
endpoint: `${ctx.service.endpoint}/user/${query.userId}`,
|
|
246
|
+
endpointVersion: 'v2',
|
|
247
|
+
custom: true,
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
{ date: query.date },
|
|
251
|
+
opts,
|
|
252
|
+
),
|
|
253
|
+
},
|
|
254
|
+
subService: {
|
|
255
|
+
category: {
|
|
256
|
+
endpoint: 'logbook/categories',
|
|
257
|
+
endpointVersion: 'v2',
|
|
258
|
+
operations: ['get', 'create', 'update', 'delete', 'list', 'listAll'],
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
},
|
|
196
262
|
pin: {
|
|
197
263
|
endpoint: 'pins',
|
|
198
264
|
endpointVersion: 'v1',
|
|
@@ -321,8 +387,9 @@ export const SERVICES = {
|
|
|
321
387
|
...ctx,
|
|
322
388
|
service: {
|
|
323
389
|
...ctx.service,
|
|
324
|
-
endpoint: `${ctx.service.endpoint}/${query.year}
|
|
390
|
+
endpoint: `${ctx.service.endpoint}/${query.year}`,
|
|
325
391
|
endpointVersion: 'v1',
|
|
392
|
+
custom: true,
|
|
326
393
|
},
|
|
327
394
|
},
|
|
328
395
|
{ users: query.users },
|
|
@@ -335,8 +402,9 @@ export const SERVICES = {
|
|
|
335
402
|
...ctx,
|
|
336
403
|
service: {
|
|
337
404
|
...ctx.service,
|
|
338
|
-
endpoint: `${ctx.service.endpoint}/${query.year}
|
|
405
|
+
endpoint: `${ctx.service.endpoint}/${query.year}`,
|
|
339
406
|
endpointVersion: 'v1',
|
|
407
|
+
custom: true,
|
|
340
408
|
},
|
|
341
409
|
},
|
|
342
410
|
{ users: query.users },
|
package/src/utils.ts
CHANGED
|
@@ -73,7 +73,7 @@ function toSearchParams(parameters?: Record<string, QueryParameterValue>): URLSe
|
|
|
73
73
|
|
|
74
74
|
function parseClientError(error: AxiosError): SDKError {
|
|
75
75
|
const axiosErrorLocation = error.response || error.request;
|
|
76
|
-
const apiErrorMessage = axiosErrorLocation.data?.
|
|
76
|
+
const apiErrorMessage = axiosErrorLocation.data?.message;
|
|
77
77
|
let url: URL | undefined;
|
|
78
78
|
try {
|
|
79
79
|
url = new URL(error.config?.url ?? '', error.config?.baseURL);
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
export interface LogbookEvent {
|
|
2
|
-
id: number;
|
|
3
|
-
created_at: number;
|
|
4
|
-
created_by: number;
|
|
5
|
-
updated_at: string;
|
|
6
|
-
updated_by: string;
|
|
7
|
-
deleted: boolean;
|
|
8
|
-
deleted_at: string;
|
|
9
|
-
deleted_by: string;
|
|
10
|
-
user: number;
|
|
11
|
-
category: number;
|
|
12
|
-
date: string;
|
|
13
|
-
time: string;
|
|
14
|
-
name: string;
|
|
15
|
-
description: string;
|
|
16
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
export interface LogbookEvent {
|
|
2
|
-
id: number;
|
|
3
|
-
created_at: number;
|
|
4
|
-
created_by: number;
|
|
5
|
-
updated_at: string;
|
|
6
|
-
updated_by: string;
|
|
7
|
-
deleted: boolean;
|
|
8
|
-
deleted_at: string;
|
|
9
|
-
deleted_by: string;
|
|
10
|
-
user: number;
|
|
11
|
-
category: number;
|
|
12
|
-
date: string;
|
|
13
|
-
time: string;
|
|
14
|
-
name: string;
|
|
15
|
-
description: string;
|
|
16
|
-
}
|
|
File without changes
|