@equinor/fusion-framework-module-services 5.1.1 → 5.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/esm/context/index.js.map +1 -1
  3. package/dist/esm/context/query/generate-endpoint.js +58 -12
  4. package/dist/esm/context/query/generate-endpoint.js.map +1 -1
  5. package/dist/esm/context/related/generate-endpoint.js +56 -12
  6. package/dist/esm/context/related/generate-endpoint.js.map +1 -1
  7. package/dist/esm/version.js +1 -1
  8. package/dist/tsconfig.tsbuildinfo +1 -1
  9. package/dist/types/context/get/client.d.ts +2 -1
  10. package/dist/types/context/get/generate-endpoint.d.ts +2 -1
  11. package/dist/types/context/get/generate-parameters.d.ts +2 -1
  12. package/dist/types/context/index.d.ts +1 -0
  13. package/dist/types/context/query/client.d.ts +2 -1
  14. package/dist/types/context/query/generate-endpoint.d.ts +11 -1
  15. package/dist/types/context/query/generate-parameters.d.ts +2 -1
  16. package/dist/types/context/related/client.d.ts +2 -1
  17. package/dist/types/context/related/generate-endpoint.d.ts +11 -1
  18. package/dist/types/context/related/generate-parameters.d.ts +2 -1
  19. package/dist/types/notification/notification/delete/client.d.ts +2 -1
  20. package/dist/types/notification/notification/delete/generate-endpoint.d.ts +2 -1
  21. package/dist/types/notification/notification/delete/generate-parameters.d.ts +2 -1
  22. package/dist/types/notification/notification/get/client.d.ts +2 -1
  23. package/dist/types/notification/notification/get/generate-endpoint.d.ts +2 -1
  24. package/dist/types/notification/notification/get/generate-parameters.d.ts +2 -1
  25. package/dist/types/notification/notification/getAll/client.d.ts +2 -1
  26. package/dist/types/notification/notification/getAll/generate-endpoint.d.ts +2 -1
  27. package/dist/types/notification/notification/getAll/generate-parameters.d.ts +2 -1
  28. package/dist/types/notification/notification/patch/client.d.ts +2 -1
  29. package/dist/types/notification/notification/patch/generate-endpoint.d.ts +2 -1
  30. package/dist/types/notification/notification/patch/generate-parameters.d.ts +2 -1
  31. package/dist/types/notification/notification/post/client.d.ts +2 -1
  32. package/dist/types/notification/notification/post/generate-endpoint.d.ts +2 -1
  33. package/dist/types/notification/notification/post/generate-parameters.d.ts +2 -1
  34. package/dist/types/notification/settings/get/client.d.ts +2 -1
  35. package/dist/types/notification/settings/get/generate-endpoint.d.ts +2 -1
  36. package/dist/types/notification/settings/get/generate-parameters.d.ts +2 -1
  37. package/dist/types/notification/settings/put/client.d.ts +2 -1
  38. package/dist/types/notification/settings/put/generate-endpoint.d.ts +2 -1
  39. package/dist/types/notification/settings/put/generate-parameters.d.ts +2 -1
  40. package/dist/types/people/person-details/client.d.ts +1 -1
  41. package/dist/types/people/person-details/generate-parameters.d.ts +1 -1
  42. package/dist/types/people/person-photo/client.d.ts +1 -1
  43. package/dist/types/people/person-photo/generate-parameters.d.ts +1 -1
  44. package/dist/types/people/query/client.d.ts +1 -1
  45. package/dist/types/people/query/generate-parameters.d.ts +1 -1
  46. package/dist/types/version.d.ts +1 -1
  47. package/package.json +11 -7
  48. package/src/context/index.ts +2 -0
  49. package/src/context/query/generate-endpoint.ts +76 -27
  50. package/src/context/related/generate-endpoint.ts +74 -26
  51. package/src/version.ts +1 -1
  52. package/tests/context.test.ts +109 -0
  53. package/tests/mocks/get-context-item.ts +60 -0
  54. package/tests/mocks/index.ts +1 -0
  55. package/tests/setup.ts +73 -0
  56. package/tsconfig.json +4 -3
  57. package/vitest.config.ts +11 -0
@@ -10,38 +10,86 @@ import type {
10
10
  RelatedContextOdataParameters,
11
11
  } from './types';
12
12
 
13
- const buildOdataFilter = (filterObj: RelatedContextOdataFilter) => {
14
- return Object.keys(filterObj).reduce((acc, key) => {
15
- switch (key) {
16
- case 'type':
17
- return filterObj[key]?.length ? { ...acc, [key]: { in: filterObj[key] } } : acc;
18
- default:
19
- return { ...acc, [key]: filterObj[key as keyof typeof filterObj] };
20
- }
21
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
22
- }, {} as any);
23
- };
24
- const buildOdataObject = (parameters: RelatedContextOdataParameters) => {
25
- return Object.entries(parameters)
26
- .filter(([_, value]) => !!value)
27
- .reduce((acc, [key, value]) => {
13
+ /**
14
+ * Builds an OData filter object based on the provided filter object.
15
+ *
16
+ * @param filterObj - The filter object containing key-value pairs to be transformed into an OData filter.
17
+ * @returns A new object representing the OData filter.
18
+ *
19
+ * @example
20
+ * ```typescript
21
+ * const filter = buildOdataFilter({ type: ['exampleType'], otherKey: 'value' });
22
+ * // filter will be { $filter: { in: ['exampleType'] }, otherKey: 'value' }
23
+ * ```
24
+ */
25
+ function buildOdataFilter(filterObj: RelatedContextOdataFilter): Record<string, unknown> {
26
+ return Object.keys(filterObj).reduce(
27
+ (acc, key) => {
28
28
  switch (key) {
29
- case 'filter':
30
- return {
31
- ...acc,
32
- [key]: buildOdataFilter(value as RelatedContextOdataFilter),
33
- };
29
+ case 'type':
30
+ if (filterObj[key]?.length) {
31
+ acc[key] = { in: filterObj[key] };
32
+ }
33
+ break;
34
34
  default:
35
- return { ...acc, [key]: parameters[key as keyof typeof parameters] };
35
+ acc[key] = filterObj[key as keyof typeof filterObj];
36
+ break;
36
37
  }
37
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
38
- }, {} as any);
39
- };
38
+ return acc;
39
+ },
40
+ {} as Record<string, unknown>,
41
+ );
42
+ }
40
43
 
41
- const createSearchParameters = (args: string | RelatedContextOdataParameters) => {
44
+ /**
45
+ * Builds an OData object from the given parameters.
46
+ *
47
+ * This function takes an object of parameters and processes each key-value pair.
48
+ * It filters out any entries with falsy values and then reduces the remaining entries
49
+ * into a new object. For the 'filter' key, it calls a specific function to build the
50
+ * OData filter. For all other keys, it directly assigns the value from the parameters.
51
+ *
52
+ * @param parameters - The parameters to build the OData object from.
53
+ * @returns An object representing the OData parameters.
54
+ */
55
+ function buildOdataObject(parameters: RelatedContextOdataParameters): Record<string, unknown> {
56
+ return Object.entries(parameters)
57
+ .filter(([_, value]) => !!value)
58
+ .reduce(
59
+ (acc, [key, value]) => {
60
+ switch (key) {
61
+ case 'filter':
62
+ acc[key] = buildOdataFilter(value as RelatedContextOdataFilter);
63
+ break;
64
+ default:
65
+ acc[key] = parameters[key as keyof typeof parameters];
66
+ break;
67
+ }
68
+ return acc;
69
+ },
70
+ {} as Record<string, unknown>,
71
+ );
72
+ }
73
+
74
+ /**
75
+ * Creates search parameters for a query.
76
+ *
77
+ * @param args - The arguments for creating search parameters. It can be either a string or an object of type `RelatedContextOdataParameters`.
78
+ * @returns The search parameters as a string.
79
+ */
80
+ function createSearchParameters(args: string | RelatedContextOdataParameters): string {
42
81
  return typeof args === 'string' ? args : buildOdataQuery(buildOdataObject(args));
43
- };
82
+ }
44
83
 
84
+ /**
85
+ * Generates an endpoint URL based on the provided API version and arguments.
86
+ *
87
+ * @template TVersion - The type of the API version, defaults to the keys of `ApiVersion`.
88
+ * @param version - The API version to use for generating the endpoint.
89
+ * @param args - The arguments required to generate the endpoint, including `id` and `query`.
90
+ * @returns The generated endpoint URL as a string.
91
+ * @throws {UnsupportedApiVersion} If the provided API version is not supported.
92
+ */
45
93
  export const generateEndpoint = <TVersion extends string = keyof typeof ApiVersion>(
46
94
  version: TVersion,
47
95
  args: RelatedContextArgs<TVersion>,
package/src/version.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  // Generated by genversion.
2
- export const version = '5.1.1';
2
+ export const version = '5.1.2';
@@ -0,0 +1,109 @@
1
+ import { describe, expect, it, vi, beforeEach, afterEach, type MockInstance } from 'vitest';
2
+
3
+ import { BASE_URL } from './setup';
4
+
5
+ import { ContextApiClient, ApiVersion } from '../src/context';
6
+
7
+ import { HttpClient } from '@equinor/fusion-framework-module-http/client';
8
+ import { UnsupportedApiVersion } from '../src/errors';
9
+
10
+ import { mockContextItem } from './mocks';
11
+
12
+ describe('Context', () => {
13
+ let httpClient: HttpClient;
14
+ let httpClientWatcher: MockInstance;
15
+ let contextClient: ContextApiClient;
16
+
17
+ beforeEach(() => {
18
+ httpClient = new HttpClient(BASE_URL, {
19
+ /* options */
20
+ });
21
+ httpClientWatcher = vi.spyOn(httpClient, 'json');
22
+ contextClient = new ContextApiClient(httpClient, 'json');
23
+ });
24
+
25
+ afterEach(() => {
26
+ httpClientWatcher.mockRestore();
27
+ });
28
+
29
+ describe('get', () => {
30
+ it('should generate parameters for valid API version and arguments for V1', async () => {
31
+ const contextId = '123e4567-e89b-12d3-a456-426614174000';
32
+ const expected = mockContextItem(contextId);
33
+ const result = await contextClient.get('v1', { id: contextId });
34
+
35
+ expect(result).toMatchObject(expected);
36
+
37
+ expect(httpClientWatcher).toHaveBeenCalledWith(
38
+ `/contexts/${contextId}/?api-version=${ApiVersion.v1}`,
39
+ undefined,
40
+ );
41
+ });
42
+
43
+ it('should throw UnsupportedApiVersion for unsupported API version', async () => {
44
+ // Attempt to get context with an unsupported API version 'v2'
45
+ // This should throw an UnsupportedApiVersion error because 'v2' is not a valid API version
46
+ try {
47
+ await contextClient.get('v2', { id: '123e4567-e89b-12d3-a456-426614174000' });
48
+ expect(true).toBe(false); // This line should not be reached
49
+ } catch (error) {
50
+ // Verify that the error is of type UnsupportedApiVersion
51
+ expect(error).toBeInstanceOf(UnsupportedApiVersion);
52
+ }
53
+ });
54
+ });
55
+
56
+ describe('query', () => {
57
+ it('should generate parameters for valid API version and arguments for V1', async () => {
58
+ const result = await contextClient.query('v1', { query: { filter: { type: ['master'] } } });
59
+
60
+ expect(result).toHaveLength(10);
61
+
62
+ const searchParams = new URLSearchParams();
63
+ searchParams.append('$filter', "type in ('master')");
64
+ searchParams.append('api-version', ApiVersion.v1);
65
+
66
+ expect(httpClientWatcher).toHaveBeenCalledWith(`/contexts/?${searchParams}`, undefined);
67
+ });
68
+
69
+ it('should throw UnsupportedApiVersion for unsupported API version', async () => {
70
+ // Attempt to get context with an unsupported API version 'v2'
71
+ // This should throw an UnsupportedApiVersion error because 'v2' is not a valid API version
72
+ try {
73
+ await contextClient.query('v2', { query: { filter: { type: ['master'] } } });
74
+ expect(true).toBe(false); // This line should not be reached
75
+ } catch (error) {
76
+ // Verify that the error is of type UnsupportedApiVersion
77
+ expect(error).toBeInstanceOf(UnsupportedApiVersion);
78
+ }
79
+ });
80
+ });
81
+
82
+ describe('related', () => {
83
+ it('should generate parameters for valid API version and arguments for V1', async () => {
84
+ const contextId = '123e4567-e89b-12d3-a456-426614174000';
85
+
86
+ await contextClient.related('v1', { id: contextId, query: { filter: { type: ['master'] } } });
87
+
88
+ const searchParams = new URLSearchParams();
89
+ searchParams.append('$filter', "type in ('master')");
90
+ searchParams.append('api-version', ApiVersion.v1);
91
+
92
+ expect(httpClientWatcher).toHaveBeenCalledWith(
93
+ `/contexts/${contextId}/relations?${searchParams}`,
94
+ undefined,
95
+ );
96
+ });
97
+ it('should throw UnsupportedApiVersion for unsupported API version', async () => {
98
+ // Attempt to get context with an unsupported API version 'v2'
99
+ // This should throw an UnsupportedApiVersion error because 'v2' is not a valid API version
100
+ try {
101
+ await contextClient.related('v2', { id: '123e4567-e89b-12d3-a456-426614174000' });
102
+ expect(true).toBe(false); // This line should not be reached
103
+ } catch (error) {
104
+ // Verify that the error is of type UnsupportedApiVersion
105
+ expect(error).toBeInstanceOf(UnsupportedApiVersion);
106
+ }
107
+ });
108
+ });
109
+ });
@@ -0,0 +1,60 @@
1
+ import { Faker, da, en } from '@faker-js/faker';
2
+ import { type ApiContextEntity, ApiVersion } from '../../src/context';
3
+
4
+ import { createHash } from 'node:crypto';
5
+
6
+ /**
7
+ * Converts a string to a seed for the Faker.js library.
8
+ *
9
+ * @param uuid - The UUID string to convert.
10
+ * @returns A numeric seed derived from the UUID.
11
+ */
12
+ function stringToSeed(uuid: string): number {
13
+ const hash = createHash('sha256').update(uuid).digest('hex');
14
+ return Number.parseInt(hash.slice(0, 8), 16);
15
+ }
16
+
17
+ /**
18
+ * Since generation of dates might have a slight difference in milliseconds
19
+ * we normalize the date to the nearest minute, so we can compare them.
20
+ *
21
+ * @note
22
+ * This might fail at exactly midnight, but the odds for that are slim. Rerun the test if it fails.
23
+ *
24
+ * @param date date to normalize
25
+ * @returns string
26
+ */
27
+ function normalizeDateMock(date: Date): string {
28
+ date.setMinutes(0);
29
+ date.setSeconds(0);
30
+ date.setMilliseconds(0);
31
+ return date.toISOString();
32
+ }
33
+
34
+ export const mockContextItem = <V extends ApiVersion = ApiVersion.v1>(
35
+ id: string,
36
+ version: V = ApiVersion.v1 as V,
37
+ ): ApiContextEntity<V> => {
38
+ const seed = stringToSeed(id);
39
+ const faker = new Faker({ seed, locale: en });
40
+ switch (version) {
41
+ case ApiVersion.v1:
42
+ return {
43
+ id: String(id),
44
+ externalId: faker.string.uuid(),
45
+ created: normalizeDateMock(faker.date.recent()),
46
+ source: faker.string.uuid(),
47
+ type: {
48
+ id: faker.string.uuid(),
49
+ isChildType: false,
50
+ parentTypeIds: null,
51
+ },
52
+ value: null,
53
+ title: faker.lorem.sentence(),
54
+ isActive: true,
55
+ isDeleted: false,
56
+ updated: normalizeDateMock(faker.date.past()),
57
+ } satisfies ApiContextEntity<ApiVersion.v1> as ApiContextEntity<V>;
58
+ }
59
+ throw new Error('Not implemented');
60
+ };
@@ -0,0 +1 @@
1
+ export { mockContextItem } from './get-context-item';
package/tests/setup.ts ADDED
@@ -0,0 +1,73 @@
1
+ import { afterAll, afterEach, beforeAll } from 'vitest';
2
+
3
+ import { setupServer } from 'msw/node';
4
+ import { http, HttpResponse } from 'msw';
5
+
6
+ import { mockContextItem } from './mocks';
7
+
8
+ import type { ApiVersion } from '../src/context';
9
+
10
+ export const BASE_URL = 'https://localhost';
11
+
12
+ // Define mock handlers for fetch requests
13
+ const handlers = [
14
+ // Mock GET /contexts
15
+ http.get(new URL('contexts', BASE_URL).toString(), (req) => {
16
+ const url = new URL(req.request.url);
17
+ const apiVersion = url.searchParams.get('api-version');
18
+ if (!apiVersion) {
19
+ return HttpResponse.error();
20
+ }
21
+ return HttpResponse.json(
22
+ new Array(10)
23
+ .fill(null)
24
+ .map((_, index) => mockContextItem(`context-item-${index}`, apiVersion as ApiVersion.v1)),
25
+ );
26
+ }),
27
+
28
+ // Mock GET /contexts/:id
29
+ http.get(new URL('contexts/:id', BASE_URL).toString(), (req) => {
30
+ const { id } = req.params;
31
+ if (!id || typeof id !== 'string') {
32
+ return HttpResponse.error();
33
+ }
34
+ const url = new URL(req.request.url);
35
+ const apiVersion = url.searchParams.get('api-version');
36
+ if (!apiVersion) {
37
+ return HttpResponse.error();
38
+ }
39
+ return HttpResponse.json(mockContextItem(id, apiVersion as ApiVersion.v1));
40
+ }),
41
+
42
+ // Mock GET /contexts/:id/relations
43
+ http.get(new URL('contexts/:id/relations', BASE_URL).toString(), (req) => {
44
+ const { id } = req.params;
45
+ if (!id || typeof id !== 'string') {
46
+ return HttpResponse.error();
47
+ }
48
+ const url = new URL(req.request.url);
49
+ const apiVersion = url.searchParams.get('api-version');
50
+ if (!apiVersion) {
51
+ return HttpResponse.error();
52
+ }
53
+ return HttpResponse.json(
54
+ new Array(3)
55
+ .fill(null)
56
+ .map((_, index) =>
57
+ mockContextItem(`context--related-item-${index}`, apiVersion as ApiVersion.v1),
58
+ ),
59
+ );
60
+ }),
61
+ ];
62
+
63
+ // Initialize the server
64
+ export const server = setupServer(...handlers);
65
+
66
+ // Start server before all tests
67
+ beforeAll(() => server.listen({ onUnhandledRequest: 'warn' }));
68
+
69
+ // Reset handlers after each test
70
+ afterEach(() => server.resetHandlers());
71
+
72
+ // Stop server after all tests
73
+ afterAll(() => server.close());
package/tsconfig.json CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  "paths": {
9
9
  "@equinor/fusion-framework-module-services": [
10
- "."
10
+ "./"
11
11
  ],
12
12
  "@equinor/fusion-framework-module-services/*": [
13
13
  "./*"
@@ -29,10 +29,11 @@
29
29
  }
30
30
  ],
31
31
  "include": [
32
- "src/**/*",
32
+ "src/**/*", "tests/**/*",
33
33
  ],
34
34
  "exclude": [
35
35
  "node_modules",
36
- "lib"
36
+ "lib",
37
+ "tests"
37
38
  ]
38
39
  }
@@ -0,0 +1,11 @@
1
+ import { defineProject } from 'vitest/config';
2
+
3
+ import { name, version } from './package.json';
4
+
5
+ export default defineProject({
6
+ test: {
7
+ include: ['tests/**/*.test.ts'],
8
+ name: `${name}@${version}`,
9
+ setupFiles: ['tests/setup.ts'],
10
+ },
11
+ });