diff --git a/functions/send-email-link/src/index.ts b/functions/send-email-link/src/index.ts index f7b7e61ce..de297d953 100644 --- a/functions/send-email-link/src/index.ts +++ b/functions/send-email-link/src/index.ts @@ -1,4 +1,5 @@ import app from '@constructive-io/knative-job-fn'; +import type { Request, Response, NextFunction } from 'express'; import { GraphQLClient } from 'graphql-request'; import gql from 'graphql-tag'; import { generate } from '@launchql/mjml'; @@ -46,6 +47,60 @@ const GetDatabaseInfo = gql` } `; +type GetUserResult = { + user: { + username: string | null; + displayName: string | null; + profilePicture: string | null; + } | null; +}; + +type LegalTermsCompany = { + website: string; + nick: string; + name: string; +}; + +type LegalTermsEmails = { + support: string; +}; + +type LegalTermsData = { + emails: LegalTermsEmails; + company: LegalTermsCompany; +}; + +type SiteTheme = { + theme: { + primary: string; + }; +}; + +type SiteDomain = { + subdomain: string | null; + domain: string; +}; + +type SiteLogo = { + url?: string | null; +} | null; + +type DatabaseInfoResult = { + database?: + | { + sites?: { + nodes?: Array<{ + domains?: { nodes?: SiteDomain[] }; + logo?: SiteLogo; + title?: string; + siteThemes?: { nodes?: SiteTheme[] }; + siteModules?: { nodes?: Array<{ data: LegalTermsData }> }; + }>; + }; + } + | null; +}; + type SendEmailParams = { email_type: 'invite_email' | 'forgot_password' | 'email_verification'; email: string; @@ -64,6 +119,10 @@ type GraphQLContext = { databaseId: string; }; +type SendEmailLinkMissing = { missing: string }; +type SendEmailLinkSuccess = { complete: true; dryRun?: true }; +export type SendEmailLinkResult = SendEmailLinkMissing | SendEmailLinkSuccess; + const getRequiredEnv = (name: string): string => { const value = process.env[name]; if (!value) { @@ -94,10 +153,10 @@ const createGraphQLClient = ( export const sendEmailLink = async ( params: SendEmailParams, context: GraphQLContext -) => { +): Promise => { const { client, meta, databaseId } = context; - const validateForType = (): { missing?: string } | null => { + const validateForType = (): SendEmailLinkMissing | null => { switch (params.email_type) { case 'invite_email': if (!params.invite_token || !params.sender_id) { @@ -131,7 +190,7 @@ export const sendEmailLink = async ( return typeValidation; } - const databaseInfo = await meta.request(GetDatabaseInfo, { + const databaseInfo = await meta.request(GetDatabaseInfo, { databaseId }); @@ -199,7 +258,7 @@ export const sendEmailLink = async ( const scope = Number(params.invite_type) === 2 ? 'org' : 'app'; url.searchParams.append('type', scope); - const inviter = await client.request(GetUser, { + const inviter = await client.request(GetUser, { userId: params.sender_id }); inviterName = inviter?.user?.displayName; @@ -239,7 +298,7 @@ export const sendEmailLink = async ( break; } default: - return false; + return { missing: 'email_type' }; } const link = url.href; @@ -295,7 +354,11 @@ export const sendEmailLink = async ( }; // HTTP/Knative entrypoint (used by @constructive-io/knative-job-fn wrapper) -app.post('/', async (req: any, res: any, next: any) => { +type SendEmailLinkRequestBody = Partial; +type SendEmailLinkRequest = Request<{}, SendEmailLinkResult, SendEmailLinkRequestBody>; +type SendEmailLinkResponse = Response; + +app.post('/', async (req: SendEmailLinkRequest, res: SendEmailLinkResponse, next: NextFunction) => { try { const params = (req.body || {}) as SendEmailParams; @@ -329,7 +392,7 @@ export default app; if (require.main === module) { const port = Number(process.env.PORT ?? 8080); // @constructive-io/knative-job-fn exposes a .listen method that delegates to the Express app - (app as any).listen(port, () => { + app.listen(port, () => { // eslint-disable-next-line no-console console.log(`[send-email-link] listening on port ${port}`); }); diff --git a/functions/simple-email/src/index.ts b/functions/simple-email/src/index.ts index 736423e0a..0b4378773 100644 --- a/functions/simple-email/src/index.ts +++ b/functions/simple-email/src/index.ts @@ -1,4 +1,5 @@ import app from '@constructive-io/knative-job-fn'; +import type { Request, Response, NextFunction } from 'express'; import { parseEnvBoolean } from '@pgpmjs/env'; import { send as sendEmail } from '@launchql/postmaster'; @@ -27,7 +28,11 @@ const getRequiredField = ( const isDryRun = parseEnvBoolean(process.env.SIMPLE_EMAIL_DRY_RUN) ?? false; -app.post('/', async (req: any, res: any, next: any) => { +type SimpleEmailResponseBody = { complete: true }; +type SimpleEmailRequest = Request<{}, SimpleEmailResponseBody, Partial>; +type SimpleEmailResponse = Response; + +app.post('/', async (req: SimpleEmailRequest, res: SimpleEmailResponse, next: NextFunction) => { try { const payload = (req.body || {}) as SimpleEmailPayload; @@ -92,7 +97,7 @@ export default app; if (require.main === module) { const port = Number(process.env.PORT ?? 8080); // @constructive-io/knative-job-fn exposes a .listen method that delegates to the underlying Express app - (app as any).listen(port, () => { + app.listen(port, () => { // eslint-disable-next-line no-console console.log(`[simple-email] listening on port ${port}`); }); diff --git a/jobs/job-pg/src/index.ts b/jobs/job-pg/src/index.ts index 45e41aa20..8d364fa37 100644 --- a/jobs/job-pg/src/index.ts +++ b/jobs/job-pg/src/index.ts @@ -55,11 +55,11 @@ const end = (pool: Pool): void => { // Callbacks registered for pool close events can accept arbitrary arguments // (we forward whatever was passed to `onClose`). -type PoolCloseCallback = (...args: any[]) => Promise | void; +type PoolCloseCallback = (...args: unknown[]) => Promise | void; class PoolManager { private pgPool: Pool; - private callbacks: Array<[PoolCloseCallback, any, any[]]>; + private callbacks: Array<[PoolCloseCallback, unknown, unknown[]]>; private _closed: boolean; constructor({ pgPool = new Pool(pgPoolConfig) }: { pgPool?: Pool } = {}) { @@ -77,7 +77,11 @@ class PoolManager { }); } - onClose(fn: PoolCloseCallback, context?: any, args: any[] = []): void { + onClose( + fn: PoolCloseCallback, + context?: unknown, + args: unknown[] = [] + ): void { this.callbacks.push([fn, context, args]); } diff --git a/jobs/job-utils/src/index.ts b/jobs/job-utils/src/index.ts index ab8b70f04..b38e80f09 100644 --- a/jobs/job-utils/src/index.ts +++ b/jobs/job-utils/src/index.ts @@ -28,8 +28,13 @@ import { Logger } from '@pgpmjs/logger'; const log = new Logger('jobs:core'); +export type QueryParams = readonly unknown[] | undefined; + export type PgClientLike = { - query(text: string, params?: any[]): Promise<{ rows: T[] }>; + query( + text: string, + params?: QueryParams + ): Promise<{ rows: T[] }>; }; export { @@ -72,7 +77,7 @@ export const completeJob = async ( ); }; -export const getJob = async ( +export const getJob = async ( client: PgClientLike, { workerId, supportedTaskNames }: GetJobParams ): Promise => { @@ -86,7 +91,7 @@ export const getJob = async ( return (job as T) ?? null; }; -export const getScheduledJob = async ( +export const getScheduledJob = async ( client: PgClientLike, { workerId, supportedTaskNames }: GetScheduledJobParams ): Promise => { @@ -100,10 +105,10 @@ export const getScheduledJob = async ( return (job as T) ?? null; }; -export const runScheduledJob = async ( +export const runScheduledJob = async ( client: PgClientLike, { jobId }: RunScheduledJobParams -): Promise => { +): Promise => { log.info(`runScheduledJob job[${jobId}]`); try { const { @@ -112,9 +117,14 @@ export const runScheduledJob = async ( `SELECT * FROM "${JOBS_SCHEMA}".run_scheduled_job($1);`, [jobId] ); - return job ?? null; - } catch (e: any) { - if (e?.message === 'ALREADY_SCHEDULED') { + return (job as T) ?? null; + } catch (e: unknown) { + if ( + e && + typeof e === 'object' && + 'message' in e && + (e as { message?: unknown }).message === 'ALREADY_SCHEDULED' + ) { return null; } throw e; diff --git a/jobs/knative-job-example/src/index.ts b/jobs/knative-job-example/src/index.ts index 5f8729847..2a5d5906c 100644 --- a/jobs/knative-job-example/src/index.ts +++ b/jobs/knative-job-example/src/index.ts @@ -1,6 +1,7 @@ import app from '@constructive-io/knative-job-fn'; +import type { Request, Response, NextFunction } from 'express'; -app.post('/', async (req: any, res: any, next: any) => { +app.post('/', async (req: Request, res: Response, next: NextFunction) => { if (req.body.throw) { next(new Error('THROWN_ERROR')); } else { diff --git a/jobs/knative-job-fn/src/index.ts b/jobs/knative-job-fn/src/index.ts index 77befd2b1..5a037966f 100644 --- a/jobs/knative-job-fn/src/index.ts +++ b/jobs/knative-job-fn/src/index.ts @@ -1,4 +1,9 @@ -import express from 'express'; +import express, { + type Express, + type Request, + type Response, + type NextFunction +} from 'express'; import bodyParser from 'body-parser'; import http from 'node:http'; import https from 'node:https'; @@ -13,7 +18,7 @@ type JobContext = { databaseId: string | undefined; }; -function getHeaders(req: any) { +function getHeaders(req: Request) { return { 'x-worker-id': req.get('X-Worker-Id'), 'x-job-id': req.get('X-Job-Id'), @@ -22,17 +27,17 @@ function getHeaders(req: any) { }; } -const app: any = express(); +const app: Express = express(); app.use(bodyParser.json()); // Basic request logging for all incoming job invocations. -app.use((req: any, res: any, next: any) => { +app.use((req: Request, res: Response, next: NextFunction) => { try { // Log only the headers we care about plus a shallow body snapshot const headers = getHeaders(req); - let body: any; + let body: unknown; if (req.body && typeof req.body === 'object') { // Only log top-level keys to avoid exposing sensitive body contents. body = { keys: Object.keys(req.body) }; @@ -57,7 +62,7 @@ app.use((req: any, res: any, next: any) => { }); // Echo job headers back on responses for debugging/traceability. -app.use((req: any, res: any, next: any) => { +app.use((req: Request, res: Response, next: NextFunction) => { res.set({ 'Content-Type': 'application/json', 'X-Worker-Id': req.get('X-Worker-Id'), @@ -172,7 +177,7 @@ const sendJobCallback = async ( }; // Attach per-request context and a finish hook to send success callbacks. -app.use((req: any, res: any, next: any) => { +app.use((req: Request, res: Response, next: NextFunction) => { const ctx: JobContext = { callbackUrl: req.get('X-Callback-Url'), workerId: req.get('X-Worker-Id'), @@ -205,64 +210,70 @@ app.use((req: any, res: any, next: any) => { }); export default { - post: function (...args: any[]) { - return app.post.apply(app, args as any); - }, - listen: (port: any, cb: () => void = () => {}) => { + post: (...args: Parameters): ReturnType => + app.post(...args), + listen: (port: number, cb: () => void = () => {}) => { // NOTE Remember that Express middleware executes in order. // You should define error handlers last, after all other middleware. - // Otherwise, your error handler won't get called + // Otherwise, your error handler won't get called. // eslint-disable-next-line no-unused-vars - app.use(async (error: any, req: any, res: any, next: any) => { - res.set({ - 'Content-Type': 'application/json', - 'X-Job-Error': true - }); + app.use( + async ( + error: any, + req: Request, + res: Response, + next: NextFunction + ) => { + res.set({ + 'Content-Type': 'application/json', + 'X-Job-Error': true + }); - // Mark job as having errored via callback, if available. - try { - const ctx: JobContext | undefined = res.locals?.jobContext; - if (ctx && !res.locals.jobCallbackSent) { - res.locals.jobCallbackSent = true; - await sendJobCallback(ctx, 'error', error?.message); + // Mark job as having errored via callback, if available. + try { + const ctx: JobContext | undefined = res.locals?.jobContext; + if (ctx && !res.locals.jobCallbackSent) { + res.locals.jobCallbackSent = true; + await sendJobCallback(ctx, 'error', error?.message); + } + } catch (err) { + // eslint-disable-next-line no-console + console.error('[knative-job-fn] Failed to send error callback', err); } - } catch (err) { - // eslint-disable-next-line no-console - console.error('[knative-job-fn] Failed to send error callback', err); - } - // Log the full error context for debugging. - try { - const headers = getHeaders(req); - - // Some error types (e.g. GraphQL ClientError) expose response info. - const errorDetails: any = { - message: error?.message, - name: error?.name, - stack: error?.stack - }; - - if (error?.response) { - errorDetails.response = { - status: error.response.status, - statusText: error.response.statusText, - errors: error.response.errors, - data: error.response.data + // Log the full error context for debugging. + try { + const headers = getHeaders(req); + + // Some error types (e.g. GraphQL ClientError) expose response info. + const errorDetails: any = { + message: error?.message, + name: error?.name, + stack: error?.stack }; + + if (error?.response) { + errorDetails.response = { + status: error.response.status, + statusText: error.response.statusText, + errors: error.response.errors, + data: error.response.data + }; + } + + // eslint-disable-next-line no-console + console.error('[knative-job-fn] Function error', { + headers, + path: req.originalUrl || req.url, + error: errorDetails + }); + } catch { + // never throw from the error logger } - // eslint-disable-next-line no-console - console.error('[knative-job-fn] Function error', { - headers, - path: req.originalUrl || req.url, - error: errorDetails - }); - } catch { - // never throw from the error logger + res.status(200).json({ message: error.message }); } - - res.status(200).json({ message: error.message }); - }); + ); app.listen(port, cb); } };