Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 64 additions & 11 deletions graphql/env/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@
<a href="https://www.npmjs.com/package/@constructive-io/graphql-env"><img height="20" src="https://img.shields.io/github/package-json/v/constructive-io/constructive?filename=graphql%2Fenv%2Fpackage.json"/></a>
</p>

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

Expand All @@ -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
Expand All @@ -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
{
Expand All @@ -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.
15 changes: 9 additions & 6 deletions graphql/env/__tests__/__snapshots__/merge.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
Expand Down Expand Up @@ -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,
Expand All @@ -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,
},
}
Expand Down
118 changes: 118 additions & 0 deletions graphql/env/__tests__/environment.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
Loading