Skip to content

Commit 41688e4

Browse files
committed
Add constructive-env and constructive-testing skills, update pgpm env reference
- New skill: constructive-env — documents the full two-layer env configuration system (@pgpmjs/env + @constructive-io/graphql-env), including all env vars, defaults, config file discovery, and which package to import when - New skill: constructive-testing — documents the testing framework hierarchy (pgsql-test → graphile-test → graphql-test → server-test), anti-patterns (manual pg.Pool creation, skipping hooks), and correct usage patterns - Updated pgpm/references/environment-configuration.md to cross-reference the new constructive-env skill
1 parent a4b75a4 commit 41688e4

8 files changed

Lines changed: 1329 additions & 0 deletions

File tree

skills/constructive-env/SKILL.md

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
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+
- Setting up `pgpm.json` config files
20+
- Writing code that needs to read environment-aware configuration
21+
- Choosing between `@pgpmjs/env` and `@constructive-io/graphql-env`
22+
- Understanding the merge hierarchy (defaults → config file → env vars → overrides)
23+
24+
## Architecture Overview
25+
26+
Two packages form a layered configuration system:
27+
28+
```
29+
@pgpmjs/types → PgpmOptions interface + pgpmDefaults
30+
31+
@pgpmjs/env → getEnvVars() + loadConfigSync() + getEnvOptions()
32+
↓ (extends)
33+
@constructive-io/graphql-types → ConstructiveOptions interface + constructiveDefaults
34+
35+
@constructive-io/graphql-env → getGraphQLEnvVars() + getEnvOptions()
36+
```
37+
38+
### Which Package to Import
39+
40+
| You're working in... | Import from |
41+
|----------------------|-------------|
42+
| PGPM packages (`pgpm/*`) | `@pgpmjs/env` |
43+
| PostgreSQL tools (`postgres/*`) | `@pgpmjs/env` or `pg-env` |
44+
| GraphQL server, explorer, codegen | `@constructive-io/graphql-env` |
45+
| Constructive CLI commands | `@constructive-io/graphql-env` |
46+
| Job functions, SMTP | `@pgpmjs/env` |
47+
| Tests (pgsql-test based) | `@pgpmjs/env` (via `getConnEnvOptions`) |
48+
49+
**Rule of thumb:** If your code needs GraphQL/Graphile/API options, use `@constructive-io/graphql-env`. Otherwise, use `@pgpmjs/env`.
50+
51+
## Merge Hierarchy
52+
53+
Options are merged in this order (later overrides earlier):
54+
55+
1. **Defaults**`pgpmDefaults` (+ `constructiveGraphqlDefaults` for GraphQL layer)
56+
2. **Config file**`pgpm.json` or `pgpm.config.js` discovered by walking up directories
57+
3. **Environment variables** — parsed from `process.env`
58+
4. **Runtime overrides** — passed programmatically to `getEnvOptions()`
59+
60+
Arrays are **replaced**, not concatenated — the later source wins completely.
61+
62+
## Quick Start
63+
64+
### Core PGPM Options
65+
66+
```typescript
67+
import { getEnvOptions } from '@pgpmjs/env';
68+
69+
// Get fully merged options (defaults + config + env + overrides)
70+
const opts = getEnvOptions();
71+
72+
// With runtime overrides
73+
const opts = getEnvOptions({
74+
pg: { database: 'mydb' },
75+
deployment: { fast: true }
76+
});
77+
78+
// With custom working directory and env object
79+
const opts = getEnvOptions({}, '/path/to/project', process.env);
80+
```
81+
82+
### Constructive Options (includes GraphQL)
83+
84+
```typescript
85+
import { getEnvOptions } from '@constructive-io/graphql-env';
86+
87+
// Gets everything: PGPM defaults + GraphQL defaults + config + env + overrides
88+
const opts = getEnvOptions({
89+
pg: { database: 'constructive' },
90+
api: { isPublic: true }
91+
});
92+
```
93+
94+
### Specialized Accessors
95+
96+
```typescript
97+
import { getConnEnvOptions, getDeploymentEnvOptions } from '@pgpmjs/env';
98+
99+
// Database connection options with roles resolved
100+
const connOpts = getConnEnvOptions({ prefix: 'test-' });
101+
102+
// Deployment options only
103+
const deployOpts = getDeploymentEnvOptions({ fast: true });
104+
```
105+
106+
## Type System
107+
108+
### PgpmOptions (core)
109+
110+
```typescript
111+
interface PgpmOptions {
112+
db?: Partial<PgTestConnectionOptions>; // DB config, roles, connections
113+
pg?: Partial<PgConfig>; // PostgreSQL connection (host, port, user, etc.)
114+
server?: ServerOptions; // HTTP server (host, port, trustProxy, origin)
115+
cdn?: CDNOptions; // S3/MinIO file storage
116+
deployment?: DeploymentOptions; // Migration deployment settings
117+
migrations?: MigrationOptions; // Code generation settings
118+
jobs?: JobsConfig; // Job worker/scheduler config
119+
errorOutput?: ErrorOutputOptions; // Error formatting
120+
smtp?: SmtpOptions; // Email configuration
121+
}
122+
```
123+
124+
### ConstructiveOptions (extends PgpmOptions)
125+
126+
```typescript
127+
interface ConstructiveOptions extends PgpmOptions {
128+
graphile?: GraphileOptions; // PostGraphile schema config
129+
features?: GraphileFeatureOptions; // Feature flags (inflection, PostGIS)
130+
api?: ApiOptions; // API routing (public/admin, roles, schemas)
131+
}
132+
```
133+
134+
## Utility Functions
135+
136+
### Env Var Parsers
137+
138+
```typescript
139+
import { parseEnvBoolean, parseEnvNumber } from '@pgpmjs/env';
140+
141+
parseEnvBoolean('true'); // true
142+
parseEnvBoolean('1'); // true
143+
parseEnvBoolean('yes'); // true
144+
parseEnvBoolean('false'); // false
145+
parseEnvBoolean(undefined); // undefined
146+
147+
parseEnvNumber('5432'); // 5432
148+
parseEnvNumber('invalid'); // undefined
149+
```
150+
151+
### Node Environment
152+
153+
```typescript
154+
import { getNodeEnv } from '@pgpmjs/env';
155+
156+
const env = getNodeEnv(); // 'development' | 'production' | 'test'
157+
```
158+
159+
### Config File Discovery
160+
161+
```typescript
162+
import { loadConfigSync, resolvePgpmPath } from '@pgpmjs/env';
163+
164+
// Load pgpm.json by walking up directory tree
165+
const config = loadConfigSync('/path/to/project');
166+
167+
// Find the pgpm config file path
168+
const pgpmPath = resolvePgpmPath('/path/to/project');
169+
```
170+
171+
## Anti-Patterns
172+
173+
**Never do this:**
174+
175+
```typescript
176+
// BAD: Manual env var access scattered throughout code
177+
const host = process.env.PGHOST || 'localhost';
178+
const port = parseInt(process.env.PGPORT || '5432');
179+
const usePublic = process.env.API_IS_PUBLIC === 'true';
180+
```
181+
182+
**Do this instead:**
183+
184+
```typescript
185+
// GOOD: Use the unified configuration system
186+
import { getEnvOptions } from '@pgpmjs/env';
187+
const opts = getEnvOptions();
188+
const { host, port } = opts.pg;
189+
```
190+
191+
```typescript
192+
// GOOD: For Constructive/GraphQL packages
193+
import { getEnvOptions } from '@constructive-io/graphql-env';
194+
const opts = getEnvOptions();
195+
const { isPublic } = opts.api;
196+
```
197+
198+
## Reference Guide
199+
200+
| Reference | Topic | Consult When |
201+
|-----------|-------|--------------|
202+
| [references/env-vars.md](references/env-vars.md) | Complete environment variables reference | Looking up specific env var names, defaults, and which config key they map to |
203+
| [references/defaults.md](references/defaults.md) | Default values for all configuration | Understanding what values are used when nothing is overridden |
204+
| [references/config-file.md](references/config-file.md) | pgpm.json configuration | Setting up project-level configuration files |
205+
206+
## Cross-References
207+
208+
- `pgpm` skill (`references/env.md`) — The `pgpm env` CLI command for shell-level env var management
209+
- `constructive-server-config` skill — Server-specific env vars and startup configuration
210+
- `constructive-deployment` skill — Deployment-specific configuration
211+
- `constructive-functions` skill — Using `parseEnvBoolean` in Knative functions
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
# Configuration File Reference
2+
3+
PGPM and Constructive support project-level configuration via `pgpm.json` or `pgpm.config.js` files.
4+
5+
## Config File Discovery
6+
7+
The system walks up the directory tree from the current working directory looking for:
8+
9+
1. `pgpm.config.js` (checked first — supports dynamic config)
10+
2. `pgpm.json` (static JSON config)
11+
12+
The first file found is used. If no config file is found, an empty object is returned (defaults still apply).
13+
14+
```
15+
project/
16+
├── packages/
17+
│ └── my-module/ ← cwd
18+
│ └── ...
19+
├── pgpm.json ← found by walking up
20+
└── ...
21+
```
22+
23+
## pgpm.json Format
24+
25+
```json
26+
{
27+
"pg": {
28+
"host": "localhost",
29+
"port": 5432,
30+
"user": "postgres",
31+
"password": "postgres",
32+
"database": "myproject"
33+
},
34+
"db": {
35+
"extensions": ["plpgsql", "uuid-ossp", "postgis"],
36+
"prefix": "test-"
37+
},
38+
"server": {
39+
"port": 3000,
40+
"host": "0.0.0.0"
41+
},
42+
"deployment": {
43+
"fast": true,
44+
"useTx": true
45+
},
46+
"cdn": {
47+
"provider": "minio",
48+
"bucketName": "uploads",
49+
"minioEndpoint": "http://localhost:9000"
50+
},
51+
"graphile": {
52+
"schema": ["app_public", "app_private"]
53+
},
54+
"features": {
55+
"simpleInflection": true,
56+
"postgis": false
57+
},
58+
"api": {
59+
"isPublic": true,
60+
"exposedSchemas": ["app_public"],
61+
"anonRole": "anonymous"
62+
}
63+
}
64+
```
65+
66+
## pgpm.config.js Format
67+
68+
For dynamic configuration:
69+
70+
```javascript
71+
module.exports = {
72+
pg: {
73+
host: process.env.CUSTOM_HOST || 'localhost',
74+
port: 5432,
75+
},
76+
deployment: {
77+
fast: process.env.NODE_ENV === 'development',
78+
}
79+
};
80+
```
81+
82+
Or with ES module default export:
83+
84+
```javascript
85+
export default {
86+
pg: {
87+
database: 'myproject',
88+
},
89+
};
90+
```
91+
92+
## Workspace Resolution
93+
94+
The config system can also resolve workspace roots. These functions walk up the directory tree looking for workspace markers:
95+
96+
| Function | Looks For | Use Case |
97+
|----------|-----------|----------|
98+
| `resolvePgpmPath()` | `pgpm.config.js` or `pgpm.json` | Find PGPM workspace root |
99+
| `resolvePnpmWorkspace()` | `pnpm-workspace.yaml` | Find pnpm workspace root |
100+
| `resolveLernaWorkspace()` | `lerna.json` | Find Lerna workspace root |
101+
| `resolveNpmWorkspace()` | `package.json` with `workspaces` field | Find npm workspace root |
102+
| `resolveWorkspaceByType()` | Dispatches to the above based on type | Generic workspace resolution |
103+
104+
```typescript
105+
import { resolvePgpmPath, resolveWorkspaceByType } from '@pgpmjs/env';
106+
107+
const pgpmRoot = resolvePgpmPath('/path/to/nested/dir');
108+
// Returns '/path/to' if pgpm.json exists there
109+
110+
const workspaceRoot = resolveWorkspaceByType('/path/to/dir', 'pnpm');
111+
// Returns the pnpm workspace root
112+
```
113+
114+
## Config File + Env Vars Interaction
115+
116+
Config file values override defaults but are overridden by environment variables:
117+
118+
```
119+
pgpmDefaults → { server: { port: 3000 } }
120+
pgpm.json → { server: { port: 4000 } } ← overrides default
121+
PORT=5000 → { server: { port: 5000 } } ← overrides config
122+
getEnvOptions({}) → { server: { port: 5000 } } ← final result
123+
```
124+
125+
For Constructive packages, GraphQL-specific keys in the config file (`graphile`, `features`, `api`) are also merged:
126+
127+
```
128+
pgpmDefaults → base
129+
constructiveGraphqlDefaults → graphile/features/api defaults
130+
pgpm.json (graphile keys) → overrides graphql defaults
131+
GRAPHILE_SCHEMA=... → overrides config
132+
getEnvOptions({}) → final result
133+
```

0 commit comments

Comments
 (0)