@accounter/server
Version:
Accounter GraphQL server
316 lines (291 loc) • 9.73 kB
text/typescript
import { config as dotenv } from 'dotenv';
import zod from 'zod';
// Prefer isolated test env file when provided, otherwise fall back to repo-level .env
dotenv({
path:
process.env.TEST_ENV_FILE && process.env.TEST_ENV_FILE.trim() !== ''
? process.env.TEST_ENV_FILE
: ['.env', '../../.env'],
debug: process.env.RELEASE ? false : true,
});
const isNumberString = (input: unknown) => zod.string().regex(/^\d+$/).safeParse(input).success;
const numberFromNumberOrNumberString = (input: unknown): number | undefined => {
if (typeof input === 'number') return input;
if (isNumberString(input)) return Number(input);
return undefined;
};
const NumberFromString = zod.preprocess(numberFromNumberOrNumberString, zod.number().min(1));
// treat an empty string (`''`) as undefined
const emptyString = <T extends zod.ZodType>(input: T) => {
return zod.preprocess((value: unknown) => {
if (value === '') return undefined;
return value;
}, input);
};
const PostgresModel = zod.object({
POSTGRES_SSL: emptyString(zod.union([zod.literal('1'), zod.literal('0')]).optional()),
POSTGRES_HOST: zod.string(),
POSTGRES_PORT: NumberFromString,
POSTGRES_DB: zod.string(),
POSTGRES_USER: zod.string(),
POSTGRES_PASSWORD: zod.string(),
POSTGRES_MAX_CLIENTS: emptyString(NumberFromString).optional().default(20),
});
const CloudinaryModel = zod.union([
zod
.object({
CLOUDINARY_NAME: zod.string().optional(),
CLOUDINARY_API_KEY: zod.string().optional(),
CLOUDINARY_API_SECRET: zod.string().optional(),
})
.superRefine((data, ctx) => {
if (
!!data.CLOUDINARY_NAME !== !!data.CLOUDINARY_API_KEY ||
!!data.CLOUDINARY_NAME !== !!data.CLOUDINARY_API_SECRET
) {
ctx.addIssue({
code: 'custom',
message:
'CLOUDINARY_NAME, CLOUDINARY_API_KEY, and CLOUDINARY_API_SECRET must be provided together.',
});
}
}),
zod.void(),
]);
const GreenInvoiceModel = zod.union([
zod
.object({
GREEN_INVOICE_ID: zod.string().optional(),
GREEN_INVOICE_SECRET: zod.string().optional(),
})
.superRefine((data, ctx) => {
if (!!data.GREEN_INVOICE_ID !== !!data.GREEN_INVOICE_SECRET) {
ctx.addIssue({
code: 'custom',
message: 'GREEN_INVOICE_ID and GREEN_INVOICE_SECRET must be provided together.',
});
}
}),
zod.void(),
]);
const HiveModel = zod.union([
zod.object({
HIVE_TOKEN: zod.string().optional(),
}),
zod.void(),
]);
const GoogleDriveModel = zod.union([
zod.object({
GOOGLE_DRIVE_API_KEY: zod.string().optional(),
}),
zod.void(),
]);
const DeelModel = zod.union([
zod.object({
DEEL_TOKEN: zod.string().optional(),
}),
zod.void(),
]);
const CredentialsModel = zod.object({
CREDENTIALS_ENCRYPTION_KEY: zod
.string()
.trim()
.regex(/^[a-f0-9]{64}$/i),
});
const GeneralModel = zod.object({
FRONTEND_URL: zod.url().optional(),
});
const OtelModel = zod
.object({
OTEL_ENABLED: emptyString(
zod
.union([zod.literal('1'), zod.literal('0')])
.optional()
.default('0'),
),
OTEL_SERVICE_NAME: emptyString(zod.string().optional().default('accounter-server')),
OTEL_SERVICE_NAMESPACE: emptyString(zod.string().optional().default('accounter')),
OTEL_DEPLOYMENT_ENV: emptyString(
zod
.string()
.optional()
.default(process.env.NODE_ENV ?? 'development'),
),
OTEL_EXPORTER_OTLP_ENDPOINT: emptyString(zod.string().optional()),
OTEL_EXPORTER_OTLP_HEADERS: emptyString(zod.string().optional()),
OTEL_TRACES_SAMPLER: emptyString(
zod
.enum([
'parentbased_traceidratio',
'always_on',
'always_off',
'traceidratio',
'parentbased_always_on',
'parentbased_always_off',
])
.optional()
.default('always_on'),
),
OTEL_TRACES_SAMPLER_ARG: emptyString(zod.string().optional()),
OTEL_STARTUP_STRICT: emptyString(
zod.union([zod.literal('true'), zod.literal('false')]).optional(),
),
})
.superRefine((data, ctx) => {
if (data.OTEL_ENABLED === '1' && !data.OTEL_EXPORTER_OTLP_ENDPOINT) {
ctx.addIssue({
code: 'custom',
path: ['OTEL_EXPORTER_OTLP_ENDPOINT'],
message: 'OTEL_EXPORTER_OTLP_ENDPOINT is required when OTEL_ENABLED is "1".',
});
}
const usesRatioSampler =
data.OTEL_TRACES_SAMPLER === 'parentbased_traceidratio' ||
data.OTEL_TRACES_SAMPLER === 'traceidratio';
if (usesRatioSampler) {
if (data.OTEL_TRACES_SAMPLER_ARG === undefined) {
ctx.addIssue({
code: 'custom',
path: ['OTEL_TRACES_SAMPLER_ARG'],
message:
'OTEL_TRACES_SAMPLER_ARG is required when OTEL_TRACES_SAMPLER is a ratio sampler ("traceidratio" or "parentbased_traceidratio").',
});
} else {
const num = Number(data.OTEL_TRACES_SAMPLER_ARG);
if (!Number.isFinite(num) || num < 0 || num > 1) {
ctx.addIssue({
code: 'custom',
path: ['OTEL_TRACES_SAMPLER_ARG'],
message: 'OTEL_TRACES_SAMPLER_ARG must be a numeric string between 0 and 1.',
});
}
}
}
});
const Auth0Model = zod.union([
zod.object({
AUTH0_DOMAIN: zod.string().min(1),
AUTH0_AUDIENCE: zod.string().min(1),
AUTH0_CLIENT_ID: zod.string().min(1),
AUTH0_CLIENT_SECRET: zod.string().min(1),
AUTH0_MANAGEMENT_AUDIENCE: zod.string().min(1),
}),
// If no Auth0 variables are provided, validation passes (optional configuration)
// We use a looser object check here because process.env is always an object
zod
.object({
AUTH0_DOMAIN: zod.literal('').optional(),
AUTH0_AUDIENCE: zod.literal('').optional(),
AUTH0_CLIENT_ID: zod.literal('').optional(),
AUTH0_CLIENT_SECRET: zod.literal('').optional(),
AUTH0_MANAGEMENT_AUDIENCE: zod.literal('').optional(),
})
.transform(() => undefined),
]);
const configs = {
postgres: PostgresModel.safeParse(process.env),
cloudinary: CloudinaryModel.safeParse(process.env),
greenInvoice: GreenInvoiceModel.safeParse(process.env),
hive: HiveModel.safeParse(process.env),
googleDrive: GoogleDriveModel.safeParse(process.env),
auth0: Auth0Model.safeParse(process.env),
deel: DeelModel.safeParse(process.env),
credentials: CredentialsModel.safeParse(process.env),
general: GeneralModel.safeParse(process.env),
otel: OtelModel.safeParse(process.env),
};
const environmentErrors: Array<string> = [];
for (const config of Object.values(configs)) {
if (config.success === false) {
environmentErrors.push(JSON.stringify(config.error.format(), null, 4));
}
}
if (environmentErrors.length) {
const fullError = environmentErrors.join(`\n`);
console.error('❌ Invalid environment variables:', fullError);
process.exit(1);
}
function extractConfig<Output>(config: zod.ZodSafeParseResult<Output>): Output {
if (!config.success) {
throw new Error('Something went wrong.');
}
return config.data;
}
const postgres = extractConfig(configs.postgres);
const cloudinary = extractConfig(configs.cloudinary);
const greenInvoice = extractConfig(configs.greenInvoice);
const hive = extractConfig(configs.hive);
const googleDrive = extractConfig(configs.googleDrive);
const auth0 = extractConfig(configs.auth0);
const deel = extractConfig(configs.deel);
const credentials = extractConfig(configs.credentials);
const general = extractConfig(configs.general);
const otel = extractConfig(configs.otel);
export const env = {
postgres: {
host: postgres.POSTGRES_HOST,
port: postgres.POSTGRES_PORT,
db: postgres.POSTGRES_DB,
user: postgres.POSTGRES_USER,
password: postgres.POSTGRES_PASSWORD,
ssl: postgres.POSTGRES_SSL === '1',
max: postgres.POSTGRES_MAX_CLIENTS,
},
cloudinary: cloudinary?.CLOUDINARY_API_KEY
? {
name: cloudinary.CLOUDINARY_NAME!,
apiKey: cloudinary.CLOUDINARY_API_KEY!,
apiSecret: cloudinary.CLOUDINARY_API_SECRET!,
}
: undefined,
greenInvoice: greenInvoice?.GREEN_INVOICE_ID
? {
id: greenInvoice.GREEN_INVOICE_ID!,
secret: greenInvoice.GREEN_INVOICE_SECRET!,
}
: undefined,
hive: hive?.HIVE_TOKEN
? {
hiveToken: hive.HIVE_TOKEN!,
}
: undefined,
googleDrive: googleDrive?.GOOGLE_DRIVE_API_KEY
? {
driveApiKey: googleDrive.GOOGLE_DRIVE_API_KEY!,
}
: undefined,
deel: deel?.DEEL_TOKEN
? {
apiToken: deel.DEEL_TOKEN!,
}
: undefined,
credentialsEncryptionKey: credentials.CREDENTIALS_ENCRYPTION_KEY,
auth0: auth0
? {
domain: auth0.AUTH0_DOMAIN,
audience: auth0.AUTH0_AUDIENCE,
clientId: auth0.AUTH0_CLIENT_ID,
clientSecret: auth0.AUTH0_CLIENT_SECRET,
managementAudience: auth0.AUTH0_MANAGEMENT_AUDIENCE,
}
: undefined,
general: {
frontendUrl: general?.FRONTEND_URL,
},
otel: {
enabled: otel.OTEL_ENABLED === '1',
serviceName: otel.OTEL_SERVICE_NAME,
serviceNamespace: otel.OTEL_SERVICE_NAMESPACE,
deploymentEnv: otel.OTEL_DEPLOYMENT_ENV,
exporterEndpoint: otel.OTEL_EXPORTER_OTLP_ENDPOINT,
exporterHeaders: otel.OTEL_EXPORTER_OTLP_HEADERS,
tracesSampler: otel.OTEL_TRACES_SAMPLER,
tracesSamplerArg:
(otel.OTEL_TRACES_SAMPLER === 'parentbased_traceidratio' ||
otel.OTEL_TRACES_SAMPLER === 'traceidratio') &&
otel.OTEL_TRACES_SAMPLER_ARG !== undefined
? Number(otel.OTEL_TRACES_SAMPLER_ARG)
: undefined,
startupStrict: otel.OTEL_STARTUP_STRICT === 'true',
},
} as const;