@intellegens/cornerstone-client 0.0.9999-alpha-30 → 0.0.9999-alpha-32
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/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/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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import { CollectionViewAdapter } from '.';
|
|
3
|
+
import { ReadSelectedComparisonOperator, ReadSelectedOrderingDirection, ReadSelectedPropertyType } from '../../data';
|
|
4
|
+
import { dateInterval, searchTerm } from '../../utils';
|
|
5
|
+
describe('CollectionViewAdapter', () => {
|
|
6
|
+
let data;
|
|
7
|
+
let error;
|
|
8
|
+
let isLoading;
|
|
9
|
+
const callback = vi.fn((_isLoading, _data, _error) => {
|
|
10
|
+
isLoading = _isLoading;
|
|
11
|
+
data = _data;
|
|
12
|
+
error = _error;
|
|
13
|
+
});
|
|
14
|
+
const options = {
|
|
15
|
+
pagination: { useTotalItemCount: true, pageSize: 5, pageNumber: 1 },
|
|
16
|
+
ordering: { maxActiveOrderingColumns: 1, orderByPaths: [] },
|
|
17
|
+
search: { textSearchableProperties: ['name'], numericSearchableProperties: ['id'] },
|
|
18
|
+
};
|
|
19
|
+
let adapter;
|
|
20
|
+
const waitForData = async (timeout = 2000) => {
|
|
21
|
+
const start = Date.now();
|
|
22
|
+
while (isLoading !== false && Date.now() - start < timeout) {
|
|
23
|
+
await new Promise(res => setTimeout(res, 50));
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
const resetAdapterState = () => {
|
|
27
|
+
data = undefined;
|
|
28
|
+
error = undefined;
|
|
29
|
+
isLoading = undefined;
|
|
30
|
+
};
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
data = undefined;
|
|
33
|
+
error = undefined;
|
|
34
|
+
isLoading = undefined;
|
|
35
|
+
});
|
|
36
|
+
it('callback returns correct loading status and collection data', async () => {
|
|
37
|
+
options.pagination.useTotalItemCount = true;
|
|
38
|
+
adapter = new CollectionViewAdapter('AnythingManuallyMappedRead', callback, options);
|
|
39
|
+
await waitForData();
|
|
40
|
+
expect(callback).toHaveBeenNthCalledWith(1, true, undefined, undefined);
|
|
41
|
+
expect(callback).toHaveBeenLastCalledWith(false, data, undefined);
|
|
42
|
+
expect(data).toBeDefined();
|
|
43
|
+
});
|
|
44
|
+
it('adapter options set correct ordering', async () => {
|
|
45
|
+
adapter = new CollectionViewAdapter('AnythingManuallyMappedRead', callback, options);
|
|
46
|
+
await waitForData();
|
|
47
|
+
resetAdapterState();
|
|
48
|
+
adapter.setOrdering(['name'], ReadSelectedOrderingDirection.Descending);
|
|
49
|
+
await waitForData();
|
|
50
|
+
expect(adapter.getCurrentOrdering()).toEqual([
|
|
51
|
+
{
|
|
52
|
+
orderByPath: ['name'],
|
|
53
|
+
orderDirection: ReadSelectedOrderingDirection.Descending,
|
|
54
|
+
},
|
|
55
|
+
]);
|
|
56
|
+
});
|
|
57
|
+
it('text search is applied to adapter', async () => {
|
|
58
|
+
const searchDefinition = searchTerm('Flu', ['name'], ['id']);
|
|
59
|
+
if (!searchDefinition)
|
|
60
|
+
throw new Error('searchDefinition is undefined');
|
|
61
|
+
adapter = new CollectionViewAdapter('AnythingManuallyMappedRead', callback, options);
|
|
62
|
+
await waitForData();
|
|
63
|
+
resetAdapterState();
|
|
64
|
+
adapter.setSearchDefinition(searchDefinition);
|
|
65
|
+
await waitForData();
|
|
66
|
+
expect(data).toBeDefined();
|
|
67
|
+
expect(data.length).toBeGreaterThan(0);
|
|
68
|
+
for (const item of data) {
|
|
69
|
+
expect(item.name).toMatch(/flu/i);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
it('numeric search is applied to adapter', async () => {
|
|
73
|
+
const searchDefinition = searchTerm('10000001', ['id'], ['id']);
|
|
74
|
+
if (!searchDefinition)
|
|
75
|
+
throw new Error('searchDefinition is undefined');
|
|
76
|
+
adapter = new CollectionViewAdapter('AnythingManuallyMappedRead', callback, options);
|
|
77
|
+
await waitForData();
|
|
78
|
+
resetAdapterState();
|
|
79
|
+
adapter.setSearchDefinition(searchDefinition);
|
|
80
|
+
await waitForData();
|
|
81
|
+
expect(data).toBeDefined();
|
|
82
|
+
expect(data.length).toBe(1);
|
|
83
|
+
for (const item of data) {
|
|
84
|
+
expect(item.id).toBe(10000001);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
it('applies date range filter from-to on the same date type property', async () => {
|
|
88
|
+
adapter = new CollectionViewAdapter('PetsSlim', callback, options);
|
|
89
|
+
const from = new Date('2017-01-01T00:00:00.000Z');
|
|
90
|
+
const to = new Date('2017-12-31T00:00:00.000Z');
|
|
91
|
+
const searchDefinition = dateInterval(from, to, ReadSelectedComparisonOperator.GreaterOrEqual, ReadSelectedComparisonOperator.LessOrEqual, ReadSelectedPropertyType.DateOnly, 'dateOfBirth');
|
|
92
|
+
if (!searchDefinition)
|
|
93
|
+
throw new Error('searchDefinition is undefined');
|
|
94
|
+
await waitForData();
|
|
95
|
+
resetAdapterState();
|
|
96
|
+
adapter.setSearchDefinition(searchDefinition);
|
|
97
|
+
await waitForData();
|
|
98
|
+
expect(data).toBeDefined();
|
|
99
|
+
});
|
|
100
|
+
it('applies pagination: page size and jump to page set skip/limit', async () => {
|
|
101
|
+
adapter = new CollectionViewAdapter('AnythingManuallyMappedRead', callback, options);
|
|
102
|
+
await waitForData();
|
|
103
|
+
const firstPageData = data;
|
|
104
|
+
resetAdapterState();
|
|
105
|
+
adapter.jumpToPage(3);
|
|
106
|
+
await waitForData();
|
|
107
|
+
expect(data).toBeDefined();
|
|
108
|
+
expect(data.length).toBe(5);
|
|
109
|
+
// Check that data is different from first page
|
|
110
|
+
expect(data).not.toEqual(firstPageData);
|
|
111
|
+
});
|
|
112
|
+
it('calculates total when useTotalItemCount=false and last page is partial', async () => {
|
|
113
|
+
options.pagination.useTotalItemCount = false;
|
|
114
|
+
options.pagination.pageSize = 10;
|
|
115
|
+
adapter = new CollectionViewAdapter('AnythingManuallyMappedRead', callback, options);
|
|
116
|
+
await waitForData();
|
|
117
|
+
const firstPageData = data;
|
|
118
|
+
resetAdapterState();
|
|
119
|
+
adapter.jumpToPage(2);
|
|
120
|
+
await waitForData();
|
|
121
|
+
expect(adapter.totalItemCount).toBe(firstPageData.length + data.length);
|
|
122
|
+
});
|
|
123
|
+
it('setOrdering correctly handles PropertyPathDto array comparison and limits columns', async () => {
|
|
124
|
+
options.ordering.maxActiveOrderingColumns = 2;
|
|
125
|
+
// Add first ordering
|
|
126
|
+
adapter = new CollectionViewAdapter('AnythingManuallyMappedRead', callback, options);
|
|
127
|
+
adapter.setOrdering(['name'], ReadSelectedOrderingDirection.Descending);
|
|
128
|
+
await waitForData();
|
|
129
|
+
expect(adapter.getCurrentOrdering()).toEqual([
|
|
130
|
+
{
|
|
131
|
+
orderByPath: ['name'],
|
|
132
|
+
orderDirection: ReadSelectedOrderingDirection.Descending,
|
|
133
|
+
},
|
|
134
|
+
]);
|
|
135
|
+
// Add second ordering
|
|
136
|
+
adapter.setOrdering(['id'], ReadSelectedOrderingDirection.Ascending);
|
|
137
|
+
await waitForData();
|
|
138
|
+
// Should have both orderings, with 'id' first (most recent)
|
|
139
|
+
expect(adapter.getCurrentOrdering()).toEqual([
|
|
140
|
+
{
|
|
141
|
+
orderByPath: ['id'],
|
|
142
|
+
orderDirection: ReadSelectedOrderingDirection.Ascending,
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
orderByPath: ['name'],
|
|
146
|
+
orderDirection: ReadSelectedOrderingDirection.Descending,
|
|
147
|
+
},
|
|
148
|
+
]);
|
|
149
|
+
// // Add third ordering - should remove the oldest one (name)
|
|
150
|
+
adapter.setOrdering(['description'], ReadSelectedOrderingDirection.Descending);
|
|
151
|
+
await waitForData();
|
|
152
|
+
expect(adapter.getCurrentOrdering()).toEqual([
|
|
153
|
+
{
|
|
154
|
+
orderByPath: ['description'],
|
|
155
|
+
orderDirection: ReadSelectedOrderingDirection.Descending,
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
orderByPath: ['id'],
|
|
159
|
+
orderDirection: ReadSelectedOrderingDirection.Ascending,
|
|
160
|
+
},
|
|
161
|
+
]);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
@@ -20,16 +20,20 @@ export interface IGlobalSearchable<TKey> extends IIdentifiable<TKey> {
|
|
|
20
20
|
export declare class SearchAdapter<TKey, TDto extends IGlobalSearchable<TKey>> {
|
|
21
21
|
onChange: SingularEventTarget<string>;
|
|
22
22
|
private _readClient;
|
|
23
|
-
|
|
23
|
+
options: SearchAdapterOptions<TDto>;
|
|
24
24
|
private _isLoading;
|
|
25
25
|
private _currentAbortController?;
|
|
26
26
|
private _lastSearchedValue;
|
|
27
27
|
private _typesToSearch;
|
|
28
28
|
private _fetchResultsDataTimeout?;
|
|
29
29
|
private _fetchResultsPromises;
|
|
30
|
+
private _totalCount;
|
|
30
31
|
constructor(options: SearchAdapterOptions<TDto>);
|
|
31
32
|
get searchTriggerMinLength(): number;
|
|
32
33
|
get multiselect(): boolean;
|
|
34
|
+
get isLoading(): boolean;
|
|
35
|
+
get totalCount(): number;
|
|
36
|
+
get hasMoreRecords(): boolean;
|
|
33
37
|
private _searchText;
|
|
34
38
|
get searchText(): string;
|
|
35
39
|
set searchText(searchText: string);
|
|
@@ -13,22 +13,32 @@ export class SearchAdapter {
|
|
|
13
13
|
onChange = new SingularEventTarget();
|
|
14
14
|
// inputs
|
|
15
15
|
_readClient;
|
|
16
|
-
|
|
16
|
+
options;
|
|
17
17
|
_isLoading = false;
|
|
18
18
|
_currentAbortController;
|
|
19
|
-
_lastSearchedValue =
|
|
19
|
+
_lastSearchedValue = undefined;
|
|
20
20
|
_typesToSearch = [];
|
|
21
21
|
_fetchResultsDataTimeout;
|
|
22
22
|
_fetchResultsPromises = [];
|
|
23
|
+
_totalCount = 0;
|
|
23
24
|
constructor(options) {
|
|
24
|
-
this.
|
|
25
|
+
this.options = options;
|
|
25
26
|
this._readClient = new ApiReadControllerClient(options.controllerName);
|
|
26
27
|
}
|
|
27
28
|
get searchTriggerMinLength() {
|
|
28
|
-
return this.
|
|
29
|
+
return this.options.searchTriggerMinLength;
|
|
29
30
|
}
|
|
30
31
|
get multiselect() {
|
|
31
|
-
return this.
|
|
32
|
+
return this.options.multiselect;
|
|
33
|
+
}
|
|
34
|
+
get isLoading() {
|
|
35
|
+
return this._isLoading;
|
|
36
|
+
}
|
|
37
|
+
get totalCount() {
|
|
38
|
+
return this._totalCount;
|
|
39
|
+
}
|
|
40
|
+
get hasMoreRecords() {
|
|
41
|
+
return this._totalCount > this._searchResults.length;
|
|
32
42
|
}
|
|
33
43
|
_searchText = '';
|
|
34
44
|
get searchText() {
|
|
@@ -60,7 +70,7 @@ export class SearchAdapter {
|
|
|
60
70
|
this.selectedItems = [];
|
|
61
71
|
}
|
|
62
72
|
// selection mode
|
|
63
|
-
if (this.
|
|
73
|
+
if (this.options.multiselect) {
|
|
64
74
|
if (!this._selectedItems.find(s => s.id === item.id)) {
|
|
65
75
|
this.selectedItems = [...this.selectedItems, item];
|
|
66
76
|
}
|
|
@@ -86,18 +96,19 @@ export class SearchAdapter {
|
|
|
86
96
|
this.onChange.dispatchEvent('onChange');
|
|
87
97
|
}
|
|
88
98
|
async _trySearch() {
|
|
89
|
-
const options = this.
|
|
99
|
+
const options = this.options;
|
|
90
100
|
this.searchResults = [];
|
|
91
101
|
if (this._searchText.length < options.searchTriggerMinLength)
|
|
92
102
|
return;
|
|
93
|
-
if (this._searchText
|
|
103
|
+
if (this._searchText === this._lastSearchedValue)
|
|
94
104
|
return;
|
|
95
105
|
this._lastSearchedValue = this._searchText;
|
|
96
106
|
this._typesToSearch =
|
|
97
|
-
options.limitSearchToSelectedType && this._selectedItems.length ? [this._selectedItems[0].type] : this.
|
|
107
|
+
options.limitSearchToSelectedType && this._selectedItems.length ? [this._selectedItems[0].type] : this.options.typesToSearch;
|
|
98
108
|
this.searchResults = this._sortByName(await this._fetchResults(options.searchDebounceDelay));
|
|
99
109
|
}
|
|
100
110
|
async _fetchResults(debounceDelay) {
|
|
111
|
+
this._isLoading = true;
|
|
101
112
|
return new Promise((resolve, reject) => {
|
|
102
113
|
this._fetchResultsPromises.push({ resolve, reject });
|
|
103
114
|
if (this._fetchResultsDataTimeout !== undefined) {
|
|
@@ -119,7 +130,6 @@ export class SearchAdapter {
|
|
|
119
130
|
});
|
|
120
131
|
}
|
|
121
132
|
async _fetchResultsDebounced() {
|
|
122
|
-
this._isLoading = true;
|
|
123
133
|
let abortController;
|
|
124
134
|
let caughtError;
|
|
125
135
|
try {
|
|
@@ -139,6 +149,8 @@ export class SearchAdapter {
|
|
|
139
149
|
if (!response.ok) {
|
|
140
150
|
throw response.error;
|
|
141
151
|
}
|
|
152
|
+
// Update total count from response metadata
|
|
153
|
+
this._totalCount = response.metadata?.totalCount ?? response.result.length;
|
|
142
154
|
// Filter out selected items from new results
|
|
143
155
|
const selectedIds = new Set((this.selectedItems || []).map(item => item.id));
|
|
144
156
|
return response.result.filter(item => !selectedIds.has(item.id));
|
|
@@ -168,7 +180,7 @@ export class SearchAdapter {
|
|
|
168
180
|
const definition = {
|
|
169
181
|
paginationDefinition: {
|
|
170
182
|
skip: 0,
|
|
171
|
-
limit: this.
|
|
183
|
+
limit: this.options.resultsLimit,
|
|
172
184
|
},
|
|
173
185
|
};
|
|
174
186
|
// Apply ordering
|
|
@@ -206,8 +218,8 @@ export class SearchAdapter {
|
|
|
206
218
|
},
|
|
207
219
|
];
|
|
208
220
|
}
|
|
209
|
-
if (this.
|
|
210
|
-
definition.searchDefinition.searches = [...(definition.searchDefinition.searches ?? []), ...this.
|
|
221
|
+
if (this.options.additionalSearchDefinitions && this.options.additionalSearchDefinitions.length > 0) {
|
|
222
|
+
definition.searchDefinition.searches = [...(definition.searchDefinition.searches ?? []), ...this.options.additionalSearchDefinitions];
|
|
211
223
|
}
|
|
212
224
|
return definition;
|
|
213
225
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import { ApiCrudControllerClient } from '.';
|
|
3
|
+
import { FetchHttpService } from '../HttpService';
|
|
4
|
+
describe('ApiCrudControllerClient', () => {
|
|
5
|
+
let client;
|
|
6
|
+
let fetchService;
|
|
7
|
+
const testId = 100;
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
fetchService = new FetchHttpService();
|
|
10
|
+
client = new ApiCrudControllerClient('/VehiclesSlim', fetchService);
|
|
11
|
+
});
|
|
12
|
+
it('should use injected HTTP service for create', async () => {
|
|
13
|
+
const newVehicle = { id: testId, brand: 'New Brand', model: 'New Model' };
|
|
14
|
+
const result = (await client.create(newVehicle));
|
|
15
|
+
expect(result.ok).toBe(true);
|
|
16
|
+
expect(result.result).toBeDefined();
|
|
17
|
+
expect(result.result.id).toBe(newVehicle.id);
|
|
18
|
+
expect(result.result.brand).toBe(newVehicle.brand);
|
|
19
|
+
expect(result.result.model).toBe(newVehicle.model);
|
|
20
|
+
});
|
|
21
|
+
it('should use injected HTTP service for update', async () => {
|
|
22
|
+
const updateVehicle = { id: testId, brand: 'Updated Brand', model: 'Updated Model' };
|
|
23
|
+
const result = (await client.update(testId, updateVehicle));
|
|
24
|
+
expect(result.ok).toBe(true);
|
|
25
|
+
expect(result.result).toBeDefined();
|
|
26
|
+
expect(result.result.id).toBe(updateVehicle.id);
|
|
27
|
+
expect(result.result.brand).toBe(updateVehicle.brand);
|
|
28
|
+
expect(result.result.model).toBe(updateVehicle.model);
|
|
29
|
+
});
|
|
30
|
+
it('should use injected HTTP service for delete', async () => {
|
|
31
|
+
const result = await client.delete(testId);
|
|
32
|
+
expect(result.ok).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import { ApiReadControllerClient } from '.';
|
|
3
|
+
import { FetchHttpService } from '../HttpService';
|
|
4
|
+
import { ReadSelectedComparisonOperator, ReadSelectedLogicalOperator, ReadSelectedPropertyType } from '../../../data';
|
|
5
|
+
describe('ApiReadControllerClient', () => {
|
|
6
|
+
let client;
|
|
7
|
+
let fetchService;
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
fetchService = new FetchHttpService();
|
|
10
|
+
client = new ApiReadControllerClient('/VehiclesSlim', fetchService);
|
|
11
|
+
});
|
|
12
|
+
it('should use injected HTTP service for readAll', async () => {
|
|
13
|
+
const result = (await client.readAll());
|
|
14
|
+
expect(result.ok).toBe(true);
|
|
15
|
+
expect(result.result).toBeDefined();
|
|
16
|
+
});
|
|
17
|
+
it('should use injected HTTP service for readSingle', async () => {
|
|
18
|
+
const testId = 1;
|
|
19
|
+
const result = (await client.readSingle(testId));
|
|
20
|
+
expect(result.ok).toBe(true);
|
|
21
|
+
expect(result.result).toBeDefined();
|
|
22
|
+
expect(typeof result.result).toBe('object');
|
|
23
|
+
expect(result.result.id).toBe(testId);
|
|
24
|
+
});
|
|
25
|
+
it('should use injected HTTP service for readSelected', async () => {
|
|
26
|
+
const definition = {
|
|
27
|
+
searchDefinition: {
|
|
28
|
+
searches: [
|
|
29
|
+
{
|
|
30
|
+
propertyCriteria: [
|
|
31
|
+
{
|
|
32
|
+
propertyName: 'Brand',
|
|
33
|
+
comparisonOperator: ReadSelectedComparisonOperator.Equal,
|
|
34
|
+
valueType: ReadSelectedPropertyType.String,
|
|
35
|
+
value: 'Audi',
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
propertyName: 'Brand',
|
|
39
|
+
comparisonOperator: ReadSelectedComparisonOperator.Equal,
|
|
40
|
+
valueType: ReadSelectedPropertyType.String,
|
|
41
|
+
value: 'BMW',
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
logicalOperator: ReadSelectedLogicalOperator.Or,
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
logicalOperator: ReadSelectedLogicalOperator.And,
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
const result = (await client.readSelected(definition));
|
|
51
|
+
expect(result.ok).toBe(true);
|
|
52
|
+
expect(result.result).toBeDefined();
|
|
53
|
+
});
|
|
54
|
+
it('should handle HTTP errors properly', async () => {
|
|
55
|
+
const invalidClient = new ApiReadControllerClient('/InvalidPath', fetchService);
|
|
56
|
+
const result = await invalidClient.readAll();
|
|
57
|
+
expect(result.ok).toBe(false);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import { FetchHttpService } from './FetchHttpService';
|
|
3
|
+
const baseUrl = 'http://localhost:5000/api';
|
|
4
|
+
const credentials = {
|
|
5
|
+
username: 'admin@test.com',
|
|
6
|
+
password: 'Password1234!',
|
|
7
|
+
};
|
|
8
|
+
describe('FetchHttpService', () => {
|
|
9
|
+
let fetchService;
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
fetchService = new FetchHttpService();
|
|
12
|
+
});
|
|
13
|
+
it('should make a GET request successfully', async () => {
|
|
14
|
+
const result = await fetchService.request(`${baseUrl}/AnythingManuallyMappedRead/ReadAll`);
|
|
15
|
+
expect(result.ok).toBe(true);
|
|
16
|
+
expect(result.status).toBe(200);
|
|
17
|
+
expect(result.statusText).toBe('OK');
|
|
18
|
+
const jsonData = await result.json();
|
|
19
|
+
expect(jsonData.result).toBeDefined();
|
|
20
|
+
expect(jsonData.error).toBeNull();
|
|
21
|
+
});
|
|
22
|
+
it('should make a POST request with body', async () => {
|
|
23
|
+
const result = await fetchService.request(`${baseUrl}/auth/signin`, {
|
|
24
|
+
method: 'POST',
|
|
25
|
+
headers: { 'Content-Type': 'application/json' },
|
|
26
|
+
body: JSON.stringify(credentials),
|
|
27
|
+
});
|
|
28
|
+
expect(result.ok).toBe(true);
|
|
29
|
+
expect(result.status).toBe(204);
|
|
30
|
+
});
|
|
31
|
+
it('should handle non-ok responses', async () => {
|
|
32
|
+
const result = await fetchService.request(`${baseUrl}/auth/signin`, {
|
|
33
|
+
method: 'POST',
|
|
34
|
+
headers: { 'Content-Type': 'application/json' },
|
|
35
|
+
body: JSON.stringify({ username: credentials.username, password: `${credentials.password}wrong` }),
|
|
36
|
+
});
|
|
37
|
+
expect(result.ok).toBe(false);
|
|
38
|
+
expect(result.status).toBe(400);
|
|
39
|
+
expect(result.statusText).toBe('Bad Request');
|
|
40
|
+
const jsonData = await result.json();
|
|
41
|
+
expect(jsonData.result).toBeNull();
|
|
42
|
+
expect(jsonData.error).toBeDefined();
|
|
43
|
+
expect(jsonData.error.code).toBe(400);
|
|
44
|
+
expect(jsonData.error.message).toBe('Invalid email or password');
|
|
45
|
+
});
|
|
46
|
+
it('should handle fetch errors from backend', async () => {
|
|
47
|
+
const result = await fetchService.request('http://localhost:5000/api/error');
|
|
48
|
+
expect(result.ok).toBe(false);
|
|
49
|
+
expect(result.status).toBe(404);
|
|
50
|
+
expect(result.statusText).toBe('Not Found');
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -30,12 +30,6 @@ export declare class UserManagementControllerClient<TKey, TResultDto extends Use
|
|
|
30
30
|
* @returns List of claims assigned to the user
|
|
31
31
|
*/
|
|
32
32
|
getUserClaims(id: TKey, signal?: AbortSignal): Promise<ApiResponseDto<ClaimDto[], ReadMetadataDto>>;
|
|
33
|
-
/**
|
|
34
|
-
* Gets all available roles in the system
|
|
35
|
-
* @param {AbortSignal} [signal] - Optional cancellation signal
|
|
36
|
-
* @returns List of all role names
|
|
37
|
-
*/
|
|
38
|
-
getAllRoles(signal?: AbortSignal): Promise<ApiResponseDto<string[], ReadMetadataDto>>;
|
|
39
33
|
/**
|
|
40
34
|
* Changes the password for a user
|
|
41
35
|
* @param id - The ID of the user
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import { UserManagementControllerClient } 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('UserManagementControllerClient', () => {
|
|
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 UserManagementControllerClient('/users', fetchService);
|
|
49
|
+
});
|
|
50
|
+
it('should use injected HTTP service for getUserRoles', async () => {
|
|
51
|
+
const result = (await client.getUserRoles(1));
|
|
52
|
+
expect(result.ok).toBe(true);
|
|
53
|
+
expect(result.result).toBeDefined();
|
|
54
|
+
});
|
|
55
|
+
it('should use injected HTTP service for getUserClaims', async () => {
|
|
56
|
+
const result = (await client.getUserClaims(1));
|
|
57
|
+
expect(result.ok).toBe(true);
|
|
58
|
+
expect(result.result).toBeDefined();
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -79,35 +79,6 @@ export class UserManagementControllerClient extends ApiCrudControllerClient {
|
|
|
79
79
|
});
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
|
-
/**
|
|
83
|
-
* Gets all available roles in the system
|
|
84
|
-
* @param {AbortSignal} [signal] - Optional cancellation signal
|
|
85
|
-
* @returns List of all role names
|
|
86
|
-
*/
|
|
87
|
-
async getAllRoles(signal) {
|
|
88
|
-
try {
|
|
89
|
-
const url = await apiInitializationService.getApiUrl(this.baseControllerPath, 'GetAllRoles');
|
|
90
|
-
const res = await this.httpService.request(url, {
|
|
91
|
-
method: 'GET',
|
|
92
|
-
credentials: 'include',
|
|
93
|
-
signal,
|
|
94
|
-
});
|
|
95
|
-
if (!res.ok) {
|
|
96
|
-
return fail(await res.json());
|
|
97
|
-
}
|
|
98
|
-
return ok(await res.json());
|
|
99
|
-
}
|
|
100
|
-
catch (err) {
|
|
101
|
-
console.error(err);
|
|
102
|
-
return fail({
|
|
103
|
-
error: {
|
|
104
|
-
code: ErrorCode.UnknownError,
|
|
105
|
-
message: 'Unknown error while fetching all roles',
|
|
106
|
-
metadata: {},
|
|
107
|
-
},
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
82
|
/**
|
|
112
83
|
* Changes the password for a user
|
|
113
84
|
* @param id - The ID of the user
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { IHttpService } from '../../..';
|
|
2
|
-
import { ApiResponseDto, EmptyMetadataDto, UserDto, UserInfoDto } from '../../../../data';
|
|
2
|
+
import { ApiResponseDto, EmptyMetadataDto, UserDto, UserInfoDto, RegisterRequestDto } from '../../../../data';
|
|
3
3
|
export { UserDto, UserInfoDto };
|
|
4
4
|
/**
|
|
5
5
|
* AuthService class is responsible for managing user authentication operations.
|
|
@@ -8,7 +8,7 @@ export { UserDto, UserInfoDto };
|
|
|
8
8
|
* @export
|
|
9
9
|
* @class AuthService
|
|
10
10
|
*/
|
|
11
|
-
export declare class AuthService<TKey, TUser extends UserDto<TKey> = UserDto<TKey>> {
|
|
11
|
+
export declare class AuthService<TKey, TRegisterUser extends RegisterRequestDto = RegisterRequestDto, TUser extends UserDto<TKey> = UserDto<TKey>> {
|
|
12
12
|
protected readonly baseControllerPath: string;
|
|
13
13
|
/**
|
|
14
14
|
* Constructor
|
|
@@ -55,6 +55,14 @@ export declare class AuthService<TKey, TUser extends UserDto<TKey> = UserDto<TKe
|
|
|
55
55
|
* @throws {Error} Error if fetch fails
|
|
56
56
|
*/
|
|
57
57
|
signOut(): Promise<ApiResponseDto<undefined, EmptyMetadataDto>>;
|
|
58
|
+
/**
|
|
59
|
+
* Registers a new user by sending user details to the API.
|
|
60
|
+
*
|
|
61
|
+
* @param {TRegisterUser} user User object containing registration data (e.g. username, password, and optionally firstName, lastName, etc.)
|
|
62
|
+
* @return {Promise<ApiResponseDto<TUser, EmptyMetadataDto>>} API response with registered user details or error info
|
|
63
|
+
* @throws {Error} Error if the request fails
|
|
64
|
+
*/
|
|
65
|
+
register(user: TRegisterUser): Promise<ApiResponseDto<TUser, EmptyMetadataDto>>;
|
|
58
66
|
/**
|
|
59
67
|
* If any API response returns an "Unauthenticated" response, call this method
|
|
60
68
|
* to update local authentication state to unauthenticated
|
|
@@ -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 {};
|