@intellegens/cornerstone-client 0.0.9999-alpha-29 → 0.0.9999-alpha-31
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/adapters/CollectionViewAdapter/index.integration.test.d.ts +1 -0
- package/dist/adapters/CollectionViewAdapter/index.integration.test.js +163 -0
- package/dist/adapters/SearchAdapter/index.d.ts +5 -1
- package/dist/adapters/SearchAdapter/index.js +25 -13
- package/dist/data/api/enum/read/ReadSelectedPropertyType.d.ts +4 -0
- package/dist/data/api/enum/read/ReadSelectedPropertyType.js +4 -0
- package/dist/data/auth/dto/index.d.ts +1 -0
- package/dist/data/auth/dto/index.js +1 -0
- package/dist/services/api/ApiCrudControllerClient/index.integration.test.d.ts +1 -0
- package/dist/services/api/ApiCrudControllerClient/index.integration.test.js +34 -0
- package/dist/services/api/ApiReadControllerClient/index.integration.test.d.ts +1 -0
- package/dist/services/api/ApiReadControllerClient/index.integration.test.js +59 -0
- package/dist/services/api/HttpService/FetchHttpService.integration.test.d.ts +1 -0
- package/dist/services/api/HttpService/FetchHttpService.integration.test.js +52 -0
- package/dist/services/api/UserManagementControllerClient/index.d.ts +0 -6
- package/dist/services/api/UserManagementControllerClient/index.integration.test.d.ts +1 -0
- package/dist/services/api/UserManagementControllerClient/index.integration.test.js +60 -0
- package/dist/services/api/UserManagementControllerClient/index.js +0 -29
- package/dist/services/auth/client/AuthService/index.d.ts +10 -2
- package/dist/services/auth/client/AuthService/index.js +40 -0
- package/dist/services/auth/client/AuthorizationManagementControllerClient/index.integration.test.d.ts +1 -0
- package/dist/services/auth/client/AuthorizationManagementControllerClient/index.integration.test.js +89 -0
- package/package.json +6 -8
- package/src/adapters/CollectionViewAdapter/index.integration.test.ts +197 -0
- package/src/adapters/SearchAdapter/index.ts +30 -13
- package/src/data/api/dto/response/ApiSuccessResponseDto.ts +1 -1
- package/src/data/api/enum/read/ReadSelectedPropertyType.ts +5 -0
- package/src/data/auth/dto/index.ts +1 -0
- package/src/services/api/ApiCrudControllerClient/index.integration.test.ts +46 -0
- package/src/services/api/ApiReadControllerClient/index.integration.test.ts +71 -0
- package/src/services/api/HttpService/FetchHttpService.integration.test.ts +65 -0
- package/src/services/api/UserManagementControllerClient/index.integration.test.ts +69 -0
- package/src/services/api/UserManagementControllerClient/index.ts +0 -32
- package/src/services/auth/client/AuthService/index.ts +48 -2
- package/src/services/auth/client/AuthorizationManagementControllerClient/index.integration.test.ts +110 -0
- package/vitest-setup.ts +43 -0
- package/vitest.config.ts +59 -0
- package/jest.config.js +0 -29
- package/tests/ApiClients.test.ts +0 -284
- package/tests/CollectionViewAdapter.test.ts +0 -392
- package/tests/HttpService.test.ts +0 -303
- package/tests/setup.ts +0 -76
|
@@ -143,6 +143,46 @@ export class AuthService {
|
|
|
143
143
|
});
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
|
+
/**
|
|
147
|
+
* Registers a new user by sending user details to the API.
|
|
148
|
+
*
|
|
149
|
+
* @param {TRegisterUser} user User object containing registration data (e.g. username, password, and optionally firstName, lastName, etc.)
|
|
150
|
+
* @return {Promise<ApiResponseDto<TUser, EmptyMetadataDto>>} API response with registered user details or error info
|
|
151
|
+
* @throws {Error} Error if the request fails
|
|
152
|
+
*/
|
|
153
|
+
async register(user) {
|
|
154
|
+
try {
|
|
155
|
+
const url = await apiInitializationService.getApiUrl(this.baseControllerPath, '/register');
|
|
156
|
+
const res = await this.httpService.request(url, {
|
|
157
|
+
method: 'POST',
|
|
158
|
+
headers: { 'Content-Type': 'application/json' },
|
|
159
|
+
body: JSON.stringify(user),
|
|
160
|
+
credentials: 'include',
|
|
161
|
+
});
|
|
162
|
+
if (!res.ok) {
|
|
163
|
+
return fail(await res.json());
|
|
164
|
+
}
|
|
165
|
+
// TODO: Handle tokens if not using cookies
|
|
166
|
+
const whoAmIRes = await this.whoAmI();
|
|
167
|
+
if (!whoAmIRes.ok) {
|
|
168
|
+
return whoAmIRes;
|
|
169
|
+
}
|
|
170
|
+
return ok({
|
|
171
|
+
result: whoAmIRes.result.user,
|
|
172
|
+
metadata: whoAmIRes.metadata,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
catch (err) {
|
|
176
|
+
console.error(err);
|
|
177
|
+
return fail({
|
|
178
|
+
error: {
|
|
179
|
+
code: ErrorCode.UnknownError,
|
|
180
|
+
message: 'Failed registering user',
|
|
181
|
+
metadata: {},
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
146
186
|
/**
|
|
147
187
|
* If any API response returns an "Unauthenticated" response, call this method
|
|
148
188
|
* to update local authentication state to unauthenticated
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/services/auth/client/AuthorizationManagementControllerClient/index.integration.test.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import { AuthorizationManagementControllerClient } from '.';
|
|
3
|
+
import fetchOrig from 'node-fetch';
|
|
4
|
+
import fetchCookie from 'fetch-cookie';
|
|
5
|
+
import { CookieJar } from 'tough-cookie';
|
|
6
|
+
// Create a fetch instance that handles auth cookies
|
|
7
|
+
const jar = new CookieJar();
|
|
8
|
+
const cookieFetch = fetchCookie(fetchOrig, jar);
|
|
9
|
+
// Custom IHttpService implementation for tests
|
|
10
|
+
class CookieFetchHttpService {
|
|
11
|
+
async request(url, config) {
|
|
12
|
+
const response = await cookieFetch(url, {
|
|
13
|
+
method: config?.method || 'GET',
|
|
14
|
+
headers: config?.headers,
|
|
15
|
+
body: config?.body,
|
|
16
|
+
signal: config?.signal,
|
|
17
|
+
});
|
|
18
|
+
const headers = {};
|
|
19
|
+
response.headers.forEach((value, key) => {
|
|
20
|
+
headers[key] = value;
|
|
21
|
+
});
|
|
22
|
+
return {
|
|
23
|
+
ok: response.ok,
|
|
24
|
+
status: response.status,
|
|
25
|
+
statusText: response.statusText,
|
|
26
|
+
headers,
|
|
27
|
+
json: () => response.json(),
|
|
28
|
+
text: () => response.text(),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const credentials = {
|
|
33
|
+
username: 'admin@test.com',
|
|
34
|
+
password: 'Password1234!',
|
|
35
|
+
};
|
|
36
|
+
const baseUrl = 'http://localhost:5000/api';
|
|
37
|
+
describe('AuthorizationManagementControllerClient', () => {
|
|
38
|
+
let client;
|
|
39
|
+
let fetchService = new CookieFetchHttpService();
|
|
40
|
+
beforeAll(async () => {
|
|
41
|
+
await fetchService.request(`${baseUrl}/auth/signin`, {
|
|
42
|
+
method: 'POST',
|
|
43
|
+
headers: { 'Content-Type': 'application/json' },
|
|
44
|
+
body: JSON.stringify(credentials),
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
client = new AuthorizationManagementControllerClient('/AuthorizationManagement', fetchService);
|
|
49
|
+
});
|
|
50
|
+
it('should fetch all roles', async () => {
|
|
51
|
+
const result = (await client.getAllRoles(false));
|
|
52
|
+
expect(result.ok).toBe(true);
|
|
53
|
+
expect(result.result).toBeDefined();
|
|
54
|
+
});
|
|
55
|
+
it('should fetch all roles with claims', async () => {
|
|
56
|
+
const result = (await client.getAllRoles(true));
|
|
57
|
+
expect(result.ok).toBe(true);
|
|
58
|
+
expect(result.result).toBeDefined();
|
|
59
|
+
expect(result.result[0].claims).toBeDefined();
|
|
60
|
+
});
|
|
61
|
+
it('should return role with claims by name', async () => {
|
|
62
|
+
const newRole = { name: 'New Role Test Claims', claims: [{ type: 'Test Claim Type', value: 'Test Claim Value' }] };
|
|
63
|
+
const create = (await client.upsertRole(newRole));
|
|
64
|
+
expect(create.ok).toBe(true);
|
|
65
|
+
const result = (await client.getRoleWithClaims(newRole.name));
|
|
66
|
+
expect(result.ok).toBe(true);
|
|
67
|
+
expect(result.result).toBeDefined();
|
|
68
|
+
expect(result.result.claims).toBeDefined();
|
|
69
|
+
expect(result.result.name).toBe(newRole.name);
|
|
70
|
+
expect(result.result.claims.length).toBe(newRole.claims.length);
|
|
71
|
+
expect(result.result.claims).toEqual(newRole.claims);
|
|
72
|
+
});
|
|
73
|
+
it('should create a new role with claims', async () => {
|
|
74
|
+
const newRole = { name: 'New Role', claims: [{ type: 'Test Claim Type', value: 'Test Claim Value' }] };
|
|
75
|
+
const result = (await client.upsertRole(newRole));
|
|
76
|
+
expect(result.ok).toBe(true);
|
|
77
|
+
expect(result.result).toBeDefined();
|
|
78
|
+
expect(result.result.name).toBe(newRole.name);
|
|
79
|
+
expect(result.result.claims.length).toBe(newRole.claims.length);
|
|
80
|
+
expect(result.result.claims).toEqual(newRole.claims);
|
|
81
|
+
});
|
|
82
|
+
it('should delete role', async () => {
|
|
83
|
+
const role = { name: 'Temp Role', claims: [] };
|
|
84
|
+
const create = (await client.upsertRole(role));
|
|
85
|
+
expect(create.ok).toBe(true);
|
|
86
|
+
const deleteResult = await client.deleteRole(role.name);
|
|
87
|
+
expect(deleteResult.ok).toBe(true);
|
|
88
|
+
});
|
|
89
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intellegens/cornerstone-client",
|
|
3
|
-
"version": "0.0.9999-alpha-
|
|
3
|
+
"version": "0.0.9999-alpha-31",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishable": true,
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -15,22 +15,18 @@
|
|
|
15
15
|
],
|
|
16
16
|
"devDependencies": {
|
|
17
17
|
"@eslint/js": "^9.21.0",
|
|
18
|
-
"@jest/globals": "^30.2.0",
|
|
19
18
|
"@types/express": "^5.0.0",
|
|
20
|
-
"@types/jest": "^29.5.12",
|
|
21
19
|
"@types/node": "^22.13.9",
|
|
22
20
|
"eslint": "^9.21.0",
|
|
23
21
|
"eslint-config-prettier": "^10.0.1",
|
|
24
22
|
"express": "^4.21.2",
|
|
25
23
|
"globals": "^16.0.0",
|
|
26
|
-
"jest": "^29.7.0",
|
|
27
|
-
"jest-environment-jsdom": "^29.7.0",
|
|
28
24
|
"prettier": "^3.5.2",
|
|
29
25
|
"rxjs": "~7.8.0",
|
|
30
|
-
"ts-jest": "^29.1.2",
|
|
31
26
|
"tsc-alias": "^1.8.11",
|
|
32
27
|
"typescript-eslint": "^8.26.0",
|
|
33
28
|
"vite": "6.3.4",
|
|
29
|
+
"vitest": "^3.2.4",
|
|
34
30
|
"zod": "^3.24.2"
|
|
35
31
|
},
|
|
36
32
|
"dependencies": {
|
|
@@ -39,8 +35,10 @@
|
|
|
39
35
|
},
|
|
40
36
|
"scripts": {
|
|
41
37
|
"clean": "pnpx rimraf '{dist}'",
|
|
42
|
-
"test": "
|
|
43
|
-
"
|
|
38
|
+
"test": "vitest run --passWithNoTests",
|
|
39
|
+
"test:unit": "vitest run --project unit --passWithNoTests",
|
|
40
|
+
"test:integration": "vitest run --project integration --passWithNoTests",
|
|
41
|
+
"coverage": "vitest run --coverage",
|
|
44
42
|
"build": "pnpm run clean && tsc && tsc-alias",
|
|
45
43
|
"start": "pnpm run build && pnpx vite build && pnpx tsx ./demo"
|
|
46
44
|
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import { CollectionViewAdapter, CollectionViewAdapterOptions } from '.';
|
|
3
|
+
import { ReadSelectedComparisonOperator, ReadSelectedOrderingDirection, ReadSelectedPropertyType } from '@/data';
|
|
4
|
+
import { dateInterval, searchTerm } from '@/utils';
|
|
5
|
+
|
|
6
|
+
type TestDto = { id: number; name: string; dateOfBirth: string; rowVersion: string };
|
|
7
|
+
|
|
8
|
+
describe('CollectionViewAdapter', () => {
|
|
9
|
+
let data: TestDto[] | undefined;
|
|
10
|
+
let error: string | undefined;
|
|
11
|
+
let isLoading: boolean | undefined;
|
|
12
|
+
|
|
13
|
+
const callback = vi.fn((_isLoading: boolean, _data: TestDto[] | undefined, _error: string | undefined) => {
|
|
14
|
+
isLoading = _isLoading;
|
|
15
|
+
data = _data;
|
|
16
|
+
error = _error;
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const options: CollectionViewAdapterOptions<TestDto> = {
|
|
20
|
+
pagination: { useTotalItemCount: true, pageSize: 5, pageNumber: 1 },
|
|
21
|
+
ordering: { maxActiveOrderingColumns: 1, orderByPaths: [] },
|
|
22
|
+
search: { textSearchableProperties: ['name'], numericSearchableProperties: ['id'] },
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
let adapter: CollectionViewAdapter<number, TestDto, TestDto>;
|
|
26
|
+
|
|
27
|
+
const waitForData = async (timeout = 2000) => {
|
|
28
|
+
const start = Date.now();
|
|
29
|
+
while (isLoading !== false && Date.now() - start < timeout) {
|
|
30
|
+
await new Promise(res => setTimeout(res, 50));
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const resetAdapterState = () => {
|
|
35
|
+
data = undefined;
|
|
36
|
+
error = undefined;
|
|
37
|
+
isLoading = undefined;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
data = undefined;
|
|
42
|
+
error = undefined;
|
|
43
|
+
isLoading = undefined;
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('callback returns correct loading status and collection data', async () => {
|
|
47
|
+
options.pagination.useTotalItemCount = true;
|
|
48
|
+
adapter = new CollectionViewAdapter('AnythingManuallyMappedRead', callback, options);
|
|
49
|
+
await waitForData();
|
|
50
|
+
|
|
51
|
+
expect(callback).toHaveBeenNthCalledWith(1, true, undefined, undefined);
|
|
52
|
+
expect(callback).toHaveBeenLastCalledWith(false, data, undefined);
|
|
53
|
+
expect(data).toBeDefined();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('adapter options set correct ordering', async () => {
|
|
57
|
+
adapter = new CollectionViewAdapter('AnythingManuallyMappedRead', callback, options);
|
|
58
|
+
await waitForData();
|
|
59
|
+
resetAdapterState();
|
|
60
|
+
adapter.setOrdering(['name'], ReadSelectedOrderingDirection.Descending);
|
|
61
|
+
await waitForData();
|
|
62
|
+
|
|
63
|
+
expect(adapter.getCurrentOrdering()).toEqual([
|
|
64
|
+
{
|
|
65
|
+
orderByPath: ['name'],
|
|
66
|
+
orderDirection: ReadSelectedOrderingDirection.Descending,
|
|
67
|
+
},
|
|
68
|
+
]);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('text search is applied to adapter', async () => {
|
|
72
|
+
const searchDefinition = searchTerm('Flu', ['name'], ['id']);
|
|
73
|
+
if (!searchDefinition) throw new Error('searchDefinition is undefined');
|
|
74
|
+
adapter = new CollectionViewAdapter('AnythingManuallyMappedRead', callback, options);
|
|
75
|
+
await waitForData();
|
|
76
|
+
resetAdapterState();
|
|
77
|
+
adapter.setSearchDefinition(searchDefinition);
|
|
78
|
+
await waitForData();
|
|
79
|
+
|
|
80
|
+
expect(data).toBeDefined();
|
|
81
|
+
expect(data!.length).toBeGreaterThan(0);
|
|
82
|
+
for (const item of data!) {
|
|
83
|
+
expect(item.name).toMatch(/flu/i);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('numeric search is applied to adapter', async () => {
|
|
88
|
+
const searchDefinition = searchTerm('10000001', ['id'], ['id']);
|
|
89
|
+
if (!searchDefinition) throw new Error('searchDefinition is undefined');
|
|
90
|
+
adapter = new CollectionViewAdapter('AnythingManuallyMappedRead', callback, options);
|
|
91
|
+
await waitForData();
|
|
92
|
+
resetAdapterState();
|
|
93
|
+
adapter.setSearchDefinition(searchDefinition);
|
|
94
|
+
await waitForData();
|
|
95
|
+
|
|
96
|
+
expect(data).toBeDefined();
|
|
97
|
+
expect(data!.length).toBe(1);
|
|
98
|
+
for (const item of data!) {
|
|
99
|
+
expect(item.id).toBe(10000001);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('applies date range filter from-to on the same date type property', async () => {
|
|
104
|
+
adapter = new CollectionViewAdapter('PetsSlim', callback, options);
|
|
105
|
+
const from = new Date('2017-01-01T00:00:00.000Z');
|
|
106
|
+
const to = new Date('2017-12-31T00:00:00.000Z');
|
|
107
|
+
|
|
108
|
+
const searchDefinition = dateInterval<TestDto>(
|
|
109
|
+
from,
|
|
110
|
+
to,
|
|
111
|
+
ReadSelectedComparisonOperator.GreaterOrEqual,
|
|
112
|
+
ReadSelectedComparisonOperator.LessOrEqual,
|
|
113
|
+
ReadSelectedPropertyType.DateOnly,
|
|
114
|
+
'dateOfBirth',
|
|
115
|
+
);
|
|
116
|
+
if (!searchDefinition) throw new Error('searchDefinition is undefined');
|
|
117
|
+
await waitForData();
|
|
118
|
+
resetAdapterState();
|
|
119
|
+
adapter.setSearchDefinition(searchDefinition);
|
|
120
|
+
await waitForData();
|
|
121
|
+
|
|
122
|
+
expect(data).toBeDefined();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('applies pagination: page size and jump to page set skip/limit', async () => {
|
|
126
|
+
adapter = new CollectionViewAdapter('AnythingManuallyMappedRead', callback, options);
|
|
127
|
+
await waitForData();
|
|
128
|
+
const firstPageData = data;
|
|
129
|
+
resetAdapterState();
|
|
130
|
+
adapter.jumpToPage(3);
|
|
131
|
+
await waitForData();
|
|
132
|
+
|
|
133
|
+
expect(data).toBeDefined();
|
|
134
|
+
expect(data!.length).toBe(5);
|
|
135
|
+
// Check that data is different from first page
|
|
136
|
+
expect(data).not.toEqual(firstPageData);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('calculates total when useTotalItemCount=false and last page is partial', async () => {
|
|
140
|
+
options.pagination.useTotalItemCount = false;
|
|
141
|
+
options.pagination.pageSize = 10;
|
|
142
|
+
adapter = new CollectionViewAdapter('AnythingManuallyMappedRead', callback, options);
|
|
143
|
+
await waitForData();
|
|
144
|
+
const firstPageData = data;
|
|
145
|
+
resetAdapterState();
|
|
146
|
+
adapter.jumpToPage(2);
|
|
147
|
+
await waitForData();
|
|
148
|
+
|
|
149
|
+
expect(adapter.totalItemCount).toBe(firstPageData!.length + data!.length);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('setOrdering correctly handles PropertyPathDto array comparison and limits columns', async () => {
|
|
153
|
+
options.ordering.maxActiveOrderingColumns = 2;
|
|
154
|
+
// Add first ordering
|
|
155
|
+
adapter = new CollectionViewAdapter('AnythingManuallyMappedRead', callback, options);
|
|
156
|
+
adapter.setOrdering(['name'], ReadSelectedOrderingDirection.Descending);
|
|
157
|
+
await waitForData();
|
|
158
|
+
|
|
159
|
+
expect(adapter.getCurrentOrdering()).toEqual([
|
|
160
|
+
{
|
|
161
|
+
orderByPath: ['name'],
|
|
162
|
+
orderDirection: ReadSelectedOrderingDirection.Descending,
|
|
163
|
+
},
|
|
164
|
+
]);
|
|
165
|
+
|
|
166
|
+
// Add second ordering
|
|
167
|
+
adapter.setOrdering(['id'], ReadSelectedOrderingDirection.Ascending);
|
|
168
|
+
await waitForData();
|
|
169
|
+
|
|
170
|
+
// Should have both orderings, with 'id' first (most recent)
|
|
171
|
+
expect(adapter.getCurrentOrdering()).toEqual([
|
|
172
|
+
{
|
|
173
|
+
orderByPath: ['id'],
|
|
174
|
+
orderDirection: ReadSelectedOrderingDirection.Ascending,
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
orderByPath: ['name'],
|
|
178
|
+
orderDirection: ReadSelectedOrderingDirection.Descending,
|
|
179
|
+
},
|
|
180
|
+
]);
|
|
181
|
+
|
|
182
|
+
// // Add third ordering - should remove the oldest one (name)
|
|
183
|
+
adapter.setOrdering(['description'], ReadSelectedOrderingDirection.Descending);
|
|
184
|
+
await waitForData();
|
|
185
|
+
|
|
186
|
+
expect(adapter.getCurrentOrdering()).toEqual([
|
|
187
|
+
{
|
|
188
|
+
orderByPath: ['description'],
|
|
189
|
+
orderDirection: ReadSelectedOrderingDirection.Descending,
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
orderByPath: ['id'],
|
|
193
|
+
orderDirection: ReadSelectedOrderingDirection.Ascending,
|
|
194
|
+
},
|
|
195
|
+
]);
|
|
196
|
+
});
|
|
197
|
+
});
|
|
@@ -41,28 +41,41 @@ export class SearchAdapter<TKey, TDto extends IGlobalSearchable<TKey>> {
|
|
|
41
41
|
public onChange = new SingularEventTarget<string>();
|
|
42
42
|
// inputs
|
|
43
43
|
private _readClient!: ApiReadControllerClient<TKey, TDto, TDto>;
|
|
44
|
-
|
|
44
|
+
public options!: SearchAdapterOptions<TDto>;
|
|
45
45
|
private _isLoading = false;
|
|
46
46
|
private _currentAbortController?: AbortController;
|
|
47
|
-
private _lastSearchedValue =
|
|
47
|
+
private _lastSearchedValue: string | undefined = undefined;
|
|
48
48
|
private _typesToSearch: string[] = [];
|
|
49
49
|
private _fetchResultsDataTimeout?: any;
|
|
50
50
|
private _fetchResultsPromises: {
|
|
51
51
|
resolve: (value: TDto[] | PromiseLike<TDto[]>) => void;
|
|
52
52
|
reject: (reason?: unknown) => void;
|
|
53
53
|
}[] = [];
|
|
54
|
+
private _totalCount = 0;
|
|
54
55
|
|
|
55
56
|
constructor(options: SearchAdapterOptions<TDto>) {
|
|
56
|
-
this.
|
|
57
|
+
this.options = options;
|
|
57
58
|
this._readClient = new ApiReadControllerClient<TKey, TDto, TDto>(options.controllerName);
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
public get searchTriggerMinLength() {
|
|
61
|
-
return this.
|
|
62
|
+
return this.options.searchTriggerMinLength;
|
|
62
63
|
}
|
|
63
64
|
|
|
64
65
|
public get multiselect() {
|
|
65
|
-
return this.
|
|
66
|
+
return this.options.multiselect;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
public get isLoading() {
|
|
70
|
+
return this._isLoading;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
public get totalCount() {
|
|
74
|
+
return this._totalCount;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
public get hasMoreRecords() {
|
|
78
|
+
return this._totalCount > this._searchResults.length;
|
|
66
79
|
}
|
|
67
80
|
|
|
68
81
|
private _searchText: string = '';
|
|
@@ -105,7 +118,7 @@ export class SearchAdapter<TKey, TDto extends IGlobalSearchable<TKey>> {
|
|
|
105
118
|
}
|
|
106
119
|
|
|
107
120
|
// selection mode
|
|
108
|
-
if (this.
|
|
121
|
+
if (this.options.multiselect) {
|
|
109
122
|
if (!this._selectedItems.find(s => s.id === item.id)) {
|
|
110
123
|
this.selectedItems = [...this.selectedItems, item];
|
|
111
124
|
}
|
|
@@ -134,20 +147,22 @@ export class SearchAdapter<TKey, TDto extends IGlobalSearchable<TKey>> {
|
|
|
134
147
|
}
|
|
135
148
|
|
|
136
149
|
private async _trySearch() {
|
|
137
|
-
const options = this.
|
|
150
|
+
const options = this.options;
|
|
138
151
|
|
|
139
152
|
this.searchResults = [];
|
|
140
153
|
if (this._searchText.length < options.searchTriggerMinLength) return;
|
|
141
|
-
if (this._searchText
|
|
154
|
+
if (this._searchText === this._lastSearchedValue) return;
|
|
142
155
|
this._lastSearchedValue = this._searchText;
|
|
143
156
|
|
|
144
157
|
this._typesToSearch =
|
|
145
|
-
options.limitSearchToSelectedType && this._selectedItems.length ? [(this._selectedItems[0] as TDto).type] : this.
|
|
158
|
+
options.limitSearchToSelectedType && this._selectedItems.length ? [(this._selectedItems[0] as TDto).type] : this.options.typesToSearch;
|
|
146
159
|
|
|
147
160
|
this.searchResults = this._sortByName(await this._fetchResults(options.searchDebounceDelay));
|
|
148
161
|
}
|
|
149
162
|
|
|
150
163
|
private async _fetchResults(debounceDelay: number): Promise<TDto[]> {
|
|
164
|
+
this._isLoading = true;
|
|
165
|
+
|
|
151
166
|
return new Promise((resolve, reject) => {
|
|
152
167
|
this._fetchResultsPromises.push({ resolve, reject });
|
|
153
168
|
if (this._fetchResultsDataTimeout !== undefined) {
|
|
@@ -167,7 +182,6 @@ export class SearchAdapter<TKey, TDto extends IGlobalSearchable<TKey>> {
|
|
|
167
182
|
}
|
|
168
183
|
|
|
169
184
|
private async _fetchResultsDebounced(): Promise<TDto[]> {
|
|
170
|
-
this._isLoading = true;
|
|
171
185
|
let abortController: AbortController | undefined;
|
|
172
186
|
let caughtError: unknown;
|
|
173
187
|
|
|
@@ -190,6 +204,9 @@ export class SearchAdapter<TKey, TDto extends IGlobalSearchable<TKey>> {
|
|
|
190
204
|
throw response.error;
|
|
191
205
|
}
|
|
192
206
|
|
|
207
|
+
// Update total count from response metadata
|
|
208
|
+
this._totalCount = response.metadata?.totalCount ?? response.result.length;
|
|
209
|
+
|
|
193
210
|
// Filter out selected items from new results
|
|
194
211
|
const selectedIds = new Set((this.selectedItems || []).map(item => item.id));
|
|
195
212
|
return response.result.filter(item => !selectedIds.has(item.id));
|
|
@@ -222,7 +239,7 @@ export class SearchAdapter<TKey, TDto extends IGlobalSearchable<TKey>> {
|
|
|
222
239
|
const definition: ReadSelectedDefinitionDto<TDto> = {
|
|
223
240
|
paginationDefinition: {
|
|
224
241
|
skip: 0,
|
|
225
|
-
limit: this.
|
|
242
|
+
limit: this.options.resultsLimit,
|
|
226
243
|
},
|
|
227
244
|
};
|
|
228
245
|
|
|
@@ -268,8 +285,8 @@ export class SearchAdapter<TKey, TDto extends IGlobalSearchable<TKey>> {
|
|
|
268
285
|
];
|
|
269
286
|
}
|
|
270
287
|
|
|
271
|
-
if (this.
|
|
272
|
-
definition.searchDefinition.searches = [...(definition.searchDefinition.searches ?? []), ...this.
|
|
288
|
+
if (this.options.additionalSearchDefinitions && this.options.additionalSearchDefinitions.length > 0) {
|
|
289
|
+
definition.searchDefinition.searches = [...(definition.searchDefinition.searches ?? []), ...this.options.additionalSearchDefinitions];
|
|
273
290
|
}
|
|
274
291
|
|
|
275
292
|
return definition;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import { ApiCrudControllerClient } from '.';
|
|
3
|
+
import { FetchHttpService } from '../HttpService';
|
|
4
|
+
|
|
5
|
+
type VehicleSlimDto = { id: number; brand: string; model: string };
|
|
6
|
+
|
|
7
|
+
describe('ApiCrudControllerClient', () => {
|
|
8
|
+
let client: ApiCrudControllerClient<number, any, any, any, any>;
|
|
9
|
+
let fetchService: FetchHttpService;
|
|
10
|
+
const testId = 100;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
fetchService = new FetchHttpService();
|
|
14
|
+
client = new ApiCrudControllerClient('/VehiclesSlim', fetchService);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should use injected HTTP service for create', async () => {
|
|
18
|
+
const newVehicle: VehicleSlimDto = { id: testId, brand: 'New Brand', model: 'New Model' };
|
|
19
|
+
|
|
20
|
+
const result = (await client.create(newVehicle)) as { ok: boolean; result: any };
|
|
21
|
+
|
|
22
|
+
expect(result.ok).toBe(true);
|
|
23
|
+
expect(result.result).toBeDefined();
|
|
24
|
+
expect(result.result.id).toBe(newVehicle.id);
|
|
25
|
+
expect(result.result.brand).toBe(newVehicle.brand);
|
|
26
|
+
expect(result.result.model).toBe(newVehicle.model);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should use injected HTTP service for update', async () => {
|
|
30
|
+
const updateVehicle: VehicleSlimDto = { id: testId, brand: 'Updated Brand', model: 'Updated Model' };
|
|
31
|
+
|
|
32
|
+
const result = (await client.update(testId, updateVehicle)) as { ok: boolean; result: any };
|
|
33
|
+
|
|
34
|
+
expect(result.ok).toBe(true);
|
|
35
|
+
expect(result.result).toBeDefined();
|
|
36
|
+
expect(result.result.id).toBe(updateVehicle.id);
|
|
37
|
+
expect(result.result.brand).toBe(updateVehicle.brand);
|
|
38
|
+
expect(result.result.model).toBe(updateVehicle.model);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should use injected HTTP service for delete', async () => {
|
|
42
|
+
const result = await client.delete(testId);
|
|
43
|
+
|
|
44
|
+
expect(result.ok).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import { ApiReadControllerClient } from '.';
|
|
3
|
+
import { FetchHttpService } from '../HttpService';
|
|
4
|
+
import { ReadSelectedComparisonOperator, ReadSelectedDefinitionDto, ReadSelectedLogicalOperator, ReadSelectedPropertyType } from '@/data';
|
|
5
|
+
|
|
6
|
+
type VehicleSlimDto = { Id: number; Brand: string; Model: string; Year: number; RowVersion: string };
|
|
7
|
+
|
|
8
|
+
describe('ApiReadControllerClient', () => {
|
|
9
|
+
let client: ApiReadControllerClient<number, any, any>;
|
|
10
|
+
let fetchService: FetchHttpService;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
fetchService = new FetchHttpService();
|
|
14
|
+
client = new ApiReadControllerClient('/VehiclesSlim', fetchService);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should use injected HTTP service for readAll', async () => {
|
|
18
|
+
const result = (await client.readAll()) as { ok: boolean; result: any };
|
|
19
|
+
|
|
20
|
+
expect(result.ok).toBe(true);
|
|
21
|
+
expect(result.result).toBeDefined();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should use injected HTTP service for readSingle', async () => {
|
|
25
|
+
const testId = 1;
|
|
26
|
+
const result = (await client.readSingle(testId)) as { ok: boolean; result: any };
|
|
27
|
+
|
|
28
|
+
expect(result.ok).toBe(true);
|
|
29
|
+
expect(result.result).toBeDefined();
|
|
30
|
+
expect(typeof result.result).toBe('object');
|
|
31
|
+
expect(result.result.id).toBe(testId);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should use injected HTTP service for readSelected', async () => {
|
|
35
|
+
const definition: ReadSelectedDefinitionDto<VehicleSlimDto> = {
|
|
36
|
+
searchDefinition: {
|
|
37
|
+
searches: [
|
|
38
|
+
{
|
|
39
|
+
propertyCriteria: [
|
|
40
|
+
{
|
|
41
|
+
propertyName: 'Brand',
|
|
42
|
+
comparisonOperator: ReadSelectedComparisonOperator.Equal,
|
|
43
|
+
valueType: ReadSelectedPropertyType.String,
|
|
44
|
+
value: 'Audi',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
propertyName: 'Brand',
|
|
48
|
+
comparisonOperator: ReadSelectedComparisonOperator.Equal,
|
|
49
|
+
valueType: ReadSelectedPropertyType.String,
|
|
50
|
+
value: 'BMW',
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
logicalOperator: ReadSelectedLogicalOperator.Or,
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
logicalOperator: ReadSelectedLogicalOperator.And,
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const result = (await client.readSelected(definition)) as { ok: boolean; result: any };
|
|
61
|
+
|
|
62
|
+
expect(result.ok).toBe(true);
|
|
63
|
+
expect(result.result).toBeDefined();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should handle HTTP errors properly', async () => {
|
|
67
|
+
const invalidClient = new ApiReadControllerClient('/InvalidPath', fetchService);
|
|
68
|
+
const result = await invalidClient.readAll();
|
|
69
|
+
expect(result.ok).toBe(false);
|
|
70
|
+
});
|
|
71
|
+
});
|