Skip to content

Commit 4b1cfd5

Browse files
Merge remote-tracking branch 'origin/devin/1775215835-bucket-provisioner-plugin' into devin/1775257329-cors-allowed-origins
2 parents a9ef022 + eb856fe commit 4b1cfd5

14 files changed

Lines changed: 2177 additions & 21 deletions

File tree

.github/workflows/run-tests.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,8 @@ jobs:
115115
env: {}
116116
- package: packages/upload-client
117117
env: {}
118+
- package: graphile/graphile-bucket-provisioner-plugin
119+
env: {}
118120

119121
env:
120122
PGHOST: localhost
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
# graphile-bucket-provisioner-plugin
2+
3+
<p align="center" width="100%">
4+
<img height="250" src="https://raw.githubusercontent.com/constructive-io/constructive/refs/heads/main/assets/outline-logo.svg" />
5+
</p>
6+
7+
<p align="center" width="100%">
8+
<a href="https://github.com/constructive-io/constructive/actions/workflows/run-tests.yaml">
9+
<img height="20" src="https://github.com/constructive-io/constructive/actions/workflows/run-tests.yaml/badge.svg" />
10+
</a>
11+
<a href="https://github.com/constructive-io/constructive/blob/main/LICENSE"><img height="20" src="https://img.shields.io/badge/license-MIT-blue.svg"/></a>
12+
<a href="https://www.npmjs.com/package/graphile-bucket-provisioner-plugin"><img height="20" src="https://img.shields.io/github/package-json/v/constructive-io/constructive?filename=graphile%2Fgraphile-bucket-provisioner-plugin%2Fpackage.json"/></a>
13+
</p>
14+
15+
PostGraphile v5 plugin that automatically provisions S3-compatible buckets when bucket rows are created in the database. Wraps bucket creation mutations to call [`@constructive-io/bucket-provisioner`](../packages/bucket-provisioner) after the database row is inserted.
16+
17+
## Features
18+
19+
- **Auto-provisioning hook** — Wraps `create*` mutations on tables tagged with `@storageBuckets` to automatically provision S3 buckets after row creation
20+
- **Explicit `provisionBucket` mutation** — GraphQL mutation for manual/retry provisioning of any bucket
21+
- **Per-database overrides** — Reads `endpoint`, `provider`, and `public_url_prefix` from the `storage_module` table for multi-tenant setups
22+
- **Lazy S3 config** — Connection config can be a function (evaluated once, cached) to avoid eager env-var reads at import time
23+
- **Graceful error handling** — Provisioning failures are logged but never fail the mutation (admin can retry via `provisionBucket`)
24+
- **Custom bucket naming** — Supports prefix-based naming or a fully custom `resolveBucketName` function
25+
26+
## Installation
27+
28+
```bash
29+
pnpm add graphile-bucket-provisioner-plugin
30+
```
31+
32+
## Quick Start
33+
34+
```typescript
35+
import { createBucketProvisionerPlugin } from 'graphile-bucket-provisioner-plugin';
36+
37+
const BucketProvisionerPlugin = createBucketProvisionerPlugin({
38+
connection: {
39+
provider: 'minio',
40+
region: 'us-east-1',
41+
endpoint: 'http://minio:9000',
42+
accessKeyId: process.env.S3_ACCESS_KEY_ID!,
43+
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY!,
44+
},
45+
allowedOrigins: ['https://app.example.com'],
46+
});
47+
48+
// Add to your PostGraphile preset
49+
const preset: GraphileConfig.Preset = {
50+
plugins: [BucketProvisionerPlugin],
51+
};
52+
```
53+
54+
Or use the convenience preset:
55+
56+
```typescript
57+
import { BucketProvisionerPreset } from 'graphile-bucket-provisioner-plugin';
58+
59+
const preset: GraphileConfig.Preset = {
60+
extends: [
61+
BucketProvisionerPreset({
62+
connection: () => ({
63+
provider: 'minio',
64+
region: 'us-east-1',
65+
endpoint: process.env.S3_ENDPOINT!,
66+
accessKeyId: process.env.S3_ACCESS_KEY_ID!,
67+
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY!,
68+
}),
69+
allowedOrigins: ['https://app.example.com'],
70+
}),
71+
],
72+
};
73+
```
74+
75+
## How It Works
76+
77+
### Auto-Provisioning (default)
78+
79+
When a `createBucket` mutation runs on a table tagged with `@storageBuckets`:
80+
81+
1. The original resolver runs first (creates the DB row via RLS)
82+
2. The plugin reads the bucket's `key` and `type` from the mutation input
83+
3. It reads the `storage_module` config for per-database endpoint/provider overrides
84+
4. It calls `BucketProvisioner.provision()` to create and configure the S3 bucket
85+
5. If provisioning fails, the error is logged but the mutation result is returned normally
86+
87+
### Explicit Mutation
88+
89+
The plugin also adds a `provisionBucket` mutation for manual provisioning or retrying failed provisions:
90+
91+
```graphql
92+
mutation {
93+
provisionBucket(input: { bucketKey: "public" }) {
94+
success
95+
bucketName
96+
accessType
97+
provider
98+
endpoint
99+
error
100+
}
101+
}
102+
```
103+
104+
This mutation:
105+
1. Reads the bucket row from the database (protected by RLS)
106+
2. Reads the storage module config for the current database
107+
3. Provisions the S3 bucket with the appropriate settings
108+
4. Returns a success/error payload
109+
110+
## API
111+
112+
### `createBucketProvisionerPlugin(options)`
113+
114+
Creates the plugin instance. Returns a `GraphileConfig.Plugin`.
115+
116+
| Option | Type | Description |
117+
|--------|------|-------------|
118+
| `connection` | `StorageConnectionConfig \| () => StorageConnectionConfig` | S3 connection config (static or lazy getter) |
119+
| `allowedOrigins` | `string[]` | CORS allowed origins for bucket configuration |
120+
| `bucketNamePrefix` | `string?` | Prefix for S3 bucket names (e.g., `"myapp"``"myapp-public"`) |
121+
| `resolveBucketName` | `(bucketKey, databaseId) => string` | Custom bucket name resolver (takes precedence over prefix) |
122+
| `versioning` | `boolean?` | Enable S3 versioning on provisioned buckets (default: `false`) |
123+
| `autoProvision` | `boolean?` | Enable auto-provisioning hook on create mutations (default: `true`) |
124+
125+
### `BucketProvisionerPreset(options)`
126+
127+
Convenience function that wraps the plugin in a `GraphileConfig.Preset`.
128+
129+
### Connection Config
130+
131+
```typescript
132+
interface StorageConnectionConfig {
133+
provider: 's3' | 'minio' | 'r2' | 'gcs' | 'spaces';
134+
region: string;
135+
endpoint?: string;
136+
accessKeyId: string;
137+
secretAccessKey: string;
138+
}
139+
```
140+
141+
### Smart Tag Detection
142+
143+
The plugin detects tables tagged with `@storageBuckets` (set by the storage module generator in constructive-db):
144+
145+
```sql
146+
COMMENT ON TABLE app_public.buckets IS E'@storageBuckets\nStorage buckets table';
147+
```
148+
149+
Only `create*` mutations on tagged tables trigger auto-provisioning. Update and delete mutations are not wrapped.
150+
151+
## Error Handling
152+
153+
The plugin is designed to never break mutations:
154+
155+
- **Auto-provisioning errors** are caught and logged. The mutation result is returned normally. The admin can retry via the `provisionBucket` mutation.
156+
- **Explicit `provisionBucket` errors** return a structured payload with `success: false` and an `error` message.
157+
- **Validation errors** (`INVALID_BUCKET_KEY`, `DATABASE_NOT_FOUND`, `STORAGE_MODULE_NOT_PROVISIONED`, `BUCKET_NOT_FOUND`) are thrown as exceptions since they indicate configuration issues.
158+
159+
## Multi-Tenant Support
160+
161+
The plugin reads per-database overrides from the `storage_module` table:
162+
163+
- `endpoint` — Override the S3 endpoint for this database
164+
- `provider` — Override the storage provider for this database
165+
- `public_url_prefix` — CDN/public URL prefix for public buckets
166+
167+
This allows different tenants to use different storage backends while sharing the same plugin configuration.

0 commit comments

Comments
 (0)