Skip to content

Commit b74e09e

Browse files
committed
wip
1 parent d9ccd50 commit b74e09e

File tree

4 files changed

+238
-14
lines changed

4 files changed

+238
-14
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import type { DatabasePool } from 'slonik';
22
import type { Logger } from '@graphql-hive/logger';
33
import type { EmailProvider } from './lib/emails/providers';
4+
import { RequestBroker } from './lib/webhooks/schema-change-notification';
45

56
export type Context = {
67
logger: Logger;
78
email: EmailProvider;
89
pg: DatabasePool;
10+
requestBroker: RequestBroker | null;
911
};
Lines changed: 213 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,214 @@
1-
import { z } from 'zod';
1+
import zod from 'zod';
2+
import { OpenTelemetryConfigurationModel } from '@hive/service-common';
3+
import { createConnectionString } from '@hive/storage';
24

3-
export const env = {};
5+
const isNumberString = (input: unknown) => zod.string().regex(/^\d+$/).safeParse(input).success;
6+
7+
const numberFromNumberOrNumberString = (input: unknown): number | undefined => {
8+
if (typeof input == 'number') return input;
9+
if (isNumberString(input)) return Number(input);
10+
};
11+
12+
const NumberFromString = zod.preprocess(numberFromNumberOrNumberString, zod.number().min(1));
13+
14+
// treat an empty string (`''`) as undefined
15+
const emptyString = <T extends zod.ZodType>(input: T) => {
16+
return zod.preprocess((value: unknown) => {
17+
if (value === '') return undefined;
18+
return value;
19+
}, input);
20+
};
21+
22+
const EnvironmentModel = zod.object({
23+
PORT: emptyString(NumberFromString.optional()),
24+
ENVIRONMENT: emptyString(zod.string().optional()),
25+
RELEASE: emptyString(zod.string().optional()),
26+
HEARTBEAT_ENDPOINT: emptyString(zod.string().url().optional()),
27+
EMAIL_FROM: zod.string().email(),
28+
});
29+
30+
const SentryModel = zod.union([
31+
zod.object({
32+
SENTRY: emptyString(zod.literal('0').optional()),
33+
}),
34+
zod.object({
35+
SENTRY: zod.literal('1'),
36+
SENTRY_DSN: zod.string(),
37+
}),
38+
]);
39+
40+
const PostgresModel = zod.object({
41+
POSTGRES_SSL: emptyString(zod.union([zod.literal('1'), zod.literal('0')]).optional()),
42+
POSTGRES_HOST: zod.string(),
43+
POSTGRES_PORT: NumberFromString,
44+
POSTGRES_DB: zod.string(),
45+
POSTGRES_USER: zod.string(),
46+
POSTGRES_PASSWORD: emptyString(zod.string().optional()),
47+
});
48+
49+
const PostmarkEmailModel = zod.object({
50+
EMAIL_PROVIDER: zod.literal('postmark'),
51+
EMAIL_PROVIDER_POSTMARK_TOKEN: zod.string(),
52+
EMAIL_PROVIDER_POSTMARK_MESSAGE_STREAM: zod.string(),
53+
});
54+
55+
const SMTPEmailModel = zod.object({
56+
EMAIL_PROVIDER: zod.literal('smtp'),
57+
EMAIL_PROVIDER_SMTP_PROTOCOL: emptyString(
58+
zod.union([zod.literal('smtp'), zod.literal('smtps')]).optional(),
59+
),
60+
EMAIL_PROVIDER_SMTP_HOST: zod.string(),
61+
EMAIL_PROVIDER_SMTP_PORT: NumberFromString,
62+
EMAIL_PROVIDER_SMTP_AUTH_USERNAME: zod.string(),
63+
EMAIL_PROVIDER_SMTP_AUTH_PASSWORD: zod.string(),
64+
EMAIL_PROVIDER_SMTP_REJECT_UNAUTHORIZED: emptyString(
65+
zod.union([zod.literal('0'), zod.literal('1')]).optional(),
66+
),
67+
});
68+
69+
const SendmailEmailModel = zod.object({
70+
EMAIL_PROVIDER: zod.literal('sendmail'),
71+
});
72+
73+
const MockEmailProviderModel = zod.object({
74+
EMAIL_PROVIDER: zod.literal('mock'),
75+
});
76+
77+
const EmailProviderModel = zod.union([
78+
PostmarkEmailModel,
79+
MockEmailProviderModel,
80+
SMTPEmailModel,
81+
SendmailEmailModel,
82+
]);
83+
84+
const PrometheusModel = zod.object({
85+
PROMETHEUS_METRICS: emptyString(zod.union([zod.literal('0'), zod.literal('1')]).optional()),
86+
PROMETHEUS_METRICS_LABEL_INSTANCE: emptyString(zod.string().optional()),
87+
PROMETHEUS_METRICS_PORT: emptyString(NumberFromString.optional()),
88+
});
89+
90+
const LogModel = zod.object({
91+
LOG_LEVEL: emptyString(
92+
zod
93+
.union([
94+
zod.literal('trace'),
95+
zod.literal('debug'),
96+
zod.literal('info'),
97+
zod.literal('warn'),
98+
zod.literal('error'),
99+
])
100+
.optional(),
101+
),
102+
REQUEST_LOGGING: emptyString(zod.union([zod.literal('0'), zod.literal('1')]).optional()).default(
103+
'1',
104+
),
105+
});
106+
107+
const configs = {
108+
base: EnvironmentModel.safeParse(process.env),
109+
email: EmailProviderModel.safeParse(process.env),
110+
sentry: SentryModel.safeParse(process.env),
111+
postgres: PostgresModel.safeParse(process.env),
112+
prometheus: PrometheusModel.safeParse(process.env),
113+
log: LogModel.safeParse(process.env),
114+
tracing: OpenTelemetryConfigurationModel.safeParse(process.env),
115+
};
116+
117+
const environmentErrors: Array<string> = [];
118+
119+
for (const config of Object.values(configs)) {
120+
if (config.success === false) {
121+
environmentErrors.push(JSON.stringify(config.error.format(), null, 4));
122+
}
123+
}
124+
125+
if (environmentErrors.length) {
126+
const fullError = environmentErrors.join(`\n`);
127+
console.error('❌ Invalid environment variables:', fullError);
128+
process.exit(1);
129+
}
130+
131+
function extractConfig<Input, Output>(config: zod.SafeParseReturnType<Input, Output>): Output {
132+
if (!config.success) {
133+
throw new Error('Something went wrong.');
134+
}
135+
return config.data;
136+
}
137+
138+
const base = extractConfig(configs.base);
139+
const email = extractConfig(configs.email);
140+
const postgres = extractConfig(configs.postgres);
141+
const sentry = extractConfig(configs.sentry);
142+
const prometheus = extractConfig(configs.prometheus);
143+
const log = extractConfig(configs.log);
144+
const tracing = extractConfig(configs.tracing);
145+
146+
const emailProviderConfig =
147+
email.EMAIL_PROVIDER === 'postmark'
148+
? ({
149+
provider: 'postmark' as const,
150+
token: email.EMAIL_PROVIDER_POSTMARK_TOKEN,
151+
messageStream: email.EMAIL_PROVIDER_POSTMARK_MESSAGE_STREAM,
152+
} as const)
153+
: email.EMAIL_PROVIDER === 'smtp'
154+
? ({
155+
provider: 'smtp' as const,
156+
protocol: email.EMAIL_PROVIDER_SMTP_PROTOCOL ?? 'smtp',
157+
host: email.EMAIL_PROVIDER_SMTP_HOST,
158+
port: email.EMAIL_PROVIDER_SMTP_PORT,
159+
auth: {
160+
user: email.EMAIL_PROVIDER_SMTP_AUTH_USERNAME,
161+
pass: email.EMAIL_PROVIDER_SMTP_AUTH_PASSWORD,
162+
},
163+
tls: {
164+
rejectUnauthorized: email.EMAIL_PROVIDER_SMTP_REJECT_UNAUTHORIZED !== '0',
165+
},
166+
} as const)
167+
: email.EMAIL_PROVIDER === 'sendmail'
168+
? ({ provider: 'sendmail' } as const)
169+
: ({ provider: 'mock' } as const);
170+
171+
export type EmailProviderConfig = typeof emailProviderConfig;
172+
export type PostmarkEmailProviderConfig = Extract<EmailProviderConfig, { provider: 'postmark' }>;
173+
export type SMTPEmailProviderConfig = Extract<EmailProviderConfig, { provider: 'smtp' }>;
174+
export type SendmailEmailProviderConfig = Extract<EmailProviderConfig, { provider: 'sendmail' }>;
175+
export type MockEmailProviderConfig = Extract<EmailProviderConfig, { provider: 'mock' }>;
176+
177+
export const env = {
178+
environment: base.ENVIRONMENT,
179+
release: base.RELEASE ?? 'local',
180+
http: {
181+
port: base.PORT ?? 6260,
182+
},
183+
tracing: {
184+
enabled: !!tracing.OPENTELEMETRY_COLLECTOR_ENDPOINT,
185+
collectorEndpoint: tracing.OPENTELEMETRY_COLLECTOR_ENDPOINT,
186+
},
187+
email: {
188+
provider: emailProviderConfig,
189+
emailFrom: base.EMAIL_FROM,
190+
},
191+
sentry: sentry.SENTRY === '1' ? { dsn: sentry.SENTRY_DSN } : null,
192+
log: {
193+
level: log.LOG_LEVEL ?? 'info',
194+
},
195+
prometheus:
196+
prometheus.PROMETHEUS_METRICS === '1'
197+
? {
198+
labels: {
199+
instance: prometheus.PROMETHEUS_METRICS_LABEL_INSTANCE ?? 'workflows',
200+
},
201+
port: prometheus.PROMETHEUS_METRICS_PORT ?? 10_254,
202+
}
203+
: null,
204+
postgres: {
205+
connectionString: createConnectionString({
206+
ssl: postgres.POSTGRES_SSL === '1',
207+
host: postgres.POSTGRES_HOST,
208+
db: postgres.POSTGRES_DB,
209+
password: postgres.POSTGRES_PASSWORD,
210+
port: postgres.POSTGRES_PORT,
211+
user: postgres.POSTGRES_USER,
212+
}),
213+
},
214+
} as const;

packages/services/workflows/src/index.ts

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,10 @@ import {
77
import { createPool } from 'slonik';
88
import { Logger } from '@graphql-hive/logger';
99
import { Context } from './context.js';
10+
import { env } from './environment.js';
11+
import { createEmailProvider } from './lib/emails/providers.js';
1012

11-
// TODO: slonik interop
12-
//
13-
const databaseUrl = 'postgresql://postgres:postgres@localhost:5432/registry';
14-
15-
const pool = await createPool(databaseUrl);
13+
const pg = await createPool(env.postgres.connectionString);
1614

1715
const modules = await Promise.all([
1816
import('./tasks/audit-log-export.js'),
@@ -23,12 +21,15 @@ const modules = await Promise.all([
2321
import('./tasks/schema-change-notification.js'),
2422
import('./tasks/usage-rate-limit-exceeded.js'),
2523
import('./tasks/usage-rate-limit-warning.js'),
26-
import('./workflows/user-onboarding.js'),
2724
]);
2825

29-
const logger = new Logger({ level: 'debug' });
26+
const logger = new Logger({ level: env.log.level });
3027

31-
const context: Context = { logger, email: {} };
28+
const context: Context = {
29+
logger,
30+
email: createEmailProvider(env.email.provider, env.email.emailFrom),
31+
pg,
32+
};
3233

3334
function logLevel(level: GraphileLogLevel) {
3435
switch (level) {
@@ -48,11 +49,18 @@ function logLevel(level: GraphileLogLevel) {
4849
}
4950

5051
let runner: Runner = await run({
51-
logger: new GraphileLogger(scope => (level, message, meta) => {
52+
logger: new GraphileLogger(_scope => (level, message, _meta) => {
5253
logger[logLevel(level)](message);
5354
}),
55+
// TODO: define cron jobs!
5456
crontab: ' ',
55-
connectionString: databaseUrl,
57+
connectionString: env.postgres.connectionString,
5658
taskList: Object.fromEntries(modules.map(module => module.task(context))),
57-
59+
});
60+
61+
process.on('SIGINT', () => {
62+
logger.info('Received shutdown signal. Stopping runner.');
63+
runner.stop().then(() => {
64+
logger.info('Runner shutdown successful.');
65+
});
5866
});

packages/services/workflows/src/tasks/schema-change-notification.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { z } from 'zod';
22
import { defineTask, implementTask } from '../kit.js';
3+
import { sendWebhook } from '../lib/webhooks/schema-change-notification.js';
34

45
export const SchemaChangeNotificationTask = defineTask({
56
name: 'schemaChangeNotification',
@@ -35,4 +36,6 @@ export const SchemaChangeNotificationTask = defineTask({
3536
}),
3637
});
3738

38-
export const task = implementTask(SchemaChangeNotificationTask, async args => {});
39+
export const task = implementTask(SchemaChangeNotificationTask, async args => {
40+
await sendWebhook({});
41+
});

0 commit comments

Comments
 (0)