Skip to content

Commit 7a0901c

Browse files
authored
Merge pull request #57 from constructive-io/devin/1773716478-env-and-testing-skills
Add constructive-env and constructive-testing skills
2 parents a4b75a4 + 3a234a4 commit 7a0901c

9 files changed

Lines changed: 947 additions & 0 deletions

File tree

skills/constructive-env/SKILL.md

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
---
2+
name: constructive-env
3+
description: Unified environment configuration for Constructive and PGPM projects — typed defaults, env var parsing, config file discovery, and hierarchical option merging. Use when asked to "configure environment", "set env vars", "manage defaults", "use getEnvOptions", "configure database connection", "set up server config", "configure SMTP", "configure CDN", "manage configuration hierarchy", or when working with @pgpmjs/env or @constructive-io/graphql-env.
4+
compatibility: Node.js 18+, @pgpmjs/env, @constructive-io/graphql-env
5+
metadata:
6+
author: constructive-io
7+
version: "1.0.0"
8+
---
9+
10+
# Constructive Environment Configuration
11+
12+
Unified, type-safe environment configuration for all Constructive and PGPM projects. Two packages work together to provide a clean, hierarchical configuration system with sensible defaults.
13+
14+
## When to Apply
15+
16+
Use this skill when:
17+
- Configuring database connections, server settings, CDN, SMTP, or jobs programmatically
18+
- Understanding how environment variables map to typed configuration objects
19+
- Writing code that needs to read environment-aware configuration
20+
- Choosing between `@pgpmjs/env` and `@constructive-io/graphql-env`
21+
- Understanding the merge hierarchy (defaults → env vars → runtime overrides)
22+
23+
## Architecture Overview
24+
25+
Two packages form a layered configuration system:
26+
27+
```
28+
@pgpmjs/types → PgpmOptions interface + pgpmDefaults
29+
30+
@pgpmjs/env → getEnvVars() + loadConfigSync() + getEnvOptions()
31+
↓ (extends)
32+
@constructive-io/graphql-types → ConstructiveOptions interface + constructiveDefaults
33+
34+
@constructive-io/graphql-env → getGraphQLEnvVars() + getEnvOptions()
35+
```
36+
37+
### Which Package to Import
38+
39+
| You're working in... | Import from |
40+
|----------------------|-------------|
41+
| PGPM packages (`pgpm/*`) | `@pgpmjs/env` |
42+
| PostgreSQL tools (`postgres/*`) | `@pgpmjs/env` or `pg-env` |
43+
| GraphQL server, explorer, codegen | `@constructive-io/graphql-env` |
44+
| Constructive CLI commands | `@constructive-io/graphql-env` |
45+
| Job functions, SMTP | `@pgpmjs/env` |
46+
| Tests (pgsql-test based) | `@pgpmjs/env` (via `getConnEnvOptions`) |
47+
48+
**Rule of thumb:** If your code needs GraphQL/Graphile/API options, use `@constructive-io/graphql-env`. Otherwise, use `@pgpmjs/env`.
49+
50+
## Merge Hierarchy
51+
52+
Options are merged in this order (later overrides earlier):
53+
54+
1. **Defaults**`pgpmDefaults` (+ `constructiveGraphqlDefaults` for GraphQL layer)
55+
2. **Config file**`pgpm.json` or `pgpm.config.js` (rarely needed — see [references/config-file.md](references/config-file.md))
56+
3. **Environment variables** — parsed from `process.env`
57+
4. **Runtime overrides** — passed programmatically to `getEnvOptions()`
58+
59+
In practice, most projects only use layers 1, 3, and 4. Config files are a last resort for shared workspace settings.
60+
61+
Arrays are **replaced**, not concatenated — the later source wins completely.
62+
63+
## Quick Start
64+
65+
### Core PGPM Options
66+
67+
```typescript
68+
import { getEnvOptions } from '@pgpmjs/env';
69+
70+
// Get fully merged options (defaults + config + env + overrides)
71+
const opts = getEnvOptions();
72+
73+
// With runtime overrides
74+
const opts = getEnvOptions({
75+
pg: { database: 'mydb' },
76+
deployment: { fast: true }
77+
});
78+
79+
// With custom working directory and env object
80+
const opts = getEnvOptions({}, '/path/to/project', process.env);
81+
```
82+
83+
### Constructive Options (includes GraphQL)
84+
85+
```typescript
86+
import { getEnvOptions } from '@constructive-io/graphql-env';
87+
88+
// Gets everything: PGPM defaults + GraphQL defaults + config + env + overrides
89+
const opts = getEnvOptions({
90+
pg: { database: 'constructive' },
91+
api: { isPublic: true }
92+
});
93+
```
94+
95+
### Specialized Accessors
96+
97+
```typescript
98+
import { getConnEnvOptions, getDeploymentEnvOptions } from '@pgpmjs/env';
99+
100+
// Database connection options with roles resolved
101+
const connOpts = getConnEnvOptions({ prefix: 'test-' });
102+
103+
// Deployment options only
104+
const deployOpts = getDeploymentEnvOptions({ fast: true });
105+
```
106+
107+
## Type System
108+
109+
### PgpmOptions (core)
110+
111+
```typescript
112+
interface PgpmOptions {
113+
db?: Partial<PgTestConnectionOptions>; // DB config, roles, connections
114+
pg?: Partial<PgConfig>; // PostgreSQL connection (host, port, user, etc.)
115+
server?: ServerOptions; // HTTP server (host, port, trustProxy, origin)
116+
cdn?: CDNOptions; // S3/MinIO file storage
117+
deployment?: DeploymentOptions; // Migration deployment settings
118+
migrations?: MigrationOptions; // Code generation settings
119+
jobs?: JobsConfig; // Job worker/scheduler config
120+
errorOutput?: ErrorOutputOptions; // Error formatting
121+
smtp?: SmtpOptions; // Email configuration
122+
}
123+
```
124+
125+
### ConstructiveOptions (extends PgpmOptions)
126+
127+
```typescript
128+
interface ConstructiveOptions extends PgpmOptions {
129+
graphile?: GraphileOptions; // PostGraphile schema config
130+
features?: GraphileFeatureOptions; // Feature flags (inflection, PostGIS)
131+
api?: ApiOptions; // API routing (public/admin, roles, schemas)
132+
}
133+
```
134+
135+
## Utility Functions
136+
137+
### Env Var Parsers
138+
139+
```typescript
140+
import { parseEnvBoolean, parseEnvNumber } from '@pgpmjs/env';
141+
142+
parseEnvBoolean('true'); // true
143+
parseEnvBoolean('1'); // true
144+
parseEnvBoolean('yes'); // true
145+
parseEnvBoolean('false'); // false
146+
parseEnvBoolean(undefined); // undefined
147+
148+
parseEnvNumber('5432'); // 5432
149+
parseEnvNumber('invalid'); // undefined
150+
```
151+
152+
### Node Environment
153+
154+
```typescript
155+
import { getNodeEnv } from '@pgpmjs/env';
156+
157+
const env = getNodeEnv(); // 'development' | 'production' | 'test'
158+
```
159+
160+
## Anti-Patterns
161+
162+
**Never do this:**
163+
164+
```typescript
165+
// BAD: Manual env var access scattered throughout code
166+
const host = process.env.PGHOST || 'localhost';
167+
const port = parseInt(process.env.PGPORT || '5432');
168+
const usePublic = process.env.API_IS_PUBLIC === 'true';
169+
```
170+
171+
**Do this instead:**
172+
173+
```typescript
174+
// GOOD: Use the unified configuration system
175+
import { getEnvOptions } from '@pgpmjs/env';
176+
const opts = getEnvOptions();
177+
const { host, port } = opts.pg;
178+
```
179+
180+
```typescript
181+
// GOOD: For Constructive/GraphQL packages
182+
import { getEnvOptions } from '@constructive-io/graphql-env';
183+
const opts = getEnvOptions();
184+
const { isPublic } = opts.api;
185+
```
186+
187+
## Reference Guide
188+
189+
| Reference | Topic | Consult When |
190+
|-----------|-------|--------------|
191+
| [references/env-vars.md](references/env-vars.md) | Source file locations for env vars and types | Finding which source file defines a specific env var or type |
192+
| [references/defaults.md](references/defaults.md) | Default values for all configuration | Understanding what values are used when nothing is overridden |
193+
| [references/config-file.md](references/config-file.md) | Config file reference (last resort) | Only when you specifically need a `pgpm.json` — this is rarely needed |
194+
195+
## Cross-References
196+
197+
- `pgpm` skill (`references/env.md`) — The `pgpm env` CLI command for shell-level env var management
198+
- `constructive-server-config` skill — Server-specific env vars and startup configuration
199+
- `constructive-deployment` skill — Deployment-specific configuration
200+
- `constructive-functions` skill — Using `parseEnvBoolean` in Knative functions
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Config File Reference (Advanced / Last Resort)
2+
3+
Config files (`pgpm.json` / `pgpm.config.js`) are supported but **should rarely be needed**. The preferred approach is to use environment variables and runtime overrides via `getEnvOptions()`.
4+
5+
Config files exist primarily for workspaces that need shared base settings across many packages (e.g., a monorepo where every package shares the same DB extensions list).
6+
7+
## When You Might Need a Config File
8+
9+
- You have a large workspace where many packages share the same non-default settings
10+
- You need dynamic configuration that depends on runtime logic (`pgpm.config.js` only)
11+
- You're working with `pgpm init workspace` which generates one automatically
12+
13+
**In most cases, env vars + `getEnvOptions()` overrides are sufficient.**
14+
15+
## How It Works
16+
17+
`loadConfigSync(cwd)` walks up the directory tree from `cwd` looking for `pgpm.config.js` (checked first) or `pgpm.json`. The first found is used. If none is found, an empty object is returned and defaults still apply.
18+
19+
Config file values sit in the merge hierarchy between defaults and env vars:
20+
21+
```
22+
defaults → config file → env vars → runtime overrides
23+
```
24+
25+
Env vars always override config file values.
26+
27+
## Source
28+
29+
- **Config loading logic:** `pgpm/env/src/config.ts`
30+
- **Workspace resolution:** `pgpm/env/src/workspace.ts`
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# Default Configuration Values
2+
3+
These defaults are applied as the base layer before config file, env vars, and runtime overrides.
4+
5+
## PGPM Core Defaults (`pgpmDefaults`)
6+
7+
Source: `@pgpmjs/types`
8+
9+
The core PGPM defaults provide base values for database connection, server, and deployment configuration. These are used by `@pgpmjs/env`'s `getEnvOptions()`.
10+
11+
Key defaults include:
12+
13+
- **`db.rootDb`**: `'postgres'`
14+
- **`db.prefix`**: `'test-db-'`
15+
- **`db.extensions`**: `['plpgsql', 'uuid-ossp']`
16+
- **`db.connections.app`**: Default app-level user/password
17+
- **`db.connections.admin`**: Default admin user/password
18+
- **`db.roles`**: Default role definitions
19+
- **`server.port`**: `3000`
20+
- **`server.host`**: `'localhost'`
21+
- **`deployment.useTx`**: `true`
22+
- **`deployment.fast`**: `false`
23+
- **`deployment.usePlan`**: `true`
24+
- **`deployment.cache`**: `false`
25+
26+
## GraphQL Defaults (`constructiveGraphqlDefaults`)
27+
28+
Source: `@constructive-io/graphql-types`
29+
30+
### Graphile Defaults (`graphileDefaults`)
31+
32+
```typescript
33+
{
34+
schema: [], // No schemas exposed by default
35+
extends: [], // No preset extensions
36+
preset: {}, // No preset overrides
37+
}
38+
```
39+
40+
### Feature Defaults (`graphileFeatureDefaults`)
41+
42+
```typescript
43+
{
44+
simpleInflection: true, // Use PgSimplifyInflection
45+
oppositeBaseNames: true, // Use opposite base names for relations
46+
postgis: true, // Enable PostGIS support
47+
}
48+
```
49+
50+
### API Defaults (`apiDefaults`)
51+
52+
```typescript
53+
{
54+
enableServicesApi: true, // Services API routing enabled
55+
exposedSchemas: [], // No schemas exposed by default
56+
anonRole: 'administrator', // Default anonymous role
57+
roleName: 'administrator', // Default authenticated role
58+
defaultDatabaseId: 'hard-coded', // Default database ID
59+
isPublic: true, // Public API mode (domain routing)
60+
metaSchemas: [
61+
'services_public',
62+
'metaschema_public',
63+
'metaschema_modules_public',
64+
],
65+
}
66+
```
67+
68+
## Combined Defaults (`constructiveDefaults`)
69+
70+
When using `@constructive-io/graphql-env`, the full defaults are a deep merge of:
71+
72+
```typescript
73+
constructiveDefaults = deepmerge.all([
74+
pgpmDefaults,
75+
constructiveGraphqlDefaults // { graphile, features, api }
76+
]);
77+
```
78+
79+
This means all PGPM defaults are preserved and the GraphQL-specific defaults are layered on top.
80+
81+
## How Defaults Are Applied
82+
83+
Defaults form the first layer in the merge hierarchy:
84+
85+
```
86+
defaults → config file → env vars → runtime overrides
87+
```
88+
89+
If you set a value in a config file, it overrides the default. If you set an env var, it overrides both the default and the config file. Runtime overrides win over everything.
90+
91+
**Array behavior**: Arrays are **replaced**, not concatenated. If the default `db.extensions` is `['plpgsql', 'uuid-ossp']` and you set `DB_EXTENSIONS=postgis`, the result is `['postgis']` — the default array is fully replaced.
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Environment Variables Reference
2+
3+
The canonical source of truth for all recognized environment variables lives in the source code. Do not maintain a separate list — always refer to these files directly.
4+
5+
## Source Files
6+
7+
### Core PGPM Env Vars
8+
9+
**File:** `pgpm/env/src/env.ts` in the `constructive` repo
10+
11+
This file defines `getEnvVars()` which maps `process.env` keys to the `PgpmOptions` type. Every env var the system recognizes is defined here via conditional spreads:
12+
13+
```typescript
14+
// Pattern used throughout env.ts:
15+
...(process.env.PGHOST && { host: process.env.PGHOST }),
16+
...(process.env.PGPORT && { port: parseEnvNumber(process.env.PGPORT) }),
17+
```
18+
19+
Covers: `PGHOST`, `PGPORT`, `PGUSER`, `PGPASSWORD`, `PGDATABASE`, `DB_*`, `SERVER_*`, `PORT`, `BUCKET_*`, `AWS_*`, `MINIO_*`, `DEPLOYMENT_*`, `JOBS_*`, `SMTP_*`, `PGPM_ERROR_*`
20+
21+
### GraphQL-Specific Env Vars
22+
23+
**File:** `graphql/env/src/env.ts` in the `constructive` repo
24+
25+
This file defines `getGraphQLEnvVars()` which adds GraphQL/Constructive-specific env vars on top of the core PGPM set.
26+
27+
Covers: `GRAPHILE_SCHEMA`, `FEATURES_*`, `API_*`
28+
29+
## Type Definitions
30+
31+
The TypeScript interfaces that define the shape of these options:
32+
33+
- **`PgpmOptions`**`pgpm/types/src/options.ts`
34+
- **`ConstructiveOptions`**`graphql/types/src/constructive.ts`
35+
- **`GraphileOptions`**`graphql/types/src/graphile.ts`
36+
- **`PgConfig`**`pgpm/types/src/pg.ts`
37+
38+
## Type Parsing
39+
40+
The env parsing uses three type-safe parsers (defined in `pgpm/env/src/env.ts`):
41+
42+
- **`parseEnvBoolean(val)`** — Accepts `'true'`, `'1'`, `'yes'` (case-insensitive) as `true`
43+
- **`parseEnvNumber(val)`** — Uses `Number()`, returns `undefined` if `NaN`
44+
- **`parseEnvStringArray(val)`** — Splits on comma, trims whitespace, filters empty strings
45+
46+
If an env var is not set, that key is omitted entirely from the options object — it does not get set to `undefined`. This allows the merge hierarchy to fall through to defaults or config file values.

0 commit comments

Comments
 (0)