From 49d3932c9d6ad12c7a111d1c1b74faaa01436a9e Mon Sep 17 00:00:00 2001 From: Zhi Zhen Date: Tue, 16 Dec 2025 19:18:31 +0800 Subject: [PATCH 1/4] refactor send-email-link --- docker-compose.jobs.yml | 31 +++++++++++++-- functions/send-email-link/package.json | 1 + functions/send-email-link/src/index.ts | 52 ++++++++++++++++++++++++++ functions/simple-email/package.json | 1 + functions/simple-email/src/index.ts | 8 +--- jobs/job-utils/src/index.ts | 6 ++- jobs/job-utils/src/runtime.ts | 10 +++++ pnpm-lock.yaml | 6 +++ 8 files changed, 104 insertions(+), 11 deletions(-) diff --git a/docker-compose.jobs.yml b/docker-compose.jobs.yml index 2818e2ba0..30d20a4a2 100644 --- a/docker-compose.jobs.yml +++ b/docker-compose.jobs.yml @@ -57,6 +57,30 @@ services: networks: - constructive-net + # Send email link function (invite, password reset, verification) + send-email-link: + container_name: send-email-link + image: constructive-launchql:dev + entrypoint: ["node", "functions/send-email-link/dist/index.js"] + environment: + NODE_ENV: development + LOG_LEVEL: info + DEFAULT_DATABASE_ID: "dbe" + GRAPHQL_URL: "http://launchql-server:3000/graphql" + META_GRAPHQL_URL: "http://launchql-server:3000/graphql" + # Mailgun / email provider configuration for @launchql/postmaster + MAILGUN_API_KEY: "change-me-mailgun-api-key" + MAILGUN_KEY: "change-me-mailgun-api-key" + MAILGUN_DOMAIN: "mg.constructive.io" + MAILGUN_FROM: "no-reply@mg.constructive.io" + MAILGUN_REPLY: "info@mg.constructive.io" + SEND_EMAIL_LINK_DRY_RUN: "true" + ports: + # Expose function locally (optional) + - "8082:8080" + networks: + - constructive-net + # Jobs runtime: callback server + worker + scheduler knative-job-service: container_name: knative-job-service @@ -65,6 +89,7 @@ services: entrypoint: ["node", "jobs/knative-job-service/dist/run.js"] depends_on: - simple-email + - send-email-link environment: NODE_ENV: development @@ -78,7 +103,7 @@ services: # Worker configuration JOBS_SUPPORT_ANY: "false" - JOBS_SUPPORTED: "simple-email" + JOBS_SUPPORTED: "simple-email,send-email-link" HOSTNAME: "knative-job-service-1" # Callback HTTP server (job completion callbacks) @@ -93,8 +118,8 @@ services: # Development-only map from task identifier -> function URL # Used by @launchql/knative-job-worker when NODE_ENV !== 'production'. - # This lets the worker call the simple-email container directly in docker-compose. - INTERNAL_GATEWAY_DEVELOPMENT_MAP: '{"simple-email":"http://simple-email:8080"}' + # This lets the worker call the function containers directly in docker-compose. + INTERNAL_GATEWAY_DEVELOPMENT_MAP: '{"simple-email":"http://simple-email:8080","send-email-link":"http://send-email-link:8080"}' ports: - "8080:8080" diff --git a/functions/send-email-link/package.json b/functions/send-email-link/package.json index 853dee96c..ed45a432f 100644 --- a/functions/send-email-link/package.json +++ b/functions/send-email-link/package.json @@ -15,6 +15,7 @@ }, "dependencies": { "@launchql/knative-job-fn": "workspace:^", + "@launchql/job-utils": "workspace:^", "@launchql/mjml": "0.1.1", "@launchql/postmaster": "0.1.4", "@launchql/styled-email": "0.1.0", diff --git a/functions/send-email-link/src/index.ts b/functions/send-email-link/src/index.ts index 0846fd315..ef55d0155 100644 --- a/functions/send-email-link/src/index.ts +++ b/functions/send-email-link/src/index.ts @@ -3,6 +3,9 @@ import { GraphQLClient } from 'graphql-request'; import gql from 'graphql-tag'; import { generate } from '@launchql/mjml'; import { send } from '@launchql/postmaster'; +import { parseEnvBoolean } from '@launchql/job-utils'; + +const isDryRun = parseEnvBoolean('SEND_EMAIL_LINK_DRY_RUN', false); const GetUser = gql` query GetUser($userId: UUID!) { @@ -84,6 +87,28 @@ export const sendEmailLink = async ( ) => { const { client, meta, databaseId } = context; + const validateForType = (): { missing?: string } | null => { + switch (params.email_type) { + case 'invite_email': + if (!params.invite_token || !params.sender_id) { + return { missing: 'invite_token_or_sender_id' }; + } + return null; + case 'forgot_password': + if (!params.user_id || !params.reset_token) { + return { missing: 'user_id_or_reset_token' }; + } + return null; + case 'email_verification': + if (!params.email_id || !params.verification_token) { + return { missing: 'email_id_or_verification_token' }; + } + return null; + default: + return { missing: 'email_type' }; + } + }; + if (!params.email_type) { return { missing: 'email_type' }; } @@ -91,6 +116,24 @@ export const sendEmailLink = async ( return { missing: 'email' }; } + const typeValidation = validateForType(); + if (typeValidation) { + return typeValidation; + } + + if (isDryRun) { + // eslint-disable-next-line no-console + console.log('[send-email-link] DRY RUN email (skipping GraphQL and send)', { + email_type: params.email_type, + email: params.email, + hasInviteToken: Boolean(params.invite_token), + hasSenderId: Boolean(params.sender_id), + hasResetToken: Boolean(params.reset_token), + hasVerificationToken: Boolean(params.verification_token) + }); + return { complete: true, dryRun: true }; + } + const databaseInfo = await meta.request(GetDatabaseInfo, { databaseId }); @@ -251,3 +294,12 @@ app.post('*', async (req: any, res: any, next: any) => { export default app; +// When executed directly (e.g. via `node dist/index.js`), start an HTTP server. +if (require.main === module) { + const port = Number(process.env.PORT ?? 8080); + // @launchql/knative-job-fn exposes a .listen method that delegates to the Express app + (app as any).listen(port, () => { + // eslint-disable-next-line no-console + console.log(`[send-email-link] listening on port ${port}`); + }); +} diff --git a/functions/simple-email/package.json b/functions/simple-email/package.json index 0f2c694fd..407258beb 100644 --- a/functions/simple-email/package.json +++ b/functions/simple-email/package.json @@ -15,6 +15,7 @@ }, "dependencies": { "@launchql/knative-job-fn": "workspace:^", + "@launchql/job-utils": "workspace:^", "@launchql/postmaster": "0.1.4" } } diff --git a/functions/simple-email/src/index.ts b/functions/simple-email/src/index.ts index 4c079c8d8..533982bf6 100644 --- a/functions/simple-email/src/index.ts +++ b/functions/simple-email/src/index.ts @@ -1,4 +1,5 @@ import app from '@launchql/knative-job-fn'; +import { parseEnvBoolean } from '@launchql/job-utils'; import { send as sendEmail } from '@launchql/postmaster'; type SimpleEmailPayload = { @@ -24,12 +25,7 @@ const getRequiredField = ( return value; }; -const isDryRun = (() => { - const val = process.env.SIMPLE_EMAIL_DRY_RUN; - if (!val) return false; - const s = val.toLowerCase(); - return s === 'true' || s === '1' || s === 'yes' || s === 'y'; -})(); +const isDryRun = parseEnvBoolean('SIMPLE_EMAIL_DRY_RUN', false); app.post('*', async (req: any, res: any, next: any) => { try { diff --git a/jobs/job-utils/src/index.ts b/jobs/job-utils/src/index.ts index e86762c4e..e9c158f3d 100644 --- a/jobs/job-utils/src/index.ts +++ b/jobs/job-utils/src/index.ts @@ -19,7 +19,8 @@ import { getJobGatewayConfig, getJobGatewayDevMap, getJobsCallbackPort, - getCallbackBaseUrl + getCallbackBaseUrl, + parseEnvBoolean } from './runtime'; import { Logger } from '@pgpmjs/logger'; @@ -41,7 +42,8 @@ export { getJobGatewayConfig, getJobGatewayDevMap, getJobsCallbackPort, - getCallbackBaseUrl + getCallbackBaseUrl, + parseEnvBoolean }; const JOBS_SCHEMA = getJobSchema(); diff --git a/jobs/job-utils/src/runtime.ts b/jobs/job-utils/src/runtime.ts index 42ef64b81..fa5835fbe 100644 --- a/jobs/job-utils/src/runtime.ts +++ b/jobs/job-utils/src/runtime.ts @@ -139,3 +139,13 @@ export const getCallbackBaseUrl = (): string => { const port = getJobsCallbackPort(); return `http://${host}:${port}/callback`; }; + +// ---- Generic env helpers ---- +export const parseEnvBoolean = ( + name: string, + defaultValue = false +): boolean => { + const parsed = toBool(process.env[name]); + if (typeof parsed === 'boolean') return parsed; + return defaultValue; +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1b62175a4..e3f860176 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -70,6 +70,9 @@ importers: functions/send-email-link: dependencies: + '@launchql/job-utils': + specifier: workspace:^ + version: link:../../jobs/job-utils '@launchql/knative-job-fn': specifier: workspace:^ version: link:../../jobs/knative-job-fn @@ -91,6 +94,9 @@ importers: functions/simple-email: dependencies: + '@launchql/job-utils': + specifier: workspace:^ + version: link:../../jobs/job-utils '@launchql/knative-job-fn': specifier: workspace:^ version: link:../../jobs/knative-job-fn From 129c9c8e501b715c6efc0a86070ce42498e46b41 Mon Sep 17 00:00:00 2001 From: Zhi Zhen Date: Wed, 17 Dec 2025 08:27:01 +0800 Subject: [PATCH 2/4] fixed graphql server 404 error --- DEVELOPMENT_JOBS.md | 72 ++++++++++++++++++++++++-- docker-compose.jobs.yml | 12 +++-- functions/send-email-link/src/index.ts | 36 ++++++------- 3 files changed, 93 insertions(+), 27 deletions(-) diff --git a/DEVELOPMENT_JOBS.md b/DEVELOPMENT_JOBS.md index c9e423d0e..7267cecd3 100644 --- a/DEVELOPMENT_JOBS.md +++ b/DEVELOPMENT_JOBS.md @@ -2,9 +2,10 @@ This guide covers a local development workflow for the jobs stack: -- Postgres + `launchql-ext-jobs` +- Postgres + `launchql-database-jobs` - LaunchQL API server - `simple-email` function +- `send-email-link` function - `knative-job-service` It assumes: @@ -102,19 +103,48 @@ This starts: - `launchql-server` – GraphQL API server - `simple-email` – Knative-style HTTP function +- `send-email-link` – Knative-style HTTP function - `knative-job-service` – jobs runtime (callback server + worker + scheduler) --- -## 5. Enqueue a test job (simple-email) +## 5. Ensure GraphQL host routing works for `send-email-link` + +LaunchQL selects the API by the HTTP `Host` header using rows in `meta_public.domains`. + +For local development, `app-svc-local` seeds `admin.localhost` as the admin API domain. `docker-compose.jobs.yml` adds a Docker network alias so other containers can resolve `admin.localhost` to the `launchql-server` container, and `send-email-link` uses: + +- `GRAPHQL_URL=http://admin.localhost:3000/graphql` + +Quick check from your host (should return JSON, not HTML): + +```sh +curl -s -H 'Host: admin.localhost' \ + -H 'Content-Type: application/json' \ + -X POST http://localhost:3000/graphql \ + --data '{"query":"query { __typename }"}' +``` + +If your GraphQL server requires auth, set `GRAPHQL_AUTH_TOKEN` before starting the jobs stack (it is passed through to the `send-email-link` container). + +--- + +## 6. Enqueue a test job (simple-email) With the jobs stack running, you can enqueue a test job from your host into the Postgres container: +First, grab a real `database_id` (required by `send-email-link`, optional for `simple-email`): + +```sh +DBID="$(docker exec -i postgres psql -U postgres -d launchql -Atc 'SELECT id FROM collections_public.database ORDER BY created_at LIMIT 1;')" +echo "$DBID" +``` + ```sh docker exec -it postgres \ psql -U postgres -d launchql -c " SELECT app_jobs.add_job( - '00000000-0000-0000-0000-000000000001'::uuid, + '$DBID'::uuid, 'simple-email', json_build_object( 'to', 'user@example.com', @@ -129,7 +159,39 @@ You should then see the job picked up by `knative-job-service` and the email pay --- -## 6. Inspect logs and iterate +## 7. Enqueue a test job (`send-email-link`) + +`send-email-link` queries GraphQL for site/database metadata, so it requires: + +- The app/meta packages deployed in step 3 (`app-svc-local`, `db-meta`) +- A real `database_id` (use `$DBID` above) +- A GraphQL hostname that matches a seeded domain route (step 5) + +With `SEND_EMAIL_LINK_DRY_RUN=true` (default in `docker-compose.jobs.yml`), enqueue a job: + +```sh +docker exec -it postgres \ + psql -U postgres -d launchql -c " + SELECT app_jobs.add_job( + '$DBID'::uuid, + 'send-email-link', + json_build_object( + 'email_type', 'invite_email', + 'email', 'user@example.com', + 'invite_token', 'invite123', + 'sender_id', '00000000-0000-0000-0000-000000000000' + )::json + ); + " +``` + +You should see a log like: + +- `[send-email-link] DRY RUN email (skipping send) ...` + +--- + +## 8. Inspect logs and iterate To watch logs while you develop: @@ -153,7 +215,7 @@ docker compose -f docker-compose.jobs.yml up --build --- -## 7. Stopping services +## 9. Stopping services To stop only the jobs stack: diff --git a/docker-compose.jobs.yml b/docker-compose.jobs.yml index 30d20a4a2..3cb492b1b 100644 --- a/docker-compose.jobs.yml +++ b/docker-compose.jobs.yml @@ -32,7 +32,10 @@ services: ports: - "3000:3000" networks: - - constructive-net + constructive-net: + aliases: + # Let other containers call the admin API using the seeded domain route. + - admin.localhost # Simple email function (Knative-style HTTP function) simple-email: @@ -66,8 +69,11 @@ services: NODE_ENV: development LOG_LEVEL: info DEFAULT_DATABASE_ID: "dbe" - GRAPHQL_URL: "http://launchql-server:3000/graphql" - META_GRAPHQL_URL: "http://launchql-server:3000/graphql" + # LaunchQL selects the API by Host header; use a seeded domain route. + GRAPHQL_URL: "http://admin.localhost:3000/graphql" + META_GRAPHQL_URL: "http://admin.localhost:3000/graphql" + # Optional: provide an existing API token (Bearer) if your server requires it. + GRAPHQL_AUTH_TOKEN: "${GRAPHQL_AUTH_TOKEN:-}" # Mailgun / email provider configuration for @launchql/postmaster MAILGUN_API_KEY: "change-me-mailgun-api-key" MAILGUN_KEY: "change-me-mailgun-api-key" diff --git a/functions/send-email-link/src/index.ts b/functions/send-email-link/src/index.ts index ef55d0155..49de9df67 100644 --- a/functions/send-email-link/src/index.ts +++ b/functions/send-email-link/src/index.ts @@ -121,19 +121,6 @@ export const sendEmailLink = async ( return typeValidation; } - if (isDryRun) { - // eslint-disable-next-line no-console - console.log('[send-email-link] DRY RUN email (skipping GraphQL and send)', { - email_type: params.email_type, - email: params.email, - hasInviteToken: Boolean(params.invite_token), - hasSenderId: Boolean(params.sender_id), - hasResetToken: Boolean(params.reset_token), - hasVerificationToken: Boolean(params.verification_token) - }); - return { complete: true, dryRun: true }; - } - const databaseInfo = await meta.request(GetDatabaseInfo, { databaseId }); @@ -252,14 +239,25 @@ export const sendEmailLink = async ( } }); - await send({ - to: params.email, - subject, - html - }); + if (isDryRun) { + // eslint-disable-next-line no-console + console.log('[send-email-link] DRY RUN email (skipping send)', { + email_type: params.email_type, + email: params.email, + subject, + link + }); + } else { + await send({ + to: params.email, + subject, + html + }); + } return { - complete: true + complete: true, + ...(isDryRun ? { dryRun: true } : null) }; }; From 364bbe057bc8679275cb272c97adfa31c774fbc6 Mon Sep 17 00:00:00 2001 From: Zhi Zhen Date: Wed, 17 Dec 2025 10:23:13 +0800 Subject: [PATCH 3/4] env vars for dry run and mail keys --- DEVELOPMENT_JOBS.md | 63 +++++++++++++++++++++++++++++++++++++++++ docker-compose.jobs.yml | 12 ++++---- 2 files changed, 69 insertions(+), 6 deletions(-) diff --git a/DEVELOPMENT_JOBS.md b/DEVELOPMENT_JOBS.md index 7267cecd3..c70f19d03 100644 --- a/DEVELOPMENT_JOBS.md +++ b/DEVELOPMENT_JOBS.md @@ -108,6 +108,69 @@ This starts: --- +### Switching dry run vs real Mailgun sending + +By default, `docker-compose.jobs.yml` runs both email functions in dry-run mode (no real email is sent), and it uses placeholder Mailgun credentials unless you provide `MAILGUN_API_KEY` / `MAILGUN_KEY`. + +Quick start commands: + +Dry run: + +```sh +docker compose -f docker-compose.jobs.yml up -d --build --force-recreate +``` + +Real sending (Mailgun): + +```sh +MAILGUN_API_KEY="your-mailgun-key" MAILGUN_KEY="your-mailgun-key" SIMPLE_EMAIL_DRY_RUN=false SEND_EMAIL_LINK_DRY_RUN=false docker compose -f docker-compose.jobs.yml up -d --build --force-recreate +``` + +To use a real Mailgun key without editing `docker-compose.jobs.yml`, set these env vars before starting the stack (or put them in a local `.env` file in the `constructive/` directory). Don't commit your `.env`. + +```sh +export MAILGUN_API_KEY="your-mailgun-key" +export MAILGUN_KEY="your-mailgun-key" +``` + +If you're not using `mg.constructive.io`, also override `MAILGUN_DOMAIN`, `MAILGUN_FROM`, and `MAILGUN_REPLY` (for example in the override file below) to match your Mailgun setup. + +To actually send email (instead of dry-run), set these env vars (or put them in your local `.env`): + +```sh +export SIMPLE_EMAIL_DRY_RUN=false +export SEND_EMAIL_LINK_DRY_RUN=false +``` + +Then recreate the stack so the new env is applied: + +```sh +docker compose -f docker-compose.jobs.yml up -d --build --force-recreate +``` + +If you prefer not to export env vars, create a local override file (don't commit it) at `docker-compose.jobs.override.yml`: + +```yml +services: + simple-email: + environment: + SIMPLE_EMAIL_DRY_RUN: "false" + + send-email-link: + environment: + SEND_EMAIL_LINK_DRY_RUN: "false" +``` + +Start the stack with both files: + +```sh +docker compose -f docker-compose.jobs.yml -f docker-compose.jobs.override.yml up -d --build --force-recreate +``` + +To switch back to dry-run, set `SIMPLE_EMAIL_DRY_RUN=true` and `SEND_EMAIL_LINK_DRY_RUN=true` (or delete the override file) and recreate again. + +--- + ## 5. Ensure GraphQL host routing works for `send-email-link` LaunchQL selects the API by the HTTP `Host` header using rows in `meta_public.domains`. diff --git a/docker-compose.jobs.yml b/docker-compose.jobs.yml index 3cb492b1b..1e7d8cb56 100644 --- a/docker-compose.jobs.yml +++ b/docker-compose.jobs.yml @@ -46,11 +46,11 @@ services: environment: NODE_ENV: development LOG_LEVEL: info - SIMPLE_EMAIL_DRY_RUN: "true" + SIMPLE_EMAIL_DRY_RUN: "${SIMPLE_EMAIL_DRY_RUN:-true}" # Mailgun / email provider configuration for @launchql/postmaster # Replace with real credentials for local testing. - MAILGUN_API_KEY: "change-me-mailgun-api-key" - MAILGUN_KEY: "change-me-mailgun-api-key" + MAILGUN_API_KEY: "${MAILGUN_API_KEY:-change-me-mailgun-api-key}" + MAILGUN_KEY: "${MAILGUN_KEY:-change-me-mailgun-api-key}" MAILGUN_DOMAIN: "mg.constructive.io" MAILGUN_FROM: "no-reply@mg.constructive.io" MAILGUN_REPLY: "info@mg.constructive.io" @@ -75,12 +75,12 @@ services: # Optional: provide an existing API token (Bearer) if your server requires it. GRAPHQL_AUTH_TOKEN: "${GRAPHQL_AUTH_TOKEN:-}" # Mailgun / email provider configuration for @launchql/postmaster - MAILGUN_API_KEY: "change-me-mailgun-api-key" - MAILGUN_KEY: "change-me-mailgun-api-key" + MAILGUN_API_KEY: "${MAILGUN_API_KEY:-change-me-mailgun-api-key}" + MAILGUN_KEY: "${MAILGUN_KEY:-change-me-mailgun-api-key}" MAILGUN_DOMAIN: "mg.constructive.io" MAILGUN_FROM: "no-reply@mg.constructive.io" MAILGUN_REPLY: "info@mg.constructive.io" - SEND_EMAIL_LINK_DRY_RUN: "true" + SEND_EMAIL_LINK_DRY_RUN: "${SEND_EMAIL_LINK_DRY_RUN:-true}" ports: # Expose function locally (optional) - "8082:8080" From af357885733fd092e82cccd1495a80c5be609ab6 Mon Sep 17 00:00:00 2001 From: zetazzz Date: Wed, 17 Dec 2025 16:39:20 +0800 Subject: [PATCH 4/4] code reuse --- functions/send-email-link/package.json | 2 +- functions/send-email-link/src/index.ts | 4 +-- functions/simple-email/package.json | 4 +-- functions/simple-email/src/index.ts | 4 +-- jobs/job-utils/src/index.ts | 6 ++-- jobs/job-utils/src/runtime.ts | 22 ++------------ package.json | 6 +++- packages/env/src/env.ts | 2 +- packages/env/src/index.ts | 2 +- pnpm-lock.yaml | 42 ++++++++------------------ 10 files changed, 30 insertions(+), 64 deletions(-) diff --git a/functions/send-email-link/package.json b/functions/send-email-link/package.json index ed45a432f..2691c70bf 100644 --- a/functions/send-email-link/package.json +++ b/functions/send-email-link/package.json @@ -15,10 +15,10 @@ }, "dependencies": { "@launchql/knative-job-fn": "workspace:^", - "@launchql/job-utils": "workspace:^", "@launchql/mjml": "0.1.1", "@launchql/postmaster": "0.1.4", "@launchql/styled-email": "0.1.0", + "@pgpmjs/env": "workspace:^", "graphql-request": "^7.1.2", "graphql-tag": "^2.12.6" } diff --git a/functions/send-email-link/src/index.ts b/functions/send-email-link/src/index.ts index 49de9df67..6c635c39d 100644 --- a/functions/send-email-link/src/index.ts +++ b/functions/send-email-link/src/index.ts @@ -3,9 +3,9 @@ import { GraphQLClient } from 'graphql-request'; import gql from 'graphql-tag'; import { generate } from '@launchql/mjml'; import { send } from '@launchql/postmaster'; -import { parseEnvBoolean } from '@launchql/job-utils'; +import { parseEnvBoolean } from '@pgpmjs/env'; -const isDryRun = parseEnvBoolean('SEND_EMAIL_LINK_DRY_RUN', false); +const isDryRun = parseEnvBoolean(process.env.SEND_EMAIL_LINK_DRY_RUN) ?? false; const GetUser = gql` query GetUser($userId: UUID!) { diff --git a/functions/simple-email/package.json b/functions/simple-email/package.json index 407258beb..339f9f32e 100644 --- a/functions/simple-email/package.json +++ b/functions/simple-email/package.json @@ -15,7 +15,7 @@ }, "dependencies": { "@launchql/knative-job-fn": "workspace:^", - "@launchql/job-utils": "workspace:^", - "@launchql/postmaster": "0.1.4" + "@launchql/postmaster": "0.1.4", + "@pgpmjs/env": "workspace:^" } } diff --git a/functions/simple-email/src/index.ts b/functions/simple-email/src/index.ts index 533982bf6..70a501ece 100644 --- a/functions/simple-email/src/index.ts +++ b/functions/simple-email/src/index.ts @@ -1,5 +1,5 @@ import app from '@launchql/knative-job-fn'; -import { parseEnvBoolean } from '@launchql/job-utils'; +import { parseEnvBoolean } from '@pgpmjs/env'; import { send as sendEmail } from '@launchql/postmaster'; type SimpleEmailPayload = { @@ -25,7 +25,7 @@ const getRequiredField = ( return value; }; -const isDryRun = parseEnvBoolean('SIMPLE_EMAIL_DRY_RUN', false); +const isDryRun = parseEnvBoolean(process.env.SIMPLE_EMAIL_DRY_RUN) ?? false; app.post('*', async (req: any, res: any, next: any) => { try { diff --git a/jobs/job-utils/src/index.ts b/jobs/job-utils/src/index.ts index e9c158f3d..e86762c4e 100644 --- a/jobs/job-utils/src/index.ts +++ b/jobs/job-utils/src/index.ts @@ -19,8 +19,7 @@ import { getJobGatewayConfig, getJobGatewayDevMap, getJobsCallbackPort, - getCallbackBaseUrl, - parseEnvBoolean + getCallbackBaseUrl } from './runtime'; import { Logger } from '@pgpmjs/logger'; @@ -42,8 +41,7 @@ export { getJobGatewayConfig, getJobGatewayDevMap, getJobsCallbackPort, - getCallbackBaseUrl, - parseEnvBoolean + getCallbackBaseUrl }; const JOBS_SCHEMA = getJobSchema(); diff --git a/jobs/job-utils/src/runtime.ts b/jobs/job-utils/src/runtime.ts index fa5835fbe..48090c149 100644 --- a/jobs/job-utils/src/runtime.ts +++ b/jobs/job-utils/src/runtime.ts @@ -1,4 +1,4 @@ -import { getEnvOptions, getNodeEnv } from '@pgpmjs/env'; +import { getEnvOptions, getNodeEnv, parseEnvBoolean } from '@pgpmjs/env'; import { defaultPgConfig, getPgEnvVars, type PgConfig } from 'pg-env'; import { getPgPool } from 'pg-cache'; import type { Pool } from 'pg'; @@ -6,14 +6,6 @@ import type { PgpmOptions } from '@pgpmjs/types'; type Maybe = T | null | undefined; -const toBool = (v: Maybe): boolean | undefined => { - if (v == null) return undefined; - const s = String(v).toLowerCase(); - if (['true', '1', 'yes', 'y'].includes(s)) return true; - if (['false', '0', 'no', 'n'].includes(s)) return false; - return undefined; -}; - const toStrArray = (v: Maybe): string[] | undefined => v ? v.split(',').map(s => s.trim()).filter(Boolean) : undefined; @@ -44,7 +36,7 @@ export const getJobSchema = (): string => { // ---- SupportAny / Supported ---- export const getJobSupportAny = (): boolean => { const opts: PgpmOptions = getEnvOptions(); - const envVal = toBool(process.env.JOBS_SUPPORT_ANY); + const envVal = parseEnvBoolean(process.env.JOBS_SUPPORT_ANY); if (typeof envVal === 'boolean') return envVal; const worker: boolean | undefined = opts.jobs?.worker?.supportAny; @@ -139,13 +131,3 @@ export const getCallbackBaseUrl = (): string => { const port = getJobsCallbackPort(); return `http://${host}:${port}/callback`; }; - -// ---- Generic env helpers ---- -export const parseEnvBoolean = ( - name: string, - defaultValue = false -): boolean => { - const parsed = toBool(process.env[name]); - if (typeof parsed === 'boolean') return parsed; - return defaultValue; -}; diff --git a/package.json b/package.json index 666d1423c..82b9e500b 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,10 @@ "@jest/test-sequencer": "^29.7.0" } } - } + }, + "onlyBuiltDependencies": [ + "@launchql/protobufjs", + "core-js-pure" + ] } } diff --git a/packages/env/src/env.ts b/packages/env/src/env.ts index 46eaf565e..2d2ba94f4 100644 --- a/packages/env/src/env.ts +++ b/packages/env/src/env.ts @@ -5,7 +5,7 @@ const parseEnvNumber = (val?: string): number | undefined => { return !isNaN(num) ? num : undefined; }; -const parseEnvBoolean = (val?: string): boolean | undefined => { +export const parseEnvBoolean = (val?: string): boolean | undefined => { if (val === undefined) return undefined; return ['true', '1', 'yes'].includes(val.toLowerCase()); }; diff --git a/packages/env/src/index.ts b/packages/env/src/index.ts index 327b05730..8fc394671 100644 --- a/packages/env/src/index.ts +++ b/packages/env/src/index.ts @@ -1,6 +1,6 @@ export { getEnvOptions, getConnEnvOptions, getDeploymentEnvOptions } from './merge'; export { loadConfigSync, loadConfigSyncFromDir, loadConfigFileSync, resolvePgpmPath } from './config'; -export { getEnvVars, getNodeEnv } from './env'; +export { getEnvVars, getNodeEnv, parseEnvBoolean } from './env'; export { walkUp } from './utils'; export type { PgpmOptions, PgTestConnectionOptions, DeploymentOptions } from '@pgpmjs/types'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e3f860176..3aa6e3e6d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -70,9 +70,6 @@ importers: functions/send-email-link: dependencies: - '@launchql/job-utils': - specifier: workspace:^ - version: link:../../jobs/job-utils '@launchql/knative-job-fn': specifier: workspace:^ version: link:../../jobs/knative-job-fn @@ -85,6 +82,9 @@ importers: '@launchql/styled-email': specifier: 0.1.0 version: 0.1.0(@babel/core@7.28.5)(encoding@0.1.13)(react-dom@19.2.0(react@19.2.0))(react-is@18.3.1)(react@19.2.0) + '@pgpmjs/env': + specifier: workspace:^ + version: link:../../packages/env/dist graphql-request: specifier: ^7.1.2 version: 7.3.4(graphql@15.10.1) @@ -94,15 +94,15 @@ importers: functions/simple-email: dependencies: - '@launchql/job-utils': - specifier: workspace:^ - version: link:../../jobs/job-utils '@launchql/knative-job-fn': specifier: workspace:^ version: link:../../jobs/knative-job-fn '@launchql/postmaster': specifier: 0.1.4 version: 0.1.4 + '@pgpmjs/env': + specifier: workspace:^ + version: link:../../packages/env/dist graphile/graphile-cache: dependencies: @@ -133,7 +133,7 @@ importers: version: 3.1.11 ts-node: specifier: ^10.9.2 - version: 10.9.2(@types/node@25.0.1)(typescript@5.9.3) + version: 10.9.2(@types/node@20.19.25)(typescript@5.9.3) publishDirectory: dist graphile/graphile-i18n: @@ -560,7 +560,7 @@ importers: version: 3.1.11 ts-node: specifier: ^10.9.2 - version: 10.9.2(@types/node@25.0.1)(typescript@5.9.3) + version: 10.9.2(@types/node@20.19.25)(typescript@5.9.3) publishDirectory: dist graphile/graphile-simple-inflector: @@ -775,7 +775,7 @@ importers: devDependencies: ts-node: specifier: ^10.9.2 - version: 10.9.2(@types/node@25.0.1)(typescript@5.9.3) + version: 10.9.2(@types/node@20.19.25)(typescript@5.9.3) jobs/knative-job-worker: dependencies: @@ -1098,7 +1098,7 @@ importers: version: 3.1.11 ts-node: specifier: ^10.9.2 - version: 10.9.2(@types/node@25.0.1)(typescript@5.9.3) + version: 10.9.2(@types/node@20.19.25)(typescript@5.9.3) publishDirectory: dist packages/gql-ast: @@ -1781,7 +1781,7 @@ importers: version: 3.1.11 ts-node: specifier: ^10.9.2 - version: 10.9.2(@types/node@25.0.1)(typescript@5.9.3) + version: 10.9.2(@types/node@20.19.25)(typescript@5.9.3) publishDirectory: dist packages/server-utils: @@ -1813,7 +1813,7 @@ importers: version: 0.1.8 ts-node: specifier: ^10.9.2 - version: 10.9.2(@types/node@25.0.1)(typescript@5.9.3) + version: 10.9.2(@types/node@20.19.25)(typescript@5.9.3) publishDirectory: dist packages/stream-to-etag: @@ -16162,24 +16162,6 @@ snapshots: v8-compile-cache-lib: 3.0.1 yn: 3.1.1 - ts-node@10.9.2(@types/node@25.0.1)(typescript@5.9.3): - dependencies: - '@cspotcode/source-map-support': 0.8.1 - '@tsconfig/node10': 1.0.12 - '@tsconfig/node12': 1.0.11 - '@tsconfig/node14': 1.0.3 - '@tsconfig/node16': 1.0.4 - '@types/node': 25.0.1 - acorn: 8.15.0 - acorn-walk: 8.3.4 - arg: 4.1.3 - create-require: 1.1.1 - diff: 4.0.2 - make-error: 1.3.6 - typescript: 5.9.3 - v8-compile-cache-lib: 3.0.1 - yn: 3.1.1 - tsconfig-paths@4.2.0: dependencies: json5: 2.2.3