-
Notifications
You must be signed in to change notification settings - Fork 138
refactor: Zod-based config loader #998
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
fabiovincenzi
wants to merge
11
commits into
finos:main
Choose a base branch
from
fabiovincenzi:config
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
80f2736
feat: create zod schema for config
fabiovincenzi 29d4562
refactor: migrate config loader to Zod
fabiovincenzi a865d65
refactor: integrate Zod-based loader and pass config to services
fabiovincenzi 5a4e068
Merge branch 'main' into config
fabiovincenzi 0fe5e0a
chore: ignore jsonschema in unused deps check
fabiovincenzi 9758a5b
Merge branch 'config' of https://github.com/fabiovincenzi/git-proxy i…
fabiovincenzi 6663aa4
Merge branch 'main' into config
JamieSlome 29b26a3
Merge branch 'main' into config
fabiovincenzi c08da33
Merge branch 'main' into config
fabiovincenzi fa83356
Merge branch 'main' into config
fabiovincenzi 6f84652
fix: fix config schema
fabiovincenzi File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,205 @@ | ||
import { z } from 'zod'; | ||
|
||
const TempPasswordSchema = z.object({ | ||
sendEmail: z.boolean().default(false), | ||
emailConfig: z.record(z.unknown()).default({}), | ||
}); | ||
|
||
const AuthorisedItemSchema = z.object({ | ||
project: z.string(), | ||
name: z.string(), | ||
url: z.string().regex(/^(?:https?:\/\/.+\.git|git@[^:]+:[^/]+\/.+\.git)$/i, { | ||
message: 'Must be a Git HTTPS URL (https://... .git) or SSH URL (git@...:... .git)', | ||
}), | ||
}); | ||
|
||
const FsSinkSchema = z.object({ | ||
type: z.literal('fs'), | ||
params: z.object({ filepath: z.string() }), | ||
enabled: z.boolean().default(true), | ||
}); | ||
|
||
const MongoSinkSchema = z.object({ | ||
type: z.literal('mongo'), | ||
connectionString: z.string(), | ||
options: z.object({ | ||
useNewUrlParser: z.boolean().default(true), | ||
useUnifiedTopology: z.boolean().default(true), | ||
tlsAllowInvalidCertificates: z.boolean().default(false), | ||
ssl: z.boolean().default(false), | ||
}), | ||
enabled: z.boolean().default(false), | ||
}); | ||
|
||
const SinkSchema = z.discriminatedUnion('type', [FsSinkSchema, MongoSinkSchema]); | ||
|
||
const ActiveDirectoryConfigSchema = z.object({ | ||
url: z.string(), | ||
baseDN: z.string(), | ||
searchBase: z.string(), | ||
}); | ||
|
||
const LocalAuthSchema = z.object({ | ||
type: z.literal('local'), | ||
enabled: z.boolean().default(true), | ||
}); | ||
|
||
const ADAuthSchema = z.object({ | ||
type: z.literal('ActiveDirectory'), | ||
enabled: z.boolean().default(false), | ||
adminGroup: z.string().default(''), | ||
userGroup: z.string().default(''), | ||
domain: z.string().default(''), | ||
adConfig: ActiveDirectoryConfigSchema, | ||
}); | ||
|
||
const AuthenticationSchema = z.discriminatedUnion('type', [LocalAuthSchema, ADAuthSchema]); | ||
|
||
const GithubApiSchema = z.object({ | ||
baseUrl: z.string().url(), | ||
}); | ||
|
||
const CommitEmailSchema = z.object({ | ||
local: z.object({ block: z.string().default('') }), | ||
domain: z.object({ allow: z.string().default('.*') }), | ||
}); | ||
|
||
const CommitBlockSchema = z.object({ | ||
literals: z.array(z.string()).default([]), | ||
patterns: z.array(z.string()).default([]), | ||
}); | ||
|
||
const CommitDiffSchema = z.object({ | ||
block: z.object({ | ||
literals: z.array(z.string()).default([]), | ||
patterns: z.array(z.string()).default([]), | ||
providers: z.record(z.unknown()).default({}), | ||
}), | ||
}); | ||
|
||
const AttestationQuestionSchema = z.object({ | ||
label: z.string(), | ||
tooltip: z.object({ | ||
text: z.string(), | ||
links: z.array(z.string()).default([]), | ||
}), | ||
}); | ||
|
||
export const RateLimitSchema = z | ||
.object({ | ||
windowMs: z.number({ description: 'Sliding window in milliseconds' }), | ||
limit: z.number({ description: 'Maximum number of requests' }), | ||
statusCode: z.number().optional().default(429), | ||
message: z.string().optional().default('Too many requests'), | ||
}) | ||
.strict(); | ||
|
||
const FileConfigSourceSchema = z | ||
.object({ | ||
type: z.literal('file'), | ||
enabled: z.boolean().default(false), | ||
path: z.string(), | ||
}) | ||
.strict(); | ||
|
||
const HttpConfigSourceSchema = z | ||
.object({ | ||
type: z.literal('http'), | ||
enabled: z.boolean().default(false), | ||
url: z.string().url(), | ||
headers: z.record(z.string()).default({}), | ||
auth: z | ||
.object({ | ||
type: z.literal('bearer'), | ||
token: z.string().default(''), | ||
}) | ||
.strict() | ||
.default({ type: 'bearer', token: '' }), | ||
}) | ||
.strict(); | ||
|
||
const GitConfigSourceSchema = z | ||
.object({ | ||
type: z.literal('git'), | ||
enabled: z.boolean().default(false), | ||
repository: z.string(), | ||
branch: z.string().default('main'), | ||
path: z.string(), | ||
auth: z | ||
.object({ | ||
type: z.literal('ssh'), | ||
privateKeyPath: z.string(), | ||
}) | ||
.strict(), | ||
}) | ||
.strict(); | ||
|
||
const ConfigSourceSchema = z.discriminatedUnion('type', [ | ||
FileConfigSourceSchema, | ||
HttpConfigSourceSchema, | ||
GitConfigSourceSchema, | ||
]); | ||
|
||
export const ConfigurationSourcesSchema = z | ||
.object({ | ||
enabled: z.boolean(), | ||
reloadIntervalSeconds: z.number().optional().default(60), | ||
merge: z.boolean().optional().default(false), | ||
sources: z.array(ConfigSourceSchema).default([]), | ||
}) | ||
.strict(); | ||
|
||
export const ConfigSchema = z | ||
.object({ | ||
proxyUrl: z.string().url().default('https://github.com'), | ||
cookieSecret: z.string().default(''), | ||
sessionMaxAgeHours: z.number().int().positive().default(12), | ||
rateLimit: RateLimitSchema.default({ windowMs: 600000, limit: 150 }), | ||
configurationSources: ConfigurationSourcesSchema.default({ | ||
enabled: false, | ||
reloadIntervalSeconds: 60, | ||
merge: false, | ||
sources: [], | ||
}), | ||
tempPassword: TempPasswordSchema.default({}), | ||
authorisedList: z.array(AuthorisedItemSchema).default([]), | ||
sink: z.array(SinkSchema).default([]), | ||
authentication: z.array(AuthenticationSchema).default([{ type: 'local', enabled: true }]), | ||
api: z | ||
.object({ | ||
github: GithubApiSchema, | ||
}) | ||
.default({ github: { baseUrl: 'https://api.github.com' } }), | ||
commitConfig: z | ||
.object({ | ||
author: z.object({ email: CommitEmailSchema }), | ||
message: z.object({ block: CommitBlockSchema }), | ||
diff: CommitDiffSchema, | ||
}) | ||
.default({ | ||
author: { email: { local: { block: '' }, domain: { allow: '.*' } } }, | ||
message: { block: { literals: [], patterns: [] } }, | ||
diff: { block: { literals: [], patterns: [], providers: {} } }, | ||
}), | ||
attestationConfig: z | ||
.object({ | ||
questions: z.array(AttestationQuestionSchema).default([]), | ||
}) | ||
.default({ questions: [] }), | ||
domains: z.record(z.string(), z.string()).default({}), | ||
privateOrganizations: z.array(z.string()).default([]), | ||
urlShortener: z.string().default(''), | ||
contactEmail: z.string().default(''), | ||
csrfProtection: z.boolean().default(true), | ||
plugins: z.array(z.unknown()).default([]), | ||
tls: z | ||
.object({ | ||
enabled: z.boolean().default(false), | ||
key: z.string().default(''), | ||
cert: z.string().default(''), | ||
}) | ||
.default({}), | ||
}) | ||
.strict(); | ||
|
||
export type Config = z.infer<typeof ConfigSchema>; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,27 +1,43 @@ | ||
import { readFileSync } from 'fs'; | ||
import { join } from 'path'; | ||
import { validate as jsonSchemaValidate } from 'jsonschema'; | ||
import { ConfigSchema, type Config } from '../../proxy.config.schema'; | ||
|
||
export let configFile: string = join(process.cwd(), 'proxy.config.json'); | ||
export let configFile: string = join(process.cwd(), 'config.proxy.json'); | ||
export let config: Config; | ||
|
||
/** | ||
* Set the config file path. | ||
* @param {string} file - The path to the config file. | ||
* Sets the path to the configuration file. | ||
* | ||
* @param {string} file - The path to the configuration file. | ||
* @return {void} | ||
*/ | ||
export function setConfigFile(file: string) { | ||
configFile = file; | ||
} | ||
|
||
/** | ||
* Validate config file. | ||
* @param {string} configFilePath - The path to the config file. | ||
* @return {boolean} - Returns true if validation is successful. | ||
* @throws Will throw an error if the validation fails. | ||
* Loads and validates the configuration file using Zod. | ||
* If validation succeeds, the parsed config is stored in the exported `config`. | ||
* | ||
* @return {Config} The validated and default-filled configuration object. | ||
* @throws {ZodError} If validation fails. | ||
*/ | ||
export function validate(configFilePath: string = configFile!): boolean { | ||
const config = JSON.parse(readFileSync(configFilePath, 'utf-8')); | ||
const schemaPath = join(process.cwd(), 'config.schema.json'); | ||
const schema = JSON.parse(readFileSync(schemaPath, 'utf-8')); | ||
jsonSchemaValidate(config, schema, { required: true, throwError: true }); | ||
export function loadConfig(): Config { | ||
const raw = JSON.parse(readFileSync(configFile, 'utf-8')); | ||
const parsed = ConfigSchema.parse(raw); | ||
config = parsed; | ||
return parsed; | ||
} | ||
|
||
/** | ||
* Validates a configuration file without mutating the exported `config`. | ||
* | ||
* @param {string} [filePath=configFile] - Path to the configuration file to validate. | ||
* @return {boolean} Returns `true` if the file passes validation. | ||
* @throws {ZodError} If validation fails. | ||
*/ | ||
export function validate(filePath: string = configFile): boolean { | ||
const raw = JSON.parse(readFileSync(filePath, 'utf-8')); | ||
ConfigSchema.parse(raw); | ||
return true; | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One thing I like about using
json-schema
is that it shows a description of each config entry, so you know what each option is supposed to toggle/modify. Apparently,zod
can do this by using.meta()
or.describe()
. source