feat: use zod to parse config

This commit is contained in:
Eli Bosley
2025-01-18 10:42:34 -05:00
parent 6f9977eea0
commit 02c197f244
7 changed files with 187 additions and 203 deletions

View File

@@ -10,72 +10,75 @@ test('it creates a FLASH config with NO OPTIONAL values', () => {
const basicConfig = initialState;
const config = getWriteableConfig(basicConfig, 'flash');
expect(config).toMatchInlineSnapshot(`
{
"api": {
"extraOrigins": "",
"version": "",
},
"local": {},
"notifier": {
"apikey": "",
},
"remote": {
"accesstoken": "",
"apikey": "",
"avatar": "",
"dynamicRemoteAccessType": "DISABLED",
"email": "",
"idtoken": "",
"localApiKey": "",
"refreshtoken": "",
"regWizTime": "",
"username": "",
"wanaccess": "",
"wanport": "",
},
"upc": {
"apikey": "",
},
}
`);
{
"api": {
"extraOrigins": "",
"version": "",
},
"local": {},
"notifier": {
"apikey": "",
},
"remote": {
"accesstoken": "",
"apikey": "",
"avatar": "",
"dynamicRemoteAccessType": "DISABLED",
"email": "",
"idtoken": "",
"localApiKey": "",
"refreshtoken": "",
"regWizTime": "",
"upnpEnabled": "",
"username": "",
"wanaccess": "",
"wanport": "",
},
"upc": {
"apikey": "",
},
}
`);
});
test('it creates a MEMORY config with NO OPTIONAL values', () => {
const basicConfig = initialState;
const config = getWriteableConfig(basicConfig, 'memory');
expect(config).toMatchInlineSnapshot(`
{
"api": {
"extraOrigins": "",
"version": "",
},
"connectionStatus": {
"minigraph": "PRE_INIT",
},
"local": {},
"notifier": {
"apikey": "",
},
"remote": {
"accesstoken": "",
"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",
"apikey": "",
"avatar": "",
"dynamicRemoteAccessType": "DISABLED",
"email": "",
"idtoken": "",
"localApiKey": "",
"refreshtoken": "",
"regWizTime": "",
"username": "",
"wanaccess": "",
"wanport": "",
},
"upc": {
"apikey": "",
},
}
`);
{
"api": {
"extraOrigins": "",
"version": "",
},
"connectionStatus": {
"minigraph": "PRE_INIT",
"upnpStatus": "",
},
"local": {},
"notifier": {
"apikey": "",
},
"remote": {
"accesstoken": "",
"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",
"apikey": "",
"avatar": "",
"dynamicRemoteAccessType": "DISABLED",
"email": "",
"idtoken": "",
"localApiKey": "",
"refreshtoken": "",
"regWizTime": "",
"upnpEnabled": "",
"username": "",
"wanaccess": "",
"wanport": "",
},
"upc": {
"apikey": "",
},
}
`);
});
test('it creates a FLASH config with OPTIONAL values', () => {

View File

@@ -68,6 +68,7 @@ export const JWKS_LOCAL_PAYLOAD: JSONWebKeySet = {
},
],
};
export const OAUTH_BASE_URL =
'https://cognito-idp.us-west-2.amazonaws.com/us-west-2_btSkhlsEk';
export const OAUTH_CLIENT_ID = '53ci4o48gac8vq5jepubkjmo36';

View File

@@ -1,95 +1,64 @@
import { getAllowedOrigins } from '@app/common/allowed-origins';
import { DynamicRemoteAccessType } from '@app/graphql/generated/api/types';
import {
type SliceState as ConfigSliceState,
initialState,
} from '@app/store/modules/config';
import { type RecursivePartial } from '@app/types';
import type {
MyServersConfig,
MyServersConfigMemory,
} from '@app/types/my-servers-config';
import { isEqual } from 'lodash-es';
export type ConfigType = 'flash' | 'memory';
type ConfigObject<T> = T extends 'flash'
? MyServersConfig
: T extends 'memory'
? MyServersConfigMemory
: never;
/**
*
* @param config Config to read from to create a new formatted server config to write
* @param mode 'flash' or 'memory', changes what fields are included in the writeable payload
* @returns
*/
export const getWriteableConfig = <T extends ConfigType>(
config: ConfigSliceState,
mode: T
): ConfigObject<T> => {
// Get current state
const { api, local, notifier, remote, upc, connectionStatus } = config;
import { getAllowedOrigins } from '@app/common/allowed-origins';
import { initialState } from '@app/store/modules/config';
import {
MyServersConfig,
MyServersConfigMemory,
MyServersConfigMemorySchema,
MyServersConfigSchema,
} from '@app/types/my-servers-config';
// Create new state
const newState: ConfigObject<T> = {
api: {
version: api?.version ?? initialState.api.version,
extraOrigins: api?.extraOrigins ?? initialState.api.extraOrigins,
},
local: {},
notifier: {
apikey: notifier.apikey ?? initialState.notifier.apikey,
},
// Define ConfigType and ConfigObject
export type ConfigType = 'flash' | 'memory';
/**
* Get a writeable configuration based on the mode ('flash' or 'memory').
*/
export const getWriteableConfig = <T extends ConfigType>(
config: T extends 'memory' ? MyServersConfigMemory : MyServersConfig,
mode: T
): T extends 'memory' ? MyServersConfigMemory : MyServersConfig => {
const schema = mode === 'memory' ? MyServersConfigMemorySchema : MyServersConfigSchema;
const defaultConfig = schema.parse(initialState);
// Use a type assertion for the mergedConfig to include `connectionStatus` only if `mode === 'memory`
const mergedConfig = {
...defaultConfig,
...config,
remote: {
wanaccess: remote.wanaccess ?? initialState.remote.wanaccess,
wanport: remote.wanport ?? initialState.remote.wanport,
...(remote.upnpEnabled ? { upnpEnabled: remote.upnpEnabled } : {}),
apikey: remote.apikey ?? initialState.remote.apikey,
localApiKey: remote.localApiKey ?? initialState.remote.localApiKey,
email: remote.email ?? initialState.remote.email,
username: remote.username ?? initialState.remote.username,
avatar: remote.avatar ?? initialState.remote.avatar,
regWizTime: remote.regWizTime ?? initialState.remote.regWizTime,
idtoken: remote.idtoken ?? initialState.remote.idtoken,
accesstoken: remote.accesstoken ?? initialState.remote.accesstoken,
refreshtoken:
remote.refreshtoken ?? initialState.remote.refreshtoken,
...(mode === 'memory'
? {
allowedOrigins:
getAllowedOrigins().join(', ')
}
: {}),
dynamicRemoteAccessType: remote.dynamicRemoteAccessType ?? DynamicRemoteAccessType.DISABLED,
...defaultConfig.remote,
...config.remote,
},
upc: {
apikey: upc.apikey ?? initialState.upc.apikey,
},
...(mode === 'memory'
? {
connectionStatus: {
minigraph:
connectionStatus.minigraph ??
initialState.connectionStatus.minigraph,
...(connectionStatus.upnpStatus
? { upnpStatus: connectionStatus.upnpStatus }
: {}),
},
}
: {}),
} as ConfigObject<T>;
return newState;
} as T extends 'memory' ? MyServersConfigMemory : MyServersConfig;
if (mode === 'memory') {
(mergedConfig as MyServersConfigMemory).remote.allowedOrigins = getAllowedOrigins().join(', ');
(mergedConfig as MyServersConfigMemory).connectionStatus = {
...(defaultConfig as MyServersConfigMemory).connectionStatus,
...(config as MyServersConfigMemory).connectionStatus,
};
}
return schema.parse(mergedConfig) as any; // Narrowing ensures correct typing
};
/**
* Helper function to convert an object into a normalized config file.
* This is used for loading config files and ensure changes have been made before the state is merged.
* Check if two configurations are equivalent by normalizing them through the Zod schema.
*/
export const areConfigsEquivalent = (
newConfigFile: RecursivePartial<MyServersConfig>,
currentConfig: ConfigSliceState
): boolean =>
// Enable to view config diffs: logger.debug(getDiff(getWriteableConfig(currentConfig, 'flash'), newConfigFile));
isEqual(newConfigFile, getWriteableConfig(currentConfig, 'flash'));
newConfigFile: Partial<MyServersConfigMemory>, // Use Partial here for flexibility
currentConfig: MyServersConfig
): boolean => {
// Parse and validate the new config file using the schema (with default values applied)
const normalizedNewConfig = MyServersConfigSchema.parse({
...currentConfig, // Use currentConfig as a baseline to fill missing fields
...newConfigFile,
});
// Get the writeable configuration for the current config
const normalizedCurrentConfig = getWriteableConfig(currentConfig, 'flash');
// Compare the normalized configurations
return isEqual(normalizedNewConfig, normalizedCurrentConfig);
};

View File

@@ -1,58 +0,0 @@
import { type DynamicRemoteAccessType, type MinigraphStatus } from '@app/graphql/generated/api/types';
interface MyServersConfig extends Record<string, unknown> {
api: {
version: string;
extraOrigins: string;
};
local: {};
notifier: {
apikey: string;
};
remote: {
wanaccess: string;
wanport: string;
upnpEnabled?: string;
apikey: string;
localApiKey?: string;
email: string;
username: string;
avatar: string;
regWizTime: string;
accesstoken: string;
idtoken: string;
refreshtoken: string;
allowedOrigins?: string;
dynamicRemoteAccessType?: DynamicRemoteAccessType;
};
upc: {
apikey: string;
};
}
export interface MyServersConfigWithMandatoryHiddenFields extends MyServersConfig {
api: {
extraOrigins: string;
};
remote: MyServersConfig['remote'] & {
upnpEnabled: string;
dynamicRemoteAccessType: DynamicRemoteAccessType;
};
}
export interface MyServersConfigMemory extends MyServersConfig {
connectionStatus: {
minigraph: MinigraphStatus;
upnpStatus?: null | string;
};
remote: MyServersConfig['remote'] & {
allowedOrigins: string;
};
}
export interface MyServersConfigMemoryWithMandatoryHiddenFields extends MyServersConfigMemory {
connectionStatus: {
minigraph: MinigraphStatus;
upnpStatus?: null | string;
};
}

View File

@@ -0,0 +1,67 @@
import { z } from 'zod';
import { DynamicRemoteAccessType, MinigraphStatus } from '@app/graphql/generated/api/types';
// Define Zod schemas
const ApiConfigSchema = z.object({
version: z.string(),
extraOrigins: z.string(),
});
const NotifierConfigSchema = z.object({
apikey: z.string(),
});
const RemoteConfigSchema = z.object({
wanaccess: z.string(),
wanport: z.string(),
upnpEnabled: z.string(),
apikey: z.string(),
localApiKey: z.string(),
email: z.string(),
username: z.string(),
avatar: z.string(),
regWizTime: z.string(),
accesstoken: z.string(),
idtoken: z.string(),
refreshtoken: z.string(),
dynamicRemoteAccessType: z.nativeEnum(DynamicRemoteAccessType),
});
const UpcConfigSchema = z.object({
apikey: z.string(),
});
// Base config schema
export const MyServersConfigSchema = z.object({
api: ApiConfigSchema,
local: z.object({}), // Empty object
notifier: NotifierConfigSchema,
remote: RemoteConfigSchema,
upc: UpcConfigSchema,
});
// Memory config schema
export const ConnectionStatusSchema = z.object({
minigraph: z.nativeEnum(MinigraphStatus),
upnpStatus: z.string().nullable().optional(),
});
export const MyServersConfigMemorySchema = MyServersConfigSchema.extend({
connectionStatus: ConnectionStatusSchema,
remote: RemoteConfigSchema.extend({
allowedOrigins: z.string(),
}),
});
// Memory config with mandatory hidden fields schema
export const MyServersConfigMemoryWithMandatoryHiddenFieldsSchema = MyServersConfigMemorySchema.extend({
connectionStatus: ConnectionStatusSchema,
});
// Infer and export types from Zod schemas
export type MyServersConfig = z.infer<typeof MyServersConfigSchema>;
export type MyServersConfigMemory = z.infer<typeof MyServersConfigMemorySchema>;
export type MyServersConfigMemoryWithMandatoryHiddenFields = z.infer<
typeof MyServersConfigMemoryWithMandatoryHiddenFieldsSchema
>;

View File

@@ -28,6 +28,7 @@ export class StartCommand extends CommandRunner {
);
if (stdout) {
this.logger.log(stdout);
process.exit(0);
}
if (stderr) {
this.logger.error(stderr);

View File

@@ -29,6 +29,7 @@ export class ValidateTokenCommand extends CommandRunner {
async run(passedParams: string[]): Promise<void> {
if (passedParams.length !== 1) {
this.logger.error('Please pass token argument only');
process.exit(1);
}
const token = passedParams[0];