From 9f30c3c9367035c53616d7b5e5cce23f440638d4 Mon Sep 17 00:00:00 2001 From: zetazzz Date: Mon, 13 Jul 2026 13:14:47 +0800 Subject: [PATCH] refactor: align env configuration ownership --- graphql/env/README.md | 75 ++++++- .../__snapshots__/merge.test.ts.snap | 15 +- graphql/env/__tests__/environment.test.ts | 118 ++++++++++ graphql/env/__tests__/merge.test.ts | 118 +++++++++- graphql/env/src/env.ts | 154 ++++++++++++- graphql/env/src/index.ts | 2 +- graphql/env/src/merge.ts | 61 ++++-- graphql/explorer/src/resolvers/uploads.ts | 2 +- graphql/server/package.json | 1 - graphql/server/src/middleware/api.ts | 2 +- graphql/server/src/middleware/auth.ts | 6 +- .../server/src/middleware/error-handler.ts | 2 +- graphql/server/src/middleware/graphile.ts | 2 +- graphql/types/README.md | 34 ++- graphql/types/src/constructive.ts | 35 ++- graphql/types/src/environment.ts | 179 +++++++++++++++ graphql/types/src/index.ts | 3 + jobs/job-utils/package.json | 3 +- jobs/job-utils/src/index.ts | 2 +- jobs/job-utils/src/runtime.ts | 20 +- .../smtppostmaster/__tests__/test-send.ts | 4 +- packages/smtppostmaster/package.json | 4 +- packages/smtppostmaster/src/index.ts | 4 +- pgpm/env/README.md | 19 +- .../__snapshots__/merge.test.ts.snap | 49 ----- pgpm/env/__tests__/merge.test.ts | 83 ++++--- pgpm/env/src/env.ts | 134 +----------- pgpm/env/src/index.ts | 9 +- pgpm/env/src/merge.ts | 48 +++- pgpm/types/src/index.ts | 1 - pgpm/types/src/jobs.ts | 206 ------------------ pgpm/types/src/pgpm.ts | 132 +---------- pnpm-lock.yaml | 30 +-- uploads/s3-streamer/__tests__/uploads.test.ts | 2 +- uploads/s3-streamer/package.json | 6 +- uploads/s3-streamer/src/s3.ts | 2 +- uploads/s3-streamer/src/streamer.ts | 2 +- 37 files changed, 906 insertions(+), 663 deletions(-) create mode 100644 graphql/env/__tests__/environment.test.ts create mode 100644 graphql/types/src/environment.ts delete mode 100644 pgpm/types/src/jobs.ts diff --git a/graphql/env/README.md b/graphql/env/README.md index e3e1f85dd5..e4f4139e40 100644 --- a/graphql/env/README.md +++ b/graphql/env/README.md @@ -12,9 +12,9 @@

-Constructive environment configuration with GraphQL/Graphile support. +Upper-level environment configuration for Constructive applications. -This package extends `@pgpmjs/env` with GraphQL-specific environment variable parsing and defaults for Constructive applications. +This package calls the lower-level `@pgpmjs/env` resolver and combines its PostgreSQL/PGPM result with Constructive defaults, Constructive config-file sections, GraphQL environment variables, and runtime overrides. Callers receive one complete `ConstructiveOptions` result and do not need to combine the two resolvers manually. ## Installation @@ -25,31 +25,80 @@ npm install @constructive-io/graphql-env ## Usage ```typescript -import { getEnvOptions } from '@constructive-io/graphql-env'; +import { getConstructiveEnvOptions } from '@constructive-io/graphql-env'; -// Get merged options (core PGPM + GraphQL defaults + env vars + config) -const options = getEnvOptions(); +// PostgreSQL/PGPM configuration and Constructive runtime configuration +const options = getConstructiveEnvOptions(); // With overrides -const options = getEnvOptions({ +const overriddenOptions = getConstructiveEnvOptions({ + server: { port: 4000 }, graphile: { schema: ['public', 'app'] }, - features: { simpleInflection: true } + features: { simpleInflection: true }, }); ``` +`getEnvOptions` is an exact alias of `getConstructiveEnvOptions`. + +## Resolution order + +Configuration is merged from lowest to highest priority: + +```text +PGPM and Constructive defaults +→ PGPM config and environment values +→ Constructive config values +→ Constructive environment values +→ runtime overrides +``` + ## Environment Variables -In addition to all environment variables supported by `@pgpmjs/env`, this package parses: +In addition to the PostgreSQL/PGPM values supplied by `@pgpmjs/env`, this package parses the Constructive-owned variables below. + +### Server + +- `PORT` +- `SERVER_HOST` +- `SERVER_TRUST_PROXY` +- `SERVER_ORIGIN` +- `SERVER_STRICT_AUTH` + +### CDN and storage + +- `BUCKET_PROVIDER` +- `BUCKET_NAME` +- `AWS_REGION` +- `AWS_ACCESS_KEY` / `AWS_ACCESS_KEY_ID` +- `AWS_SECRET_KEY` / `AWS_SECRET_ACCESS_KEY` +- `CDN_ENDPOINT` +- `CDN_PUBLIC_URL_PREFIX` + +### Jobs + +- `JOBS_SCHEMA` +- `JOBS_SUPPORT_ANY` +- `JOBS_SUPPORTED` +- `INTERNAL_GATEWAY_URL` +- `INTERNAL_JOBS_CALLBACK_URL` +- `INTERNAL_JOBS_CALLBACK_PORT` + +### SMTP + +This package owns the existing `SMTP_*` configuration previously parsed by `@pgpmjs/env`. ### GraphQL Schema + - `GRAPHILE_SCHEMA` - Comma-separated list of PostgreSQL schemas to expose ### Feature Flags + - `FEATURES_SIMPLE_INFLECTION` - Enable simple inflection plugin - `FEATURES_OPPOSITE_BASE_NAMES` - Enable opposite base names - `FEATURES_POSTGIS` - Enable PostGIS support ### API Configuration + - `API_ENABLE_SERVICES` - Enable services API (domain/subdomain routing) - `API_IS_PUBLIC` - Whether API is public - `API_EXPOSED_SCHEMAS` - Comma-separated list of exposed schemas @@ -60,7 +109,7 @@ In addition to all environment variables supported by `@pgpmjs/env`, this packag ## Defaults -GraphQL defaults are provided by `@constructive-io/graphql-types`: +Constructive defaults are provided by `@constructive-io/graphql-types`. They include PGPM defaults, the existing GraphQL defaults, and the server/CDN/jobs/SMTP defaults moved in this refactor. ```typescript { @@ -84,5 +133,9 @@ GraphQL defaults are provided by `@constructive-io/graphql-types`: ## When to Use -- Use `@constructive-io/graphql-env` for Constructive applications that need GraphQL/Graphile configuration -- Use `@pgpmjs/env` for pure PGPM tooling that doesn't need GraphQL support +- Use `@constructive-io/graphql-env` for GraphQL servers and other Constructive applications that need the complete aggregate. +- Use `@pgpmjs/env` for pure PostgreSQL/PGPM tooling. + +## Follow-up boundary + +This refactor moves only server, CDN/storage, jobs, SMTP, and `getNodeEnv()` ownership out of PGPM. It does not consolidate environment variables currently managed by Mailgun providers, individual functions, Knative runtimes, LLM integrations, observability, CAPTCHA, Graphile runtime helpers, or test harnesses. Existing behavior in those areas remains unchanged and can be addressed separately. diff --git a/graphql/env/__tests__/__snapshots__/merge.test.ts.snap b/graphql/env/__tests__/__snapshots__/merge.test.ts.snap index b6969aaf51..d3b334c426 100644 --- a/graphql/env/__tests__/__snapshots__/merge.test.ts.snap +++ b/graphql/env/__tests__/__snapshots__/merge.test.ts.snap @@ -21,8 +21,8 @@ exports[`getEnvOptions merges pgpm defaults, graphql defaults, config, env, and "awsAccessKey": "minioadmin", "awsRegion": "us-east-1", "awsSecretKey": "minioadmin", - "bucketName": "test-bucket", - "endpoint": "http://localhost:9000", + "bucketName": "override-bucket", + "endpoint": "http://config-storage:9000", "provider": "minio", "publicUrlPrefix": "http://localhost:9000", }, @@ -84,11 +84,11 @@ exports[`getEnvOptions merges pgpm defaults, graphql defaults, config, env, and "supported": [], }, "schema": { - "schema": "app_jobs", + "schema": "env_jobs", }, "worker": { "gracefulShutdown": true, - "hostname": "worker-0", + "hostname": "config-worker", "pollInterval": 1000, "schema": "app_jobs", "supportAny": true, @@ -108,16 +108,19 @@ exports[`getEnvOptions merges pgpm defaults, graphql defaults, config, env, and "user": "env-user", }, "server": { - "host": "localhost", + "host": "env-server", + "origin": "https://config-origin.test", "port": 5000, "strictAuth": false, "trustProxy": false, }, "smtp": { "debug": false, + "from": "config@example.test", + "host": "env-smtp", "logger": false, "pool": false, - "port": 587, + "port": 2525, "secure": false, }, } diff --git a/graphql/env/__tests__/environment.test.ts b/graphql/env/__tests__/environment.test.ts new file mode 100644 index 0000000000..939542570c --- /dev/null +++ b/graphql/env/__tests__/environment.test.ts @@ -0,0 +1,118 @@ +import { getGraphQLEnvVars, getNodeEnv } from '../src/env'; + +describe('moved Constructive environment variables', () => { + it('parses server, CDN, jobs, and SMTP with the existing semantics', () => { + const result = getGraphQLEnvVars({ + PORT: '4321', + SERVER_HOST: '0.0.0.0', + SERVER_TRUST_PROXY: 'true', + SERVER_ORIGIN: 'https://example.test', + SERVER_STRICT_AUTH: 'false', + BUCKET_PROVIDER: 'gcs', + BUCKET_NAME: 'assets', + AWS_REGION: 'eu-west-1', + AWS_ACCESS_KEY: 'primary-access', + AWS_ACCESS_KEY_ID: 'alias-access', + AWS_SECRET_KEY: 'primary-secret', + AWS_SECRET_ACCESS_KEY: 'alias-secret', + CDN_ENDPOINT: 'https://storage.example.test', + CDN_PUBLIC_URL_PREFIX: 'https://cdn.example.test', + JOBS_SCHEMA: 'custom_jobs', + JOBS_SUPPORT_ANY: 'false', + JOBS_SUPPORTED: 'alpha, beta', + INTERNAL_GATEWAY_URL: 'http://gateway.internal:8080', + INTERNAL_JOBS_CALLBACK_URL: 'http://callback.internal:12345', + INTERNAL_JOBS_CALLBACK_PORT: '23456', + SMTP_HOST: 'smtp.example.test', + SMTP_PORT: '465', + SMTP_SECURE: 'true', + SMTP_USER: 'mailer', + SMTP_PASS: 'password', + SMTP_FROM: 'from@example.test', + SMTP_REPLY_TO: 'reply@example.test', + SMTP_REQUIRE_TLS: 'true', + SMTP_TLS_REJECT_UNAUTHORIZED: 'false', + SMTP_POOL: 'true', + SMTP_MAX_CONNECTIONS: '4', + SMTP_MAX_MESSAGES: '50', + SMTP_NAME: 'mailer.example.test', + SMTP_LOGGER: 'true', + SMTP_DEBUG: 'false', + }); + + expect(result.server).toEqual({ + port: 4321, + host: '0.0.0.0', + trustProxy: true, + origin: 'https://example.test', + strictAuth: false, + }); + expect(result.cdn).toEqual({ + provider: 'gcs', + bucketName: 'assets', + awsRegion: 'eu-west-1', + awsAccessKey: 'primary-access', + awsSecretKey: 'primary-secret', + endpoint: 'https://storage.example.test', + publicUrlPrefix: 'https://cdn.example.test', + }); + expect(result.jobs).toEqual({ + schema: { schema: 'custom_jobs' }, + worker: { supportAny: false, supported: ['alpha', 'beta'] }, + scheduler: { supportAny: false, supported: ['alpha', 'beta'] }, + gateway: { + gatewayUrl: 'http://gateway.internal:8080', + callbackUrl: 'http://callback.internal:12345', + callbackPort: 23456, + }, + }); + expect(result.smtp).toEqual({ + host: 'smtp.example.test', + port: 465, + secure: true, + user: 'mailer', + pass: 'password', + from: 'from@example.test', + replyTo: 'reply@example.test', + requireTLS: true, + tlsRejectUnauthorized: false, + pool: true, + maxConnections: 4, + maxMessages: 50, + name: 'mailer.example.test', + logger: true, + debug: false, + }); + }); + + it('keeps the existing AWS alias fallback order', () => { + expect( + getGraphQLEnvVars({ + AWS_ACCESS_KEY_ID: 'alias-access', + AWS_SECRET_ACCESS_KEY: 'alias-secret', + }).cdn + ).toEqual({ + awsAccessKey: 'alias-access', + awsSecretKey: 'alias-secret', + }); + }); +}); + +describe('getNodeEnv', () => { + const originalNodeEnv = process.env.NODE_ENV; + + afterEach(() => { + if (originalNodeEnv === undefined) delete process.env.NODE_ENV; + else process.env.NODE_ENV = originalNodeEnv; + }); + + it.each([ + ['production', 'production'], + ['test', 'test'], + ['development', 'development'], + ['other', 'development'], + ])('maps %s to %s', (value, expected) => { + process.env.NODE_ENV = value; + expect(getNodeEnv()).toBe(expected); + }); +}); diff --git a/graphql/env/__tests__/merge.test.ts b/graphql/env/__tests__/merge.test.ts index edbc3f6df6..2dfd38f209 100644 --- a/graphql/env/__tests__/merge.test.ts +++ b/graphql/env/__tests__/merge.test.ts @@ -1,4 +1,4 @@ -import { getEnvOptions } from '../src/merge'; +import { getConstructiveEnvOptions, getEnvOptions } from '../src/merge'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; @@ -17,6 +17,10 @@ describe('getEnvOptions', () => { } }); + it('is the exact alias of getConstructiveEnvOptions', () => { + expect(getEnvOptions).toBe(getConstructiveEnvOptions); + }); + it('merges pgpm defaults, graphql defaults, config, env, and overrides', () => { tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'graphql-env-')); writeConfig(tempDir, { @@ -25,7 +29,21 @@ describe('getEnvOptions', () => { database: 'config-db' }, server: { - port: 4000 + host: 'config-server', + port: 4000, + origin: 'https://config-origin.test' + }, + cdn: { + bucketName: 'config-bucket', + endpoint: 'http://config-storage:9000' + }, + jobs: { + schema: { schema: 'config_jobs' }, + worker: { hostname: 'config-worker' } + }, + smtp: { + host: 'config-smtp', + from: 'config@example.test' }, graphile: { schema: ['config_schema'] @@ -43,6 +61,11 @@ describe('getEnvOptions', () => { const testEnv: NodeJS.ProcessEnv = { PGHOST: 'env-host', PGUSER: 'env-user', + PORT: '4500', + SERVER_HOST: 'env-server', + BUCKET_NAME: 'env-bucket', + JOBS_SCHEMA: 'env_jobs', + SMTP_HOST: 'env-smtp', GRAPHILE_SCHEMA: 'env_schema_a,env_schema_b', FEATURES_SIMPLE_INFLECTION: 'true', FEATURES_POSTGIS: 'false', @@ -66,6 +89,12 @@ describe('getEnvOptions', () => { server: { port: 5000 }, + cdn: { + bucketName: 'override-bucket' + }, + smtp: { + port: 2525 + }, graphile: { schema: ['override_schema'] }, @@ -82,6 +111,24 @@ describe('getEnvOptions', () => { ); expect(result).toMatchSnapshot(); + expect(result.server).toMatchObject({ + host: 'env-server', + port: 5000, + origin: 'https://config-origin.test' + }); + expect(result.cdn).toMatchObject({ + bucketName: 'override-bucket', + endpoint: 'http://config-storage:9000' + }); + expect(result.jobs).toMatchObject({ + schema: { schema: 'env_jobs' }, + worker: { hostname: 'config-worker' } + }); + expect(result.smtp).toMatchObject({ + host: 'env-smtp', + port: 2525, + from: 'config@example.test' + }); }); it('replaces graphql array fields with later values (overrides win)', () => { @@ -93,13 +140,18 @@ describe('getEnvOptions', () => { api: { exposedSchemas: ['public', 'shared'], metaSchemas: ['metaschema_public', 'services_public', 'config_meta'] + }, + jobs: { + worker: { supported: ['config-worker', 'shared'] }, + scheduler: { supported: ['config-scheduler', 'shared'] } } }); const testEnv: NodeJS.ProcessEnv = { GRAPHILE_SCHEMA: 'shared_schema,env_schema', API_EXPOSED_SCHEMAS: 'shared,env_schema', - API_META_SCHEMAS: 'services_public,env_meta' + API_META_SCHEMAS: 'services_public,env_meta', + JOBS_SUPPORTED: 'shared,env-job' }; const result = getEnvOptions( @@ -110,6 +162,10 @@ describe('getEnvOptions', () => { api: { exposedSchemas: ['public', 'override_schema'], metaSchemas: ['env_meta', 'override_meta'] + }, + jobs: { + worker: { supported: ['override-worker', 'shared'] }, + scheduler: { supported: ['override-scheduler', 'shared'] } } }, tempDir, @@ -120,5 +176,61 @@ describe('getEnvOptions', () => { expect(result.graphile?.schema).toEqual(['override_schema', 'shared_schema']); expect(result.api?.exposedSchemas).toEqual(['public', 'override_schema']); expect(result.api?.metaSchemas).toEqual(['env_meta', 'override_meta']); + expect(result.jobs?.worker?.supported).toEqual([ + 'override-worker', + 'shared' + ]); + expect(result.jobs?.scheduler?.supported).toEqual([ + 'override-scheduler', + 'shared' + ]); + }); + + it('preserves the moved Constructive defaults', () => { + tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'graphql-env-defaults-')); + + const result = getConstructiveEnvOptions({}, tempDir, {}); + + expect(result.server).toEqual({ + host: 'localhost', + port: 3000, + trustProxy: false, + strictAuth: false + }); + expect(result.cdn).toEqual({ + provider: 'minio', + bucketName: 'test-bucket', + awsRegion: 'us-east-1', + awsAccessKey: 'minioadmin', + awsSecretKey: 'minioadmin', + endpoint: 'http://localhost:9000', + publicUrlPrefix: 'http://localhost:9000' + }); + expect(result.jobs).toEqual({ + schema: { schema: 'app_jobs' }, + worker: { + schema: 'app_jobs', + hostname: 'worker-0', + supportAny: true, + supported: [], + pollInterval: 1000, + gracefulShutdown: true + }, + scheduler: { + schema: 'app_jobs', + hostname: 'scheduler-0', + supportAny: true, + supported: [], + pollInterval: 1000, + gracefulShutdown: true + } + }); + expect(result.smtp).toEqual({ + port: 587, + secure: false, + pool: false, + logger: false, + debug: false + }); }); }); diff --git a/graphql/env/src/env.ts b/graphql/env/src/env.ts index fdf8d62c29..c903d26328 100644 --- a/graphql/env/src/env.ts +++ b/graphql/env/src/env.ts @@ -1,4 +1,7 @@ -import { ConstructiveOptions } from '@constructive-io/graphql-types'; +import { + BucketProvider, + ConstructiveOptions +} from '@constructive-io/graphql-types'; /** * Parse GraphQL-related environment variables. @@ -9,6 +12,27 @@ const parseEnvBoolean = (val?: string): boolean | undefined => { return ['true', '1', 'yes'].includes(val.toLowerCase()); }; +const parseEnvNumber = (val?: string): number | undefined => { + const num = Number(val); + return !isNaN(num) ? num : undefined; +}; + +const parseEnvStringArray = (val?: string): string[] | undefined => { + if (!val) return undefined; + return val + .split(',') + .map(s => s.trim()) + .filter(Boolean); +}; + +type NodeEnv = 'development' | 'production' | 'test'; + +export const getNodeEnv = (): NodeEnv => { + const env = process.env.NODE_ENV?.toLowerCase(); + if (env === 'production' || env === 'test') return env; + return 'development'; +}; + /** * @param env - Environment object to read from (defaults to process.env for backwards compatibility) */ @@ -28,6 +52,45 @@ export const getGraphQLEnvVars = (env: NodeJS.ProcessEnv = process.env): Partial API_ROLE_NAME, API_DEFAULT_DATABASE_ID, + PORT, + SERVER_HOST, + SERVER_TRUST_PROXY, + SERVER_ORIGIN, + SERVER_STRICT_AUTH, + + BUCKET_PROVIDER, + BUCKET_NAME, + AWS_REGION, + AWS_ACCESS_KEY, + AWS_ACCESS_KEY_ID, + AWS_SECRET_KEY, + AWS_SECRET_ACCESS_KEY, + CDN_ENDPOINT, + CDN_PUBLIC_URL_PREFIX, + + JOBS_SCHEMA, + JOBS_SUPPORT_ANY, + JOBS_SUPPORTED, + INTERNAL_GATEWAY_URL, + INTERNAL_JOBS_CALLBACK_URL, + INTERNAL_JOBS_CALLBACK_PORT, + + SMTP_HOST, + SMTP_PORT, + SMTP_SECURE, + SMTP_USER, + SMTP_PASS, + SMTP_FROM, + SMTP_REPLY_TO, + SMTP_REQUIRE_TLS, + SMTP_TLS_REJECT_UNAUTHORIZED, + SMTP_POOL, + SMTP_MAX_CONNECTIONS, + SMTP_MAX_MESSAGES, + SMTP_NAME, + SMTP_LOGGER, + SMTP_DEBUG, + EMBEDDER_PROVIDER, EMBEDDER_MODEL, EMBEDDER_BASE_URL, @@ -58,6 +121,95 @@ export const getGraphQLEnvVars = (env: NodeJS.ProcessEnv = process.env): Partial ...(API_ROLE_NAME && { roleName: API_ROLE_NAME }), ...(API_DEFAULT_DATABASE_ID && { defaultDatabaseId: API_DEFAULT_DATABASE_ID }), }, + server: { + ...(PORT && { port: parseEnvNumber(PORT) }), + ...(SERVER_HOST && { host: SERVER_HOST }), + ...(SERVER_TRUST_PROXY && { + trustProxy: parseEnvBoolean(SERVER_TRUST_PROXY) + }), + ...(SERVER_ORIGIN && { origin: SERVER_ORIGIN }), + ...(SERVER_STRICT_AUTH && { + strictAuth: parseEnvBoolean(SERVER_STRICT_AUTH) + }) + }, + cdn: { + ...(BUCKET_PROVIDER && { provider: BUCKET_PROVIDER as BucketProvider }), + ...(BUCKET_NAME && { bucketName: BUCKET_NAME }), + ...(AWS_REGION && { awsRegion: AWS_REGION }), + ...((AWS_ACCESS_KEY || AWS_ACCESS_KEY_ID) && { + awsAccessKey: AWS_ACCESS_KEY || AWS_ACCESS_KEY_ID + }), + ...((AWS_SECRET_KEY || AWS_SECRET_ACCESS_KEY) && { + awsSecretKey: AWS_SECRET_KEY || AWS_SECRET_ACCESS_KEY + }), + ...(CDN_ENDPOINT && { endpoint: CDN_ENDPOINT }), + ...(CDN_PUBLIC_URL_PREFIX && { publicUrlPrefix: CDN_PUBLIC_URL_PREFIX }) + }, + jobs: { + ...(JOBS_SCHEMA && { + schema: { + schema: JOBS_SCHEMA + } + }), + ...((JOBS_SUPPORT_ANY || JOBS_SUPPORTED) && { + worker: { + ...(JOBS_SUPPORT_ANY && { + supportAny: parseEnvBoolean(JOBS_SUPPORT_ANY) + }), + ...(JOBS_SUPPORTED && { + supported: parseEnvStringArray(JOBS_SUPPORTED) + }) + }, + scheduler: { + ...(JOBS_SUPPORT_ANY && { + supportAny: parseEnvBoolean(JOBS_SUPPORT_ANY) + }), + ...(JOBS_SUPPORTED && { + supported: parseEnvStringArray(JOBS_SUPPORTED) + }) + } + }), + ...((INTERNAL_GATEWAY_URL || + INTERNAL_JOBS_CALLBACK_URL || + INTERNAL_JOBS_CALLBACK_PORT) && { + gateway: { + ...(INTERNAL_GATEWAY_URL && { + gatewayUrl: INTERNAL_GATEWAY_URL + }), + ...(INTERNAL_JOBS_CALLBACK_URL && { + callbackUrl: INTERNAL_JOBS_CALLBACK_URL + }), + ...(INTERNAL_JOBS_CALLBACK_PORT && { + callbackPort: parseEnvNumber(INTERNAL_JOBS_CALLBACK_PORT) + }) + } + }) + }, + smtp: { + ...(SMTP_HOST && { host: SMTP_HOST }), + ...(SMTP_PORT && { port: parseEnvNumber(SMTP_PORT) }), + ...(SMTP_SECURE && { secure: parseEnvBoolean(SMTP_SECURE) }), + ...(SMTP_USER && { user: SMTP_USER }), + ...(SMTP_PASS && { pass: SMTP_PASS }), + ...(SMTP_FROM && { from: SMTP_FROM }), + ...(SMTP_REPLY_TO && { replyTo: SMTP_REPLY_TO }), + ...(SMTP_REQUIRE_TLS && { + requireTLS: parseEnvBoolean(SMTP_REQUIRE_TLS) + }), + ...(SMTP_TLS_REJECT_UNAUTHORIZED && { + tlsRejectUnauthorized: parseEnvBoolean(SMTP_TLS_REJECT_UNAUTHORIZED) + }), + ...(SMTP_POOL && { pool: parseEnvBoolean(SMTP_POOL) }), + ...(SMTP_MAX_CONNECTIONS && { + maxConnections: parseEnvNumber(SMTP_MAX_CONNECTIONS) + }), + ...(SMTP_MAX_MESSAGES && { + maxMessages: parseEnvNumber(SMTP_MAX_MESSAGES) + }), + ...(SMTP_NAME && { name: SMTP_NAME }), + ...(SMTP_LOGGER && { logger: parseEnvBoolean(SMTP_LOGGER) }), + ...(SMTP_DEBUG && { debug: parseEnvBoolean(SMTP_DEBUG) }) + }, ...((EMBEDDER_PROVIDER || CHAT_PROVIDER) && { llm: { ...((EMBEDDER_PROVIDER || EMBEDDER_MODEL || EMBEDDER_BASE_URL) && { diff --git a/graphql/env/src/index.ts b/graphql/env/src/index.ts index cfe0e18f11..9863a7fcbf 100644 --- a/graphql/env/src/index.ts +++ b/graphql/env/src/index.ts @@ -1,3 +1,3 @@ // Export Constructive-specific env functions export { getEnvOptions, getConstructiveEnvOptions } from './merge'; -export { getGraphQLEnvVars } from './env'; +export { getGraphQLEnvVars, getNodeEnv } from './env'; diff --git a/graphql/env/src/merge.ts b/graphql/env/src/merge.ts index 669c75269f..005b657827 100644 --- a/graphql/env/src/merge.ts +++ b/graphql/env/src/merge.ts @@ -1,8 +1,38 @@ import deepmerge from 'deepmerge'; -import { ConstructiveOptions, constructiveGraphqlDefaults } from '@constructive-io/graphql-types'; -import { getEnvOptions as getPgpmEnvOptions, loadConfigSync, replaceArrays } from '@pgpmjs/env'; +import { + ConstructiveOptions, + constructiveDefaults +} from '@constructive-io/graphql-types'; +import { getPgpmEnvOptions, loadConfigSync, replaceArrays } from '@pgpmjs/env'; import { getGraphQLEnvVars } from './env'; +const CONSTRUCTIVE_OPTION_KEYS = [ + 'graphile', + 'features', + 'api', + 'server', + 'cdn', + 'jobs', + 'smtp' +] as const; + +const projectConstructiveOptions = ( + options: unknown +): Partial => { + if (!options || typeof options !== 'object') return {}; + + const source = options as Record; + const projected: Record = {}; + + for (const key of CONSTRUCTIVE_OPTION_KEYS) { + if (Object.prototype.hasOwnProperty.call(source, key)) { + projected[key] = source[key]; + } + } + + return projected as Partial; +}; + /** * Get Constructive environment options by merging: * 1. Core PGPM defaults (from @pgpmjs/env) @@ -18,7 +48,7 @@ import { getGraphQLEnvVars } from './env'; * @param cwd - Working directory for config file resolution * @param env - Environment object to read from (defaults to process.env for backwards compatibility) */ -export const getEnvOptions = ( +const resolveConstructiveEnvOptions = ( overrides: Partial = {}, cwd: string = process.cwd(), env: NodeJS.ProcessEnv = process.env @@ -26,24 +56,13 @@ export const getEnvOptions = ( // Get core PGPM options (includes pgpmDefaults + config + core env vars) const coreOptions = getPgpmEnvOptions({}, cwd, env); - // Get GraphQL-specific env vars + const configOptions = projectConstructiveOptions(loadConfigSync(cwd)); const graphqlEnvOptions = getGraphQLEnvVars(env); - - // Load config again to get any GraphQL-specific config - // Config files can contain Constructive options (graphile, features, api) - // even though loadConfigSync returns PgpmOptions type - const configOptions = loadConfigSync(cwd) as Partial; - - // Merge in order: core -> graphql defaults -> config (for graphql keys) -> graphql env -> overrides + return deepmerge.all([ + constructiveDefaults, coreOptions, - constructiveGraphqlDefaults, - // Only merge graphql-related keys from config (if present) - { - ...(configOptions.graphile && { graphile: configOptions.graphile }), - ...(configOptions.features && { features: configOptions.features }), - ...(configOptions.api && { api: configOptions.api }), - }, + configOptions, graphqlEnvOptions, overrides ], { @@ -51,7 +70,5 @@ export const getEnvOptions = ( }) as ConstructiveOptions; }; -/** - * Alias - same as getEnvOptions - */ -export const getConstructiveEnvOptions = getEnvOptions; +export const getConstructiveEnvOptions = resolveConstructiveEnvOptions; +export const getEnvOptions = getConstructiveEnvOptions; diff --git a/graphql/explorer/src/resolvers/uploads.ts b/graphql/explorer/src/resolvers/uploads.ts index 54dad0c9e1..ba2e3d78d2 100644 --- a/graphql/explorer/src/resolvers/uploads.ts +++ b/graphql/explorer/src/resolvers/uploads.ts @@ -1,8 +1,8 @@ import Streamer from '@constructive-io/s3-streamer'; +import type { BucketProvider } from '@constructive-io/graphql-types'; import uploadNames from '@constructive-io/upload-names'; import { ReadStream } from 'fs'; import type { GraphQLResolveInfo } from 'graphql'; -import type { BucketProvider } from '@pgpmjs/types'; interface UploaderOptions { bucketName: string; diff --git a/graphql/server/package.json b/graphql/server/package.json index f8ef384197..067ef83993 100644 --- a/graphql/server/package.json +++ b/graphql/server/package.json @@ -48,7 +48,6 @@ "@constructive-io/s3-utils": "workspace:^", "@constructive-io/url-domains": "workspace:^", "@graphile-contrib/pg-many-to-many": "2.0.0-rc.2", - "@pgpmjs/env": "workspace:^", "@pgpmjs/logger": "workspace:^", "@pgpmjs/server-utils": "workspace:^", "@pgpmjs/types": "workspace:^", diff --git a/graphql/server/src/middleware/api.ts b/graphql/server/src/middleware/api.ts index 005be581fd..486d855422 100644 --- a/graphql/server/src/middleware/api.ts +++ b/graphql/server/src/middleware/api.ts @@ -1,4 +1,4 @@ -import { getNodeEnv } from '@pgpmjs/env'; +import { getNodeEnv } from '@constructive-io/graphql-env'; import { Logger } from '@pgpmjs/logger'; import { svcCache } from '@pgpmjs/server-utils'; import { parseUrl } from '@constructive-io/url-domains'; diff --git a/graphql/server/src/middleware/auth.ts b/graphql/server/src/middleware/auth.ts index b86e765c89..aeb3d5ed2c 100644 --- a/graphql/server/src/middleware/auth.ts +++ b/graphql/server/src/middleware/auth.ts @@ -1,6 +1,6 @@ -import { getNodeEnv } from '@pgpmjs/env'; +import { getNodeEnv } from '@constructive-io/graphql-env'; +import type { ConstructiveOptions } from '@constructive-io/graphql-types'; import { Logger } from '@pgpmjs/logger'; -import { PgpmOptions } from '@pgpmjs/types'; import { NextFunction, Request, RequestHandler, Response } from 'express'; import { getPgPool } from 'pg-cache'; import pgQueryContext from 'pg-query-context'; @@ -27,7 +27,7 @@ const parseCookieToken = (req: Request, cookieName: string): string | undefined }; export const createAuthenticateMiddleware = ( - opts: PgpmOptions + opts: ConstructiveOptions ): RequestHandler => { return async ( req: Request, diff --git a/graphql/server/src/middleware/error-handler.ts b/graphql/server/src/middleware/error-handler.ts index 3820274675..f5daafcab6 100644 --- a/graphql/server/src/middleware/error-handler.ts +++ b/graphql/server/src/middleware/error-handler.ts @@ -1,4 +1,4 @@ -import { getNodeEnv } from '@pgpmjs/env'; +import { getNodeEnv } from '@constructive-io/graphql-env'; import { Logger } from '@pgpmjs/logger'; import type { ErrorRequestHandler, NextFunction, Request, Response } from 'express'; diff --git a/graphql/server/src/middleware/graphile.ts b/graphql/server/src/middleware/graphile.ts index 477e61288b..d5a0dabffe 100644 --- a/graphql/server/src/middleware/graphile.ts +++ b/graphql/server/src/middleware/graphile.ts @@ -1,5 +1,5 @@ import crypto from 'node:crypto'; -import { getNodeEnv } from '@pgpmjs/env'; +import { getNodeEnv } from '@constructive-io/graphql-env'; import type { ConstructiveOptions } from '@constructive-io/graphql-types'; import { Logger } from '@pgpmjs/logger'; import type { NextFunction, Request, RequestHandler, Response } from 'express'; diff --git a/graphql/types/README.md b/graphql/types/README.md index 64a07165bb..0ed3213750 100644 --- a/graphql/types/README.md +++ b/graphql/types/README.md @@ -12,9 +12,9 @@

-GraphQL/Graphile types for the Constructive framework. +Configuration types for the Constructive framework. -This package contains TypeScript type definitions for PostGraphile/Graphile configuration used by Constructive server, explorer, and related packages. +This package contains the upper-level `ConstructiveOptions` model used by Constructive servers, explorers, jobs, storage, email, and related packages. `ConstructiveOptions` extends the lower-level `PgpmOptions` model, so one resolved object contains both PostgreSQL/PGPM configuration and Constructive application configuration. ## Installation @@ -25,12 +25,12 @@ npm install @constructive-io/graphql-types ## Usage ```typescript -import { - ConstructiveOptions, - GraphileOptions, - ApiOptions, +import { + ConstructiveOptions, + GraphileOptions, + ApiOptions, GraphileFeatureOptions, - constructiveDefaults + constructiveDefaults, } from '@constructive-io/graphql-types'; // ConstructiveOptions extends PgpmOptions with GraphQL configuration @@ -47,6 +47,9 @@ const config: ConstructiveOptions = { simpleInflection: true, postgis: true, }, + server: { + port: 4000, + }, }; ``` @@ -54,7 +57,18 @@ const config: ConstructiveOptions = { ### ConstructiveOptions -Full configuration options for Constructive framework, extending `PgpmOptions` with GraphQL/Graphile configuration. +Full configuration options for the Constructive framework. It extends `PgpmOptions` and owns GraphQL/Graphile configuration plus the server, CDN/storage, jobs, and SMTP sections moved out of PGPM. + +### Environment ownership types + +This package owns the following types and their defaults: + +- `ServerOptions` +- `BucketProvider` and `CDNOptions` +- jobs worker, scheduler, gateway, operation DTOs, and `JobsConfig` +- `SmtpOptions` + +The lower-level `@pgpmjs/types` package continues to own PostgreSQL connections, test database settings, workspace/package configuration, deployment, migrations, and PGPM error output. ### GraphileOptions @@ -68,6 +82,6 @@ Configuration for the Constructive API including meta API settings, exposed sche Feature flags for GraphQL/Graphile including inflection settings and PostGIS support. -## Re-exports +## Scope -This package re-exports all types from `@pgpmjs/types` for convenience, so you can import both core PGPM types and GraphQL types from a single package. +This ownership change is intentionally limited to server, CDN/storage, jobs, and SMTP configuration. Moving `getNodeEnv()` belongs to `@constructive-io/graphql-env`; consolidating Mailgun, function-specific, Knative, LLM, observability, CAPTCHA, Graphile runtime, and test-only environment models remains follow-up work. diff --git a/graphql/types/src/constructive.ts b/graphql/types/src/constructive.ts index fc64c547c0..12daaf951c 100644 --- a/graphql/types/src/constructive.ts +++ b/graphql/types/src/constructive.ts @@ -5,10 +5,7 @@ import { pgpmDefaults, PgTestConnectionOptions, DeploymentOptions, - ServerOptions, - CDNOptions, - MigrationOptions, - JobsConfig + MigrationOptions } from '@pgpmjs/types'; import { GraphileOptions, @@ -19,6 +16,16 @@ import { apiDefaults } from './graphile'; import { LlmOptions } from './llm'; +import { + CDNOptions, + JobsConfig, + ServerOptions, + SmtpOptions, + cdnDefaults, + jobsDefaults, + serverDefaults, + smtpDefaults +} from './environment'; /** * GraphQL-specific options for Constructive @@ -30,6 +37,14 @@ export interface ConstructiveGraphQLOptions { features?: GraphileFeatureOptions; /** API configuration options */ api?: ApiOptions; + /** HTTP server configuration */ + server?: ServerOptions; + /** CDN and file storage configuration */ + cdn?: CDNOptions; + /** Job system configuration */ + jobs?: JobsConfig; + /** SMTP email configuration */ + smtp?: SmtpOptions; } /** @@ -57,6 +72,8 @@ export interface ConstructiveOptions extends PgpmOptions, ConstructiveGraphQLOpt migrations?: MigrationOptions; /** Job system configuration */ jobs?: JobsConfig; + /** SMTP email configuration */ + smtp?: SmtpOptions; /** LLM provider configuration (embeddings, chat, RAG) */ llm?: LlmOptions; } @@ -67,7 +84,15 @@ export interface ConstructiveOptions extends PgpmOptions, ConstructiveGraphQLOpt export const constructiveGraphqlDefaults: ConstructiveGraphQLOptions = { graphile: graphileDefaults, features: graphileFeatureDefaults, - api: apiDefaults + api: apiDefaults, + server: serverDefaults, + cdn: cdnDefaults, + jobs: { + schema: jobsDefaults.schema, + worker: jobsDefaults.worker, + scheduler: jobsDefaults.scheduler + }, + smtp: smtpDefaults }; /** diff --git a/graphql/types/src/environment.ts b/graphql/types/src/environment.ts new file mode 100644 index 0000000000..ac0bca19c2 --- /dev/null +++ b/graphql/types/src/environment.ts @@ -0,0 +1,179 @@ +/** HTTP server configuration. */ +export interface ServerOptions { + host?: string; + port?: number; + trustProxy?: boolean; + origin?: string; + strictAuth?: boolean; +} + +export const serverDefaults: ServerOptions = { + host: 'localhost', + port: 3000, + trustProxy: false, + strictAuth: false, +}; + +/** Storage provider type for CDN/bucket operations. */ +export type BucketProvider = 's3' | 'minio' | 'gcs'; + +/** CDN and file storage configuration. */ +export interface CDNOptions { + provider?: BucketProvider; + bucketName?: string; + awsRegion?: string; + awsAccessKey?: string; + awsSecretKey?: string; + endpoint?: string; + publicUrlPrefix?: string; +} + +export const cdnDefaults: CDNOptions = { + provider: 'minio', + bucketName: 'test-bucket', + awsRegion: 'us-east-1', + awsAccessKey: 'minioadmin', + awsSecretKey: 'minioadmin', + endpoint: 'http://localhost:9000', + publicUrlPrefix: 'http://localhost:9000', +}; + +export interface JobSchemaConfig { + schema: string; +} + +export interface JobHostnameConfig { + hostname: string; +} + +export interface JobTaskSupportConfig { + supportAny: boolean; + supported: string[]; +} + +export interface JobGatewayConfig { + gatewayUrl: string; + callbackUrl: string; + callbackPort: number; +} + +export interface FailJobParams { + workerId: string; + jobId: number | string; + message: string; +} + +export interface CompleteJobParams { + workerId: string; + jobId: number | string; +} + +export interface GetJobParams { + workerId: string; + supportedTaskNames: string[] | null; +} + +export interface GetScheduledJobParams { + workerId: string; + supportedTaskNames: string[] | null; +} + +export interface RunScheduledJobParams { + jobId: number | string; +} + +export interface ReleaseScheduledJobsParams { + workerId: string; + ids?: Array; +} + +export interface ReleaseJobsParams { + workerId: string; +} + +export interface Job { + id: number | string; + task_name: string; + payload?: any; + worker_id?: string; + max_attempts?: number; + attempts?: number; + priority?: number; + created_at?: Date | string; + updated_at?: Date | string; + run_at?: Date | string; + last_error?: string; +} + +export interface JobWorkerConfig + extends JobSchemaConfig, JobHostnameConfig, JobTaskSupportConfig { + pollInterval?: number; + gracefulShutdown?: boolean; +} + +export interface JobSchedulerConfig + extends JobSchemaConfig, JobHostnameConfig, JobTaskSupportConfig { + pollInterval?: number; + gracefulShutdown?: boolean; +} + +export interface JobsConfig { + schema?: Partial; + worker?: Partial; + scheduler?: Partial; + gateway?: Partial; +} + +export const jobsDefaults: JobsConfig = { + schema: { + schema: 'app_jobs', + }, + worker: { + schema: 'app_jobs', + hostname: 'worker-0', + supportAny: true, + supported: [], + pollInterval: 1000, + gracefulShutdown: true, + }, + scheduler: { + schema: 'app_jobs', + hostname: 'scheduler-0', + supportAny: true, + supported: [], + pollInterval: 1000, + gracefulShutdown: true, + }, + gateway: { + gatewayUrl: 'http://gateway:8080', + callbackUrl: 'http://callback:12345', + callbackPort: 12345, + }, +}; + +/** SMTP email configuration. */ +export interface SmtpOptions { + host?: string; + port?: number; + secure?: boolean; + user?: string; + pass?: string; + from?: string; + replyTo?: string; + requireTLS?: boolean; + tlsRejectUnauthorized?: boolean; + pool?: boolean; + maxConnections?: number; + maxMessages?: number; + name?: string; + logger?: boolean; + debug?: boolean; +} + +export const smtpDefaults: SmtpOptions = { + port: 587, + secure: false, + pool: false, + logger: false, + debug: false, +}; diff --git a/graphql/types/src/index.ts b/graphql/types/src/index.ts index a66eb0bf63..4b27c46c68 100644 --- a/graphql/types/src/index.ts +++ b/graphql/types/src/index.ts @@ -29,3 +29,6 @@ export { LlmEmbedderOptions, LlmChatOptions } from './llm'; + +// Export Constructive runtime environment types and defaults +export * from './environment'; diff --git a/jobs/job-utils/package.json b/jobs/job-utils/package.json index a14e1108a1..38194f6064 100644 --- a/jobs/job-utils/package.json +++ b/jobs/job-utils/package.json @@ -36,9 +36,10 @@ "makage": "^0.3.0" }, "dependencies": { + "@constructive-io/graphql-env": "workspace:^", + "@constructive-io/graphql-types": "workspace:^", "@pgpmjs/env": "workspace:^", "@pgpmjs/logger": "workspace:^", - "@pgpmjs/types": "workspace:^", "pg-cache": "workspace:^", "pg-env": "workspace:^" } diff --git a/jobs/job-utils/src/index.ts b/jobs/job-utils/src/index.ts index ab8b70f04a..cb63668e7c 100644 --- a/jobs/job-utils/src/index.ts +++ b/jobs/job-utils/src/index.ts @@ -6,7 +6,7 @@ import type { RunScheduledJobParams, ReleaseScheduledJobsParams, ReleaseJobsParams -} from '@pgpmjs/types'; +} from '@constructive-io/graphql-types'; import { getJobSchema, diff --git a/jobs/job-utils/src/runtime.ts b/jobs/job-utils/src/runtime.ts index 4f0903eff8..c59365b854 100644 --- a/jobs/job-utils/src/runtime.ts +++ b/jobs/job-utils/src/runtime.ts @@ -1,9 +1,9 @@ -import { getEnvOptions, getNodeEnv, parseEnvBoolean } from '@pgpmjs/env'; +import { getEnvOptions, getNodeEnv } from '@constructive-io/graphql-env'; +import { jobsDefaults, type ConstructiveOptions } from '@constructive-io/graphql-types'; +import { parseEnvBoolean } from '@pgpmjs/env'; import { defaultPgConfig, getPgEnvVars, type PgConfig } from 'pg-env'; import { buildConnectionString, getPgPool } from 'pg-cache'; import type { Pool } from 'pg'; -import type { PgpmOptions } from '@pgpmjs/types'; -import { jobsDefaults } from '@pgpmjs/types'; type Maybe = T | null | undefined; @@ -12,7 +12,7 @@ const toStrArray = (v: Maybe): string[] | undefined => // ---- PG config ---- export const getJobPgConfig = (): PgConfig => { - const opts: PgpmOptions = getEnvOptions(); + const opts: ConstructiveOptions = getEnvOptions(); const envOnly = getPgEnvVars(); return { @@ -32,7 +32,7 @@ export const getJobConnectionString = (): string => { // ---- Schema ---- export const getJobSchema = (): string => { - const opts: PgpmOptions = getEnvOptions(); + const opts: ConstructiveOptions = getEnvOptions(); const fromOpts: string | undefined = opts.jobs?.schema?.schema; return ( fromOpts || @@ -43,7 +43,7 @@ export const getJobSchema = (): string => { // ---- SupportAny / Supported ---- export const getJobSupportAny = (): boolean => { - const opts: PgpmOptions = getEnvOptions(); + const opts: ConstructiveOptions = getEnvOptions(); const envVal = parseEnvBoolean(process.env.JOBS_SUPPORT_ANY); if (typeof envVal === 'boolean') return envVal; @@ -59,7 +59,7 @@ export const getJobSupportAny = (): boolean => { }; export const getJobSupported = (): string[] => { - const opts: PgpmOptions = getEnvOptions(); + const opts: ConstructiveOptions = getEnvOptions(); const worker: string[] | undefined = opts.jobs?.worker?.supported; const scheduler: string[] | undefined = opts.jobs?.scheduler?.supported; @@ -73,7 +73,7 @@ export const getJobSupported = (): string[] => { // ---- Hostnames ---- export const getWorkerHostname = (): string => { - const opts: PgpmOptions = getEnvOptions(); + const opts: ConstructiveOptions = getEnvOptions(); return ( process.env.HOSTNAME || opts.jobs?.worker?.hostname || @@ -83,7 +83,7 @@ export const getWorkerHostname = (): string => { }; export const getSchedulerHostname = (): string => { - const opts: PgpmOptions = getEnvOptions(); + const opts: ConstructiveOptions = getEnvOptions(); return ( process.env.HOSTNAME || opts.jobs?.scheduler?.hostname || @@ -94,7 +94,7 @@ export const getSchedulerHostname = (): string => { // ---- Job gateway config (generic HTTP gateway) ---- export const getJobGatewayConfig = () => { - const opts: PgpmOptions = getEnvOptions(); + const opts: ConstructiveOptions = getEnvOptions(); const gateway = opts.jobs?.gateway ?? {}; const defaults = jobsDefaults.gateway ?? { gatewayUrl: 'http://gateway:8080', diff --git a/packages/smtppostmaster/__tests__/test-send.ts b/packages/smtppostmaster/__tests__/test-send.ts index 9a747da343..c002700df1 100644 --- a/packages/smtppostmaster/__tests__/test-send.ts +++ b/packages/smtppostmaster/__tests__/test-send.ts @@ -1,5 +1,5 @@ -import { getEnvOptions } from '@pgpmjs/env'; -import { SmtpOptions } from '@pgpmjs/types'; +import { getEnvOptions } from '@constructive-io/graphql-env'; +import type { SmtpOptions } from '@constructive-io/graphql-types'; import { send } from '../src/index'; import { createSmtpCatcher } from './smtp-catcher'; diff --git a/packages/smtppostmaster/package.json b/packages/smtppostmaster/package.json index 3b315710c6..e1327e394c 100644 --- a/packages/smtppostmaster/package.json +++ b/packages/smtppostmaster/package.json @@ -31,8 +31,8 @@ "test:send:dev": "ts-node __tests__/test-send.ts" }, "dependencies": { - "@pgpmjs/env": "workspace:^", - "@pgpmjs/types": "workspace:^", + "@constructive-io/graphql-env": "workspace:^", + "@constructive-io/graphql-types": "workspace:^", "nodemailer": "^6.9.13" }, "devDependencies": { diff --git a/packages/smtppostmaster/src/index.ts b/packages/smtppostmaster/src/index.ts index de72bc76d1..ad9ec9c6b0 100644 --- a/packages/smtppostmaster/src/index.ts +++ b/packages/smtppostmaster/src/index.ts @@ -1,7 +1,7 @@ import nodemailer, { SendMailOptions } from 'nodemailer'; import SMTPTransport from 'nodemailer/lib/smtp-transport'; -import { getEnvOptions } from '@pgpmjs/env'; -import { SmtpOptions } from '@pgpmjs/types'; +import { getEnvOptions } from '@constructive-io/graphql-env'; +import type { SmtpOptions } from '@constructive-io/graphql-types'; type SendInput = { to: string | string[]; diff --git a/pgpm/env/README.md b/pgpm/env/README.md index bc41f0aa9d..357206c3b3 100644 --- a/pgpm/env/README.md +++ b/pgpm/env/README.md @@ -12,19 +12,30 @@

-Environment management for PGPM (and Constructive) projects. Provides unified configuration resolution from defaults, config files, environment variables, and overrides. +Lower-level environment management for PostgreSQL and the PGPM toolchain. It resolves PGPM defaults, config files, environment variables, and runtime overrides without taking ownership of Constructive application configuration. + +`@pgpmjs/env` owns database connections, test database settings, workspace/package configuration, deployment, migrations, and PGPM error output. HTTP server, CDN/storage, jobs, and SMTP configuration are owned by the upper-level `@constructive-io/graphql-env` resolver. ## Features - Config file discovery using `walkUp` utility -- Environment variable parsing +- PostgreSQL and PGPM environment variable parsing - Unified merge hierarchy: defaults → config → env vars → overrides +- An explicit PGPM-only result boundary - TypeScript support with full type safety ## Usage ```typescript -import { getEnvOptions } from '@pgpmjs/env'; +import { getPgpmEnvOptions } from '@pgpmjs/env'; -const options = getEnvOptions(overrides, cwd); +const options = getPgpmEnvOptions(overrides, cwd); ``` + +`getEnvOptions` is an exact alias of `getPgpmEnvOptions` for existing PGPM callers. + +For a complete Constructive configuration, including PGPM values plus GraphQL runtime configuration, use `getConstructiveEnvOptions` from `@constructive-io/graphql-env`. + +## Ownership change + +This refactor moves only the existing server, CDN/storage, jobs, and SMTP configuration, plus `getNodeEnv()`, from PGPM env to GraphQL env. It does not consolidate other environment variables currently read by Mailgun providers, functions, Knative runtimes, LLM integrations, or other packages; those remain follow-up work. diff --git a/pgpm/env/__tests__/__snapshots__/merge.test.ts.snap b/pgpm/env/__tests__/__snapshots__/merge.test.ts.snap index 8ba29ba739..59760cd174 100644 --- a/pgpm/env/__tests__/__snapshots__/merge.test.ts.snap +++ b/pgpm/env/__tests__/__snapshots__/merge.test.ts.snap @@ -2,15 +2,6 @@ exports[`getEnvOptions merges defaults, config, env, and overrides 1`] = ` { - "cdn": { - "awsAccessKey": "minioadmin", - "awsRegion": "us-east-1", - "awsSecretKey": "minioadmin", - "bucketName": "test-bucket", - "endpoint": "http://localhost:9000", - "provider": "minio", - "publicUrlPrefix": "http://localhost:9000", - }, "db": { "connections": { "admin": { @@ -47,33 +38,6 @@ exports[`getEnvOptions merges defaults, config, env, and overrides 1`] = ` "queryHistoryLimit": 30, "verbose": false, }, - "jobs": { - "scheduler": { - "gracefulShutdown": true, - "hostname": "scheduler-0", - "pollInterval": 1000, - "schema": "app_jobs", - "supportAny": false, - "supported": [ - "alpha", - "beta", - ], - }, - "schema": { - "schema": "app_jobs", - }, - "worker": { - "gracefulShutdown": true, - "hostname": "worker-0", - "pollInterval": 1000, - "schema": "app_jobs", - "supportAny": false, - "supported": [ - "alpha", - "beta", - ], - }, - }, "migrations": { "codegen": { "useTx": false, @@ -86,18 +50,5 @@ exports[`getEnvOptions merges defaults, config, env, and overrides 1`] = ` "port": 6543, "user": "env-user", }, - "server": { - "host": "localhost", - "port": 9999, - "strictAuth": false, - "trustProxy": false, - }, - "smtp": { - "debug": false, - "logger": false, - "pool": false, - "port": 587, - "secure": false, - }, } `; diff --git a/pgpm/env/__tests__/merge.test.ts b/pgpm/env/__tests__/merge.test.ts index c880841c58..ce1bb9c6e1 100644 --- a/pgpm/env/__tests__/merge.test.ts +++ b/pgpm/env/__tests__/merge.test.ts @@ -1,4 +1,8 @@ -import { getConnEnvOptions, getEnvOptions } from '../src/merge'; +import { + getConnEnvOptions, + getEnvOptions, + getPgpmEnvOptions +} from '../src/merge'; import { pgpmDefaults, PgpmOptions } from '@pgpmjs/types'; import * as fs from 'fs'; import * as os from 'os'; @@ -111,7 +115,6 @@ describe('getConnEnvOptions', () => { describe('getEnvOptions', () => { let tempDir = ''; - type PgpmOptionsWithPackages = PgpmOptions & { packages?: string[] }; afterEach(() => { if (tempDir) { @@ -120,6 +123,10 @@ describe('getEnvOptions', () => { } }); + it('is the exact alias of getPgpmEnvOptions', () => { + expect(getEnvOptions).toBe(getPgpmEnvOptions); + }); + it('merges defaults, config, env, and overrides', () => { tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pgpm-env-')); writeConfig(tempDir, { @@ -158,7 +165,7 @@ describe('getEnvOptions', () => { }; const result = getEnvOptions( - { + ({ db: { prefix: 'override-', cwd: '' @@ -172,7 +179,7 @@ describe('getEnvOptions', () => { deployment: { cache: true } - }, + } as unknown as PgpmOptions), tempDir, testEnv ); @@ -186,43 +193,67 @@ describe('getEnvOptions', () => { db: { extensions: ['uuid', 'postgis'] }, - jobs: { - worker: { - supported: ['alpha', 'beta'] - }, - scheduler: { - supported: ['beta', 'gamma'] - } - }, packages: ['testing/*', 'packages/*'] }); const testEnv: NodeJS.ProcessEnv = { - DB_EXTENSIONS: 'postgis,pgcrypto', - JOBS_SUPPORTED: 'beta,gamma,delta' + DB_EXTENSIONS: 'postgis,pgcrypto' }; - const overrides: PgpmOptionsWithPackages = { + const overrides: PgpmOptions = { db: { extensions: ['uuid', 'hstore'] }, - jobs: { - worker: { - supported: ['delta', 'epsilon'] - }, - scheduler: { - supported: ['gamma', 'zeta'] - } - }, packages: ['testing/*', 'extensions/*'] }; - const result = getEnvOptions(overrides, tempDir, testEnv) as PgpmOptionsWithPackages; + const result = getEnvOptions(overrides, tempDir, testEnv); // Arrays are replaced, not merged - overrides win completely expect(result.db?.extensions).toEqual(['uuid', 'hstore']); - expect(result.jobs?.worker?.supported).toEqual(['delta', 'epsilon']); - expect(result.jobs?.scheduler?.supported).toEqual(['gamma', 'zeta']); expect(result.packages).toEqual(['testing/*', 'extensions/*']); }); + + it('projects config, environment, and overrides to PGPM-owned keys', () => { + tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pgpm-env-boundary-')); + writeConfig(tempDir, { + packages: ['modules/*'], + boilerplates: { repo: 'https://example.test/templates.git' }, + driver: { plugin: '@pgpmjs/example-driver' }, + server: { port: 4000 }, + cdn: { bucketName: 'config-bucket' }, + jobs: { schema: { schema: 'config_jobs' } }, + smtp: { host: 'config-smtp' }, + graphile: { schema: ['public'] } + }); + + const overrides = { + settings: { retained: true }, + server: { port: 5000 }, + cdn: { bucketName: 'override-bucket' }, + jobs: { schema: { schema: 'override_jobs' } }, + smtp: { host: 'override-smtp' }, + api: { isPublic: false } + } as unknown as PgpmOptions; + + const result = getPgpmEnvOptions(overrides, tempDir, { + PORT: '6000', + BUCKET_NAME: 'env-bucket', + JOBS_SCHEMA: 'env_jobs', + SMTP_HOST: 'env-smtp' + }); + + expect(result).not.toHaveProperty('server'); + expect(result).not.toHaveProperty('cdn'); + expect(result).not.toHaveProperty('jobs'); + expect(result).not.toHaveProperty('smtp'); + expect(result).not.toHaveProperty('graphile'); + expect(result).not.toHaveProperty('api'); + expect(result.packages).toEqual(['modules/*']); + expect(result.boilerplates).toEqual({ + repo: 'https://example.test/templates.git' + }); + expect(result.driver).toEqual({ plugin: '@pgpmjs/example-driver' }); + expect(result.settings).toEqual({ retained: true }); + }); }); diff --git a/pgpm/env/src/env.ts b/pgpm/env/src/env.ts index 6423c059d4..64eb07a8db 100644 --- a/pgpm/env/src/env.ts +++ b/pgpm/env/src/env.ts @@ -1,4 +1,4 @@ -import { PgpmOptions, BucketProvider } from '@pgpmjs/types'; +import { PgpmOptions } from '@pgpmjs/types'; export const parseEnvNumber = (val?: string): number | undefined => { const num = Number(val); @@ -10,14 +10,6 @@ export const parseEnvBoolean = (val?: string): boolean | undefined => { return ['true', '1', 'yes'].includes(val.toLowerCase()); }; -const parseEnvStringArray = (val?: string): string[] | undefined => { - if (!val) return undefined; - return val - .split(',') - .map(s => s.trim()) - .filter(Boolean); -}; - /** * Parse core PGPM environment variables. * GraphQL-related env vars (GRAPHILE_*, FEATURES_*, API_*) are handled by @constructive-io/graphql-env. @@ -40,28 +32,12 @@ export const getEnvVars = (env: NodeJS.ProcessEnv = process.env): PgpmOptions => DB_CONNECTIONS_ADMIN_USER, DB_CONNECTIONS_ADMIN_PASSWORD, - PORT, - SERVER_HOST, - SERVER_TRUST_PROXY, - SERVER_ORIGIN, - SERVER_STRICT_AUTH, - PGHOST, PGPORT, PGUSER, PGPASSWORD, PGDATABASE, - BUCKET_PROVIDER, - BUCKET_NAME, - AWS_REGION, - AWS_ACCESS_KEY, - AWS_ACCESS_KEY_ID, - AWS_SECRET_KEY, - AWS_SECRET_ACCESS_KEY, - CDN_ENDPOINT, - CDN_PUBLIC_URL_PREFIX, - DEPLOYMENT_USE_TX, DEPLOYMENT_FAST, DEPLOYMENT_USE_PLAN, @@ -70,35 +46,10 @@ export const getEnvVars = (env: NodeJS.ProcessEnv = process.env): PgpmOptions => MIGRATIONS_CODEGEN_USE_TX, - // Jobs-related env vars - JOBS_SCHEMA, - JOBS_SUPPORT_ANY, - JOBS_SUPPORTED, - INTERNAL_GATEWAY_URL, - INTERNAL_JOBS_CALLBACK_URL, - INTERNAL_JOBS_CALLBACK_PORT, - // Error output formatting env vars PGPM_ERROR_QUERY_HISTORY_LIMIT, PGPM_ERROR_MAX_LENGTH, - PGPM_ERROR_VERBOSE, - - // SMTP email env vars - SMTP_HOST, - SMTP_PORT, - SMTP_SECURE, - SMTP_USER, - SMTP_PASS, - SMTP_FROM, - SMTP_REPLY_TO, - SMTP_REQUIRE_TLS, - SMTP_TLS_REJECT_UNAUTHORIZED, - SMTP_POOL, - SMTP_MAX_CONNECTIONS, - SMTP_MAX_MESSAGES, - SMTP_NAME, - SMTP_LOGGER, - SMTP_DEBUG + PGPM_ERROR_VERBOSE } = env; return { @@ -132,13 +83,6 @@ export const getEnvVars = (env: NodeJS.ProcessEnv = process.env): PgpmOptions => } }), }, - server: { - ...(PORT && { port: parseEnvNumber(PORT) }), - ...(SERVER_HOST && { host: SERVER_HOST }), - ...(SERVER_TRUST_PROXY && { trustProxy: parseEnvBoolean(SERVER_TRUST_PROXY) }), - ...(SERVER_ORIGIN && { origin: SERVER_ORIGIN }), - ...(SERVER_STRICT_AUTH && { strictAuth: parseEnvBoolean(SERVER_STRICT_AUTH) }), - }, pg: { ...(PGHOST && { host: PGHOST }), ...(PGPORT && { port: parseEnvNumber(PGPORT) }), @@ -146,15 +90,6 @@ export const getEnvVars = (env: NodeJS.ProcessEnv = process.env): PgpmOptions => ...(PGPASSWORD && { password: PGPASSWORD }), ...(PGDATABASE && { database: PGDATABASE }), }, - cdn: { - ...(BUCKET_PROVIDER && { provider: BUCKET_PROVIDER as BucketProvider }), - ...(BUCKET_NAME && { bucketName: BUCKET_NAME }), - ...(AWS_REGION && { awsRegion: AWS_REGION }), - ...((AWS_ACCESS_KEY || AWS_ACCESS_KEY_ID) && { awsAccessKey: AWS_ACCESS_KEY || AWS_ACCESS_KEY_ID }), - ...((AWS_SECRET_KEY || AWS_SECRET_ACCESS_KEY) && { awsSecretKey: AWS_SECRET_KEY || AWS_SECRET_ACCESS_KEY }), - ...(CDN_ENDPOINT && { endpoint: CDN_ENDPOINT }), - ...(CDN_PUBLIC_URL_PREFIX && { publicUrlPrefix: CDN_PUBLIC_URL_PREFIX }), - }, deployment: { ...(DEPLOYMENT_USE_TX && { useTx: parseEnvBoolean(DEPLOYMENT_USE_TX) }), ...(DEPLOYMENT_FAST && { fast: parseEnvBoolean(DEPLOYMENT_FAST) }), @@ -169,75 +104,10 @@ export const getEnvVars = (env: NodeJS.ProcessEnv = process.env): PgpmOptions => } }), }, - jobs: { - ...(JOBS_SCHEMA && { - schema: { - schema: JOBS_SCHEMA - } - }), - ...((JOBS_SUPPORT_ANY || JOBS_SUPPORTED) && { - worker: { - ...(JOBS_SUPPORT_ANY && { - supportAny: parseEnvBoolean(JOBS_SUPPORT_ANY) - }), - ...(JOBS_SUPPORTED && { - supported: parseEnvStringArray(JOBS_SUPPORTED) - }) - }, - scheduler: { - ...(JOBS_SUPPORT_ANY && { - supportAny: parseEnvBoolean(JOBS_SUPPORT_ANY) - }), - ...(JOBS_SUPPORTED && { - supported: parseEnvStringArray(JOBS_SUPPORTED) - }) - } - }), - ...((INTERNAL_GATEWAY_URL || - INTERNAL_JOBS_CALLBACK_URL || - INTERNAL_JOBS_CALLBACK_PORT) && { - gateway: { - ...(INTERNAL_GATEWAY_URL && { - gatewayUrl: INTERNAL_GATEWAY_URL - }), - ...(INTERNAL_JOBS_CALLBACK_URL && { - callbackUrl: INTERNAL_JOBS_CALLBACK_URL - }), - ...(INTERNAL_JOBS_CALLBACK_PORT && { - callbackPort: parseEnvNumber(INTERNAL_JOBS_CALLBACK_PORT) - }) - } - }) - }, errorOutput: { ...(PGPM_ERROR_QUERY_HISTORY_LIMIT && { queryHistoryLimit: parseEnvNumber(PGPM_ERROR_QUERY_HISTORY_LIMIT) }), ...(PGPM_ERROR_MAX_LENGTH && { maxLength: parseEnvNumber(PGPM_ERROR_MAX_LENGTH) }), ...(PGPM_ERROR_VERBOSE && { verbose: parseEnvBoolean(PGPM_ERROR_VERBOSE) }), - }, - smtp: { - ...(SMTP_HOST && { host: SMTP_HOST }), - ...(SMTP_PORT && { port: parseEnvNumber(SMTP_PORT) }), - ...(SMTP_SECURE && { secure: parseEnvBoolean(SMTP_SECURE) }), - ...(SMTP_USER && { user: SMTP_USER }), - ...(SMTP_PASS && { pass: SMTP_PASS }), - ...(SMTP_FROM && { from: SMTP_FROM }), - ...(SMTP_REPLY_TO && { replyTo: SMTP_REPLY_TO }), - ...(SMTP_REQUIRE_TLS && { requireTLS: parseEnvBoolean(SMTP_REQUIRE_TLS) }), - ...(SMTP_TLS_REJECT_UNAUTHORIZED && { tlsRejectUnauthorized: parseEnvBoolean(SMTP_TLS_REJECT_UNAUTHORIZED) }), - ...(SMTP_POOL && { pool: parseEnvBoolean(SMTP_POOL) }), - ...(SMTP_MAX_CONNECTIONS && { maxConnections: parseEnvNumber(SMTP_MAX_CONNECTIONS) }), - ...(SMTP_MAX_MESSAGES && { maxMessages: parseEnvNumber(SMTP_MAX_MESSAGES) }), - ...(SMTP_NAME && { name: SMTP_NAME }), - ...(SMTP_LOGGER && { logger: parseEnvBoolean(SMTP_LOGGER) }), - ...(SMTP_DEBUG && { debug: parseEnvBoolean(SMTP_DEBUG) }), } }; }; - -type NodeEnv = 'development' | 'production' | 'test'; - -export const getNodeEnv = (): NodeEnv => { - const env = process.env.NODE_ENV?.toLowerCase(); - if (env === 'production' || env === 'test') return env; - return 'development'; -}; diff --git a/pgpm/env/src/index.ts b/pgpm/env/src/index.ts index 9ec47ea631..9055bc1aee 100644 --- a/pgpm/env/src/index.ts +++ b/pgpm/env/src/index.ts @@ -1,4 +1,9 @@ -export { getEnvOptions, getConnEnvOptions, getDeploymentEnvOptions } from './merge'; +export { + getPgpmEnvOptions, + getEnvOptions, + getConnEnvOptions, + getDeploymentEnvOptions +} from './merge'; export { loadConfigSync, loadConfigSyncFromDir, @@ -10,7 +15,7 @@ export { resolveWorkspaceByType } from './config'; export type { WorkspaceType } from './config'; -export { getEnvVars, getNodeEnv, parseEnvBoolean, parseEnvNumber } from './env'; +export { getEnvVars, parseEnvBoolean, parseEnvNumber } from './env'; export { walkUp, replaceArrays } from './utils'; export type { PgpmOptions, PgTestConnectionOptions, DeploymentOptions } from '@pgpmjs/types'; diff --git a/pgpm/env/src/merge.ts b/pgpm/env/src/merge.ts index d0e920db3d..3f6e87cbc6 100644 --- a/pgpm/env/src/merge.ts +++ b/pgpm/env/src/merge.ts @@ -4,6 +4,35 @@ import { loadConfigSync } from './config'; import { getEnvVars } from './env'; import { replaceArrays } from './utils'; +const PGPM_OPTION_KEYS = [ + 'db', + 'pg', + 'packages', + 'name', + 'version', + 'settings', + 'boilerplates', + 'deployment', + 'migrations', + 'errorOutput', + 'driver' +] as const; + +const projectPgpmOptions = (options: unknown): PgpmOptions => { + if (!options || typeof options !== 'object') return {}; + + const source = options as Record; + const projected: Record = {}; + + for (const key of PGPM_OPTION_KEYS) { + if (Object.prototype.hasOwnProperty.call(source, key)) { + projected[key] = source[key]; + } + } + + return projected as PgpmOptions; +}; + /** * Get core PGPM environment options by merging: * 1. PGPM defaults @@ -17,21 +46,26 @@ import { replaceArrays } from './utils'; * @param cwd - Working directory for config file resolution * @param env - Environment object to read from (defaults to process.env for backwards compatibility) */ -export const getEnvOptions = ( +const resolvePgpmEnvOptions = ( overrides: PgpmOptions = {}, cwd: string = process.cwd(), env: NodeJS.ProcessEnv = process.env ): PgpmOptions => { - const configOptions = loadConfigSync(cwd); - const envOptions = getEnvVars(env); - - return deepmerge.all([pgpmDefaults, configOptions, envOptions, overrides], { + const defaultOptions = projectPgpmOptions(pgpmDefaults); + const configOptions = projectPgpmOptions(loadConfigSync(cwd)); + const envOptions = projectPgpmOptions(getEnvVars(env)); + const overrideOptions = projectPgpmOptions(overrides); + + return deepmerge.all([defaultOptions, configOptions, envOptions, overrideOptions], { arrayMerge: replaceArrays }); }; +export const getPgpmEnvOptions = resolvePgpmEnvOptions; +export const getEnvOptions = getPgpmEnvOptions; + export const getConnEnvOptions = (overrides: Partial = {}, cwd: string = process.cwd()): PgTestConnectionOptions => { - const opts = getEnvOptions({ + const opts = getPgpmEnvOptions({ db: overrides }, cwd); @@ -60,7 +94,7 @@ export const getConnEnvOptions = (overrides: Partial = }; export const getDeploymentEnvOptions = (overrides: Partial = {}, cwd: string = process.cwd()): DeploymentOptions => { - const opts = getEnvOptions({ + const opts = getPgpmEnvOptions({ deployment: overrides }, cwd); return opts.deployment; diff --git a/pgpm/types/src/index.ts b/pgpm/types/src/index.ts index 9b2ae3f529..1774034210 100644 --- a/pgpm/types/src/index.ts +++ b/pgpm/types/src/index.ts @@ -3,5 +3,4 @@ export * from './error'; export * from './error-factory'; export * from './pg-error-format'; export * from './pgpm'; -export * from './jobs'; export * from './update'; diff --git a/pgpm/types/src/jobs.ts b/pgpm/types/src/jobs.ts deleted file mode 100644 index feeb2ca083..0000000000 --- a/pgpm/types/src/jobs.ts +++ /dev/null @@ -1,206 +0,0 @@ -/** - * Job schema configuration - */ -export interface JobSchemaConfig { - /** PostgreSQL schema name for job tables and functions (defaults to 'app_jobs') */ - schema: string; -} - -/** - * Worker/Scheduler hostname configuration - */ -export interface JobHostnameConfig { - /** Unique identifier for the worker or scheduler instance */ - hostname: string; -} - -/** - * Job task support configuration - */ -export interface JobTaskSupportConfig { - /** Whether to support any/all task types (defaults to true) */ - supportAny: boolean; - /** Array of explicitly supported task names */ - supported: string[]; -} - -/** - * Job HTTP gateway configuration - */ -export interface JobGatewayConfig { - /** Internal gateway URL for job HTTP function calls */ - gatewayUrl: string; - /** Callback URL for job completion notifications */ - callbackUrl: string; - /** Port for internal job callback server (defaults to 12345) */ - callbackPort: number; -} - -/** - * Parameters for failing a job - */ -export interface FailJobParams { - /** Worker ID that is failing the job */ - workerId: string; - /** Job ID to fail */ - jobId: number | string; - /** Error message or reason for failure */ - message: string; -} - -/** - * Parameters for completing a job - */ -export interface CompleteJobParams { - /** Worker ID that completed the job */ - workerId: string; - /** Job ID to mark as complete */ - jobId: number | string; -} - -/** - * Parameters for getting a job from the queue - */ -export interface GetJobParams { - /** Worker ID requesting a job */ - workerId: string; - /** - * Array of task names this worker supports. - * When `null`, the job system will consider any task as supported. - */ - supportedTaskNames: string[] | null; -} - -/** - * Parameters for getting a scheduled job - */ -export interface GetScheduledJobParams { - /** Worker ID requesting a scheduled job */ - workerId: string; - /** - * Array of task names this worker supports. - * When `null`, the scheduler will consider any task as supported. - */ - supportedTaskNames: string[] | null; -} - -/** - * Parameters for running a scheduled job - */ -export interface RunScheduledJobParams { - /** Job ID to run */ - jobId: number | string; -} - -/** - * Parameters for releasing scheduled jobs - */ -export interface ReleaseScheduledJobsParams { - /** Worker ID releasing the jobs */ - workerId: string; - /** - * Array of job IDs to release. - * When omitted, all scheduled jobs for the worker may be released. - */ - ids?: Array; -} - -/** - * Parameters for releasing all jobs held by a worker - */ -export interface ReleaseJobsParams { - /** Worker ID releasing all its jobs */ - workerId: string; -} - -/** - * Job record structure from database - */ -export interface Job { - /** Unique job identifier */ - id: number | string; - /** Task name/type for this job */ - task_name: string; - /** JSON payload data for the job */ - payload?: any; - /** Worker ID currently assigned to this job */ - worker_id?: string; - /** Maximum number of retry attempts */ - max_attempts?: number; - /** Current attempt number */ - attempts?: number; - /** Priority level for job execution */ - priority?: number; - /** Timestamp when job was created */ - created_at?: Date | string; - /** Timestamp when job was last updated */ - updated_at?: Date | string; - /** Timestamp when job should run (for scheduled jobs) */ - run_at?: Date | string; - /** Last error message if job failed */ - last_error?: string; -} - -/** - * Worker configuration options - */ -export interface JobWorkerConfig extends JobSchemaConfig, JobHostnameConfig, JobTaskSupportConfig { - /** Polling interval in milliseconds */ - pollInterval?: number; - /** Whether to enable graceful shutdown */ - gracefulShutdown?: boolean; -} - -/** - * Scheduler configuration options - */ -export interface JobSchedulerConfig extends JobSchemaConfig, JobHostnameConfig, JobTaskSupportConfig { - /** Polling interval in milliseconds for checking scheduled jobs */ - pollInterval?: number; - /** Whether to enable graceful shutdown */ - gracefulShutdown?: boolean; -} - -/** - * Complete job system configuration - */ -export interface JobsConfig { - /** Job schema configuration */ - schema?: Partial; - /** Worker configuration */ - worker?: Partial; - /** Scheduler configuration */ - scheduler?: Partial; - /** Job HTTP gateway configuration */ - gateway?: Partial; -} - -/** - * Default configuration values for job system - */ -export const jobsDefaults: JobsConfig = { - schema: { - schema: 'app_jobs' - }, - worker: { - schema: 'app_jobs', - hostname: 'worker-0', - supportAny: true, - supported: [], - pollInterval: 1000, - gracefulShutdown: true - }, - scheduler: { - schema: 'app_jobs', - hostname: 'scheduler-0', - supportAny: true, - supported: [], - pollInterval: 1000, - gracefulShutdown: true - }, - gateway: { - gatewayUrl: 'http://gateway:8080', - callbackUrl: 'http://callback:12345', - callbackPort: 12345 - } -}; diff --git a/pgpm/types/src/pgpm.ts b/pgpm/types/src/pgpm.ts index c80cc38eba..6cb8600343 100644 --- a/pgpm/types/src/pgpm.ts +++ b/pgpm/types/src/pgpm.ts @@ -2,7 +2,6 @@ import { execSync } from 'child_process'; import { PgConfig } from 'pg-env'; import { PgpmDriverConfig } from './driver'; -import { JobsConfig } from './jobs'; /** * Authentication options for test client sessions @@ -90,83 +89,6 @@ export interface TestUserCredentials { admin?: DatabaseConnectionOptions; } -/** - * HTTP server configuration - */ -export interface ServerOptions { - /** Server host address */ - host?: string; - /** Server port number */ - port?: number; - /** Whether to trust proxy headers */ - trustProxy?: boolean; - /** CORS origin configuration */ - origin?: string; - /** Whether to enforce strict authentication */ - strictAuth?: boolean; -} - -/** - * Storage provider type for CDN/bucket operations - */ -export type BucketProvider = 's3' | 'minio' | 'gcs'; - -/** - * CDN and file storage configuration - */ -export interface CDNOptions { - /** Storage provider type (s3, minio, gcs). Defaults to 'minio' for local dev */ - provider?: BucketProvider; - /** S3 bucket name for file storage */ - bucketName?: string; - /** AWS region for S3 bucket */ - awsRegion?: string; - /** AWS access key for S3 */ - awsAccessKey?: string; - /** AWS secret key for S3 */ - awsSecretKey?: string; - /** S3-compatible API endpoint URL (MinIO, R2, DO Spaces, GCS, etc.) */ - endpoint?: string; - /** Public URL prefix for generating download URLs (e.g., CDN domain, S3 public URL) */ - publicUrlPrefix?: string; -} - -/** - * SMTP email configuration options - */ -export interface SmtpOptions { - /** SMTP server hostname */ - host?: string; - /** SMTP server port (defaults to 587 for non-secure, 465 for secure) */ - port?: number; - /** Use TLS/SSL connection (defaults based on port: true for 465, false otherwise) */ - secure?: boolean; - /** SMTP authentication username */ - user?: string; - /** SMTP authentication password */ - pass?: string; - /** Default sender email address */ - from?: string; - /** Default reply-to email address */ - replyTo?: string; - /** Require TLS upgrade via STARTTLS */ - requireTLS?: boolean; - /** Reject unauthorized TLS certificates */ - tlsRejectUnauthorized?: boolean; - /** Use connection pooling for multiple emails */ - pool?: boolean; - /** Maximum number of pooled connections */ - maxConnections?: number; - /** Maximum messages per connection before reconnecting */ - maxMessages?: number; - /** SMTP client hostname for EHLO/HELO */ - name?: string; - /** Enable nodemailer logging */ - logger?: boolean; - /** Enable nodemailer debug output */ - debug?: boolean; -} - /** * Code generation settings */ @@ -254,25 +176,18 @@ export interface DeploymentOptions { * Main configuration options for the PGPM framework * Note: GraphQL/Graphile options (graphile, api, features) are in @constructive-io/graphql-types */ -export interface PgpmOptions { +export interface PgpmOptions + extends Partial> { /** Test database configuration options */ db?: Partial; /** PostgreSQL connection configuration */ pg?: Partial; - /** HTTP server configuration */ - server?: ServerOptions; - /** CDN and file storage configuration */ - cdn?: CDNOptions; /** Module deployment configuration */ deployment?: DeploymentOptions; /** Migration and code generation options */ migrations?: MigrationOptions; - /** Job system configuration */ - jobs?: JobsConfig; /** Error output formatting options */ errorOutput?: ErrorOutputOptions; - /** SMTP email configuration */ - smtp?: SmtpOptions; /** * Pluggable migration backend. Undefined = built-in `pg` (server) path. * Set `driver.plugin` to a package (e.g. `@pgpmjs/pglite-adapter`) resolved @@ -315,21 +230,6 @@ export const pgpmDefaults: PgpmOptions = { password: 'password', database: 'postgres' }, - server: { - host: 'localhost', - port: 3000, - trustProxy: false, - strictAuth: false - }, - cdn: { - provider: 'minio', - bucketName: 'test-bucket', - awsRegion: 'us-east-1', - awsAccessKey: 'minioadmin', - awsSecretKey: 'minioadmin', - endpoint: 'http://localhost:9000', - publicUrlPrefix: 'http://localhost:9000' - }, deployment: { useTx: true, fast: false, @@ -343,39 +243,11 @@ export const pgpmDefaults: PgpmOptions = { useTx: false } }, - jobs: { - schema: { - schema: 'app_jobs' - }, - worker: { - schema: 'app_jobs', - hostname: 'worker-0', - supportAny: true, - supported: [], - pollInterval: 1000, - gracefulShutdown: true - }, - scheduler: { - schema: 'app_jobs', - hostname: 'scheduler-0', - supportAny: true, - supported: [], - pollInterval: 1000, - gracefulShutdown: true - } - }, errorOutput: { queryHistoryLimit: 30, maxLength: 10000, verbose: false }, - smtp: { - port: 587, - secure: false, - pool: false, - logger: false, - debug: false - } }; export function getGitConfigInfo(): { username: string; email: string } { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1937c93cfd..aedf9ffe42 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1691,9 +1691,6 @@ importers: '@graphile-contrib/pg-many-to-many': specifier: 2.0.0-rc.2 version: 2.0.0-rc.2 - '@pgpmjs/env': - specifier: workspace:^ - version: link:../../pgpm/env/dist '@pgpmjs/logger': specifier: workspace:^ version: link:../../pgpm/logger/dist @@ -2064,15 +2061,18 @@ importers: jobs/job-utils: dependencies: + '@constructive-io/graphql-env': + specifier: workspace:^ + version: link:../../graphql/env/dist + '@constructive-io/graphql-types': + specifier: workspace:^ + version: link:../../graphql/types/dist '@pgpmjs/env': specifier: workspace:^ version: link:../../pgpm/env/dist '@pgpmjs/logger': specifier: workspace:^ version: link:../../pgpm/logger/dist - '@pgpmjs/types': - specifier: workspace:^ - version: link:../../pgpm/types/dist pg-cache: specifier: workspace:^ version: link:../../postgres/pg-cache/dist @@ -2659,12 +2659,12 @@ importers: packages/smtppostmaster: dependencies: - '@pgpmjs/env': + '@constructive-io/graphql-env': specifier: workspace:^ - version: link:../../pgpm/env/dist - '@pgpmjs/types': + version: link:../../graphql/env/dist + '@constructive-io/graphql-types': specifier: workspace:^ - version: link:../../pgpm/types/dist + version: link:../../graphql/types/dist nodemailer: specifier: ^6.9.13 version: 6.10.1 @@ -3567,16 +3567,16 @@ importers: '@constructive-io/content-type-stream': specifier: workspace:^ version: link:../content-type-stream/dist + '@constructive-io/graphql-types': + specifier: workspace:^ + version: link:../../graphql/types/dist '@constructive-io/s3-utils': specifier: workspace:^ version: link:../s3-utils/dist - '@pgpmjs/types': - specifier: workspace:^ - version: link:../../pgpm/types/dist devDependencies: - '@pgpmjs/env': + '@constructive-io/graphql-env': specifier: workspace:^ - version: link:../../pgpm/env/dist + version: link:../../graphql/env/dist glob: specifier: ^13.0.6 version: 13.0.6 diff --git a/uploads/s3-streamer/__tests__/uploads.test.ts b/uploads/s3-streamer/__tests__/uploads.test.ts index bce7520ac1..98273dbea7 100644 --- a/uploads/s3-streamer/__tests__/uploads.test.ts +++ b/uploads/s3-streamer/__tests__/uploads.test.ts @@ -1,5 +1,5 @@ import { S3Client } from '@aws-sdk/client-s3'; -import { getEnvOptions } from '@pgpmjs/env'; +import { getEnvOptions } from '@constructive-io/graphql-env'; import { createS3Bucket } from '@constructive-io/s3-utils'; import { createReadStream } from 'fs'; import { sync as glob } from 'glob'; diff --git a/uploads/s3-streamer/package.json b/uploads/s3-streamer/package.json index 9123c6c9c4..e4d1cc4b1f 100644 --- a/uploads/s3-streamer/package.json +++ b/uploads/s3-streamer/package.json @@ -29,7 +29,7 @@ "test:watch": "jest --watch" }, "devDependencies": { - "@pgpmjs/env": "workspace:^", + "@constructive-io/graphql-env": "workspace:^", "glob": "^13.0.6", "makage": "^0.3.0" }, @@ -37,8 +37,8 @@ "@aws-sdk/client-s3": "^3.1052.0", "@aws-sdk/lib-storage": "^3.1052.0", "@constructive-io/content-type-stream": "workspace:^", - "@constructive-io/s3-utils": "workspace:^", - "@pgpmjs/types": "workspace:^" + "@constructive-io/graphql-types": "workspace:^", + "@constructive-io/s3-utils": "workspace:^" }, "keywords": [ "s3", diff --git a/uploads/s3-streamer/src/s3.ts b/uploads/s3-streamer/src/s3.ts index 8948d886aa..d274a43fb8 100644 --- a/uploads/s3-streamer/src/s3.ts +++ b/uploads/s3-streamer/src/s3.ts @@ -1,7 +1,7 @@ import { createS3Client } from '@constructive-io/s3-utils'; import type { StorageProvider } from '@constructive-io/s3-utils'; +import type { BucketProvider } from '@constructive-io/graphql-types'; import type { S3Client } from '@aws-sdk/client-s3'; -import type { BucketProvider } from '@pgpmjs/types'; interface S3Options { awsAccessKey: string; diff --git a/uploads/s3-streamer/src/streamer.ts b/uploads/s3-streamer/src/streamer.ts index 3ce5059d78..6cc8a5eb34 100644 --- a/uploads/s3-streamer/src/streamer.ts +++ b/uploads/s3-streamer/src/streamer.ts @@ -1,6 +1,6 @@ import { S3Client } from '@aws-sdk/client-s3'; import { streamContentType } from '@constructive-io/content-type-stream'; -import type { BucketProvider } from '@pgpmjs/types'; +import type { BucketProvider } from '@constructive-io/graphql-types'; import type { Readable } from 'stream'; import getS3 from './s3';