Skip to content

Commit 19cf1be

Browse files
committed
feat: use zod to parse config
1 parent 0507713 commit 19cf1be

File tree

7 files changed

+178
-194
lines changed

7 files changed

+178
-194
lines changed

api/src/__test__/core/utils/files/config-file-normalizer.test.ts

Lines changed: 63 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -10,72 +10,75 @@ test('it creates a FLASH config with NO OPTIONAL values', () => {
1010
const basicConfig = initialState;
1111
const config = getWriteableConfig(basicConfig, 'flash');
1212
expect(config).toMatchInlineSnapshot(`
13-
{
14-
"api": {
15-
"extraOrigins": "",
16-
"version": "",
17-
},
18-
"local": {},
19-
"notifier": {
20-
"apikey": "",
21-
},
22-
"remote": {
23-
"accesstoken": "",
24-
"apikey": "",
25-
"avatar": "",
26-
"dynamicRemoteAccessType": "DISABLED",
27-
"email": "",
28-
"idtoken": "",
29-
"localApiKey": "",
30-
"refreshtoken": "",
31-
"regWizTime": "",
32-
"username": "",
33-
"wanaccess": "",
34-
"wanport": "",
35-
},
36-
"upc": {
37-
"apikey": "",
38-
},
39-
}
40-
`);
13+
{
14+
"api": {
15+
"extraOrigins": "",
16+
"version": "",
17+
},
18+
"local": {},
19+
"notifier": {
20+
"apikey": "",
21+
},
22+
"remote": {
23+
"accesstoken": "",
24+
"apikey": "",
25+
"avatar": "",
26+
"dynamicRemoteAccessType": "DISABLED",
27+
"email": "",
28+
"idtoken": "",
29+
"localApiKey": "",
30+
"refreshtoken": "",
31+
"regWizTime": "",
32+
"upnpEnabled": "",
33+
"username": "",
34+
"wanaccess": "",
35+
"wanport": "",
36+
},
37+
"upc": {
38+
"apikey": "",
39+
},
40+
}
41+
`);
4142
});
4243

4344
test('it creates a MEMORY config with NO OPTIONAL values', () => {
4445
const basicConfig = initialState;
4546
const config = getWriteableConfig(basicConfig, 'memory');
4647
expect(config).toMatchInlineSnapshot(`
47-
{
48-
"api": {
49-
"extraOrigins": "",
50-
"version": "",
51-
},
52-
"connectionStatus": {
53-
"minigraph": "PRE_INIT",
54-
},
55-
"local": {},
56-
"notifier": {
57-
"apikey": "",
58-
},
59-
"remote": {
60-
"accesstoken": "",
61-
"allowedOrigins": "/var/run/unraid-notifications.sock, /var/run/unraid-php.sock, /var/run/unraid-cli.sock, https://connect.myunraid.net, https://connect-staging.myunraid.net, https://dev-my.myunraid.net:4000",
62-
"apikey": "",
63-
"avatar": "",
64-
"dynamicRemoteAccessType": "DISABLED",
65-
"email": "",
66-
"idtoken": "",
67-
"localApiKey": "",
68-
"refreshtoken": "",
69-
"regWizTime": "",
70-
"username": "",
71-
"wanaccess": "",
72-
"wanport": "",
73-
},
74-
"upc": {
75-
"apikey": "",
76-
},
77-
}
78-
`);
48+
{
49+
"api": {
50+
"extraOrigins": "",
51+
"version": "",
52+
},
53+
"connectionStatus": {
54+
"minigraph": "PRE_INIT",
55+
"upnpStatus": "",
56+
},
57+
"local": {},
58+
"notifier": {
59+
"apikey": "",
60+
},
61+
"remote": {
62+
"accesstoken": "",
63+
"allowedOrigins": "/var/run/unraid-notifications.sock, /var/run/unraid-php.sock, /var/run/unraid-cli.sock, https://connect.myunraid.net, https://connect-staging.myunraid.net, https://dev-my.myunraid.net:4000",
64+
"apikey": "",
65+
"avatar": "",
66+
"dynamicRemoteAccessType": "DISABLED",
67+
"email": "",
68+
"idtoken": "",
69+
"localApiKey": "",
70+
"refreshtoken": "",
71+
"regWizTime": "",
72+
"upnpEnabled": "",
73+
"username": "",
74+
"wanaccess": "",
75+
"wanport": "",
76+
},
77+
"upc": {
78+
"apikey": "",
79+
},
80+
}
81+
`);
7982
});
8083

8184
test('it creates a FLASH config with OPTIONAL values', () => {

api/src/consts.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ export const JWKS_LOCAL_PAYLOAD: JSONWebKeySet = {
6868
},
6969
],
7070
};
71+
7172
export const OAUTH_BASE_URL =
7273
'https://cognito-idp.us-west-2.amazonaws.com/us-west-2_btSkhlsEk';
7374
export const OAUTH_CLIENT_ID = '53ci4o48gac8vq5jepubkjmo36';
Lines changed: 45 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,95 +1,64 @@
1+
import { isEqual } from 'lodash-es';
2+
13
import { getAllowedOrigins } from '@app/common/allowed-origins';
2-
import { DynamicRemoteAccessType } from '@app/graphql/generated/api/types';
4+
import { initialState } from '@app/store/modules/config';
35
import {
4-
type SliceState as ConfigSliceState,
5-
initialState,
6-
} from '@app/store/modules/config';
7-
import { type RecursivePartial } from '@app/types';
8-
import type {
96
MyServersConfig,
107
MyServersConfigMemory,
8+
MyServersConfigMemorySchema,
9+
MyServersConfigSchema,
1110
} from '@app/types/my-servers-config';
12-
import { isEqual } from 'lodash-es';
1311

12+
// Define ConfigType and ConfigObject
1413
export type ConfigType = 'flash' | 'memory';
15-
type ConfigObject<T> = T extends 'flash'
16-
? MyServersConfig
17-
: T extends 'memory'
18-
? MyServersConfigMemory
19-
: never;
14+
2015
/**
21-
*
22-
* @param config Config to read from to create a new formatted server config to write
23-
* @param mode 'flash' or 'memory', changes what fields are included in the writeable payload
24-
* @returns
16+
* Get a writeable configuration based on the mode ('flash' or 'memory').
2517
*/
26-
2718
export const getWriteableConfig = <T extends ConfigType>(
28-
config: ConfigSliceState,
19+
config: T extends 'memory' ? MyServersConfigMemory : MyServersConfig,
2920
mode: T
30-
): ConfigObject<T> => {
31-
// Get current state
32-
const { api, local, notifier, remote, upc, connectionStatus } = config;
21+
): T extends 'memory' ? MyServersConfigMemory : MyServersConfig => {
22+
const schema = mode === 'memory' ? MyServersConfigMemorySchema : MyServersConfigSchema;
3323

34-
// Create new state
35-
36-
const newState: ConfigObject<T> = {
37-
api: {
38-
version: api?.version ?? initialState.api.version,
39-
extraOrigins: api?.extraOrigins ?? initialState.api.extraOrigins,
40-
},
41-
local: {},
42-
notifier: {
43-
apikey: notifier.apikey ?? initialState.notifier.apikey,
44-
},
24+
const defaultConfig = schema.parse(initialState);
25+
// Use a type assertion for the mergedConfig to include `connectionStatus` only if `mode === 'memory`
26+
const mergedConfig = {
27+
...defaultConfig,
28+
...config,
4529
remote: {
46-
wanaccess: remote.wanaccess ?? initialState.remote.wanaccess,
47-
wanport: remote.wanport ?? initialState.remote.wanport,
48-
...(remote.upnpEnabled ? { upnpEnabled: remote.upnpEnabled } : {}),
49-
apikey: remote.apikey ?? initialState.remote.apikey,
50-
localApiKey: remote.localApiKey ?? initialState.remote.localApiKey,
51-
email: remote.email ?? initialState.remote.email,
52-
username: remote.username ?? initialState.remote.username,
53-
avatar: remote.avatar ?? initialState.remote.avatar,
54-
regWizTime: remote.regWizTime ?? initialState.remote.regWizTime,
55-
idtoken: remote.idtoken ?? initialState.remote.idtoken,
56-
accesstoken: remote.accesstoken ?? initialState.remote.accesstoken,
57-
refreshtoken:
58-
remote.refreshtoken ?? initialState.remote.refreshtoken,
59-
...(mode === 'memory'
60-
? {
61-
allowedOrigins:
62-
getAllowedOrigins().join(', ')
63-
}
64-
: {}),
65-
dynamicRemoteAccessType: remote.dynamicRemoteAccessType ?? DynamicRemoteAccessType.DISABLED,
30+
...defaultConfig.remote,
31+
...config.remote,
6632
},
67-
upc: {
68-
apikey: upc.apikey ?? initialState.upc.apikey,
69-
},
70-
...(mode === 'memory'
71-
? {
72-
connectionStatus: {
73-
minigraph:
74-
connectionStatus.minigraph ??
75-
initialState.connectionStatus.minigraph,
76-
...(connectionStatus.upnpStatus
77-
? { upnpStatus: connectionStatus.upnpStatus }
78-
: {}),
79-
},
80-
}
81-
: {}),
82-
} as ConfigObject<T>;
83-
return newState;
33+
} as T extends 'memory' ? MyServersConfigMemory : MyServersConfig;
34+
35+
if (mode === 'memory') {
36+
(mergedConfig as MyServersConfigMemory).remote.allowedOrigins = getAllowedOrigins().join(', ');
37+
(mergedConfig as MyServersConfigMemory).connectionStatus = {
38+
...(defaultConfig as MyServersConfigMemory).connectionStatus,
39+
...(config as MyServersConfigMemory).connectionStatus,
40+
};
41+
}
42+
43+
return schema.parse(mergedConfig) as any; // Narrowing ensures correct typing
8444
};
8545

8646
/**
87-
* Helper function to convert an object into a normalized config file.
88-
* This is used for loading config files and ensure changes have been made before the state is merged.
47+
* Check if two configurations are equivalent by normalizing them through the Zod schema.
8948
*/
9049
export const areConfigsEquivalent = (
91-
newConfigFile: RecursivePartial<MyServersConfig>,
92-
currentConfig: ConfigSliceState
93-
): boolean =>
94-
// Enable to view config diffs: logger.debug(getDiff(getWriteableConfig(currentConfig, 'flash'), newConfigFile));
95-
isEqual(newConfigFile, getWriteableConfig(currentConfig, 'flash'));
50+
newConfigFile: Partial<MyServersConfigMemory>, // Use Partial here for flexibility
51+
currentConfig: MyServersConfig
52+
): boolean => {
53+
// Parse and validate the new config file using the schema (with default values applied)
54+
const normalizedNewConfig = MyServersConfigSchema.parse({
55+
...currentConfig, // Use currentConfig as a baseline to fill missing fields
56+
...newConfigFile,
57+
});
58+
59+
// Get the writeable configuration for the current config
60+
const normalizedCurrentConfig = getWriteableConfig(currentConfig, 'flash');
61+
62+
// Compare the normalized configurations
63+
return isEqual(normalizedNewConfig, normalizedCurrentConfig);
64+
};

api/src/types/my-servers-config.d.ts

Lines changed: 0 additions & 58 deletions
This file was deleted.

0 commit comments

Comments
 (0)