graphile-bucket-provisioner-plugin 1.14.0 → 1.14.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -9
- package/esm/index.d.ts +1 -8
- package/esm/index.js +1 -8
- package/esm/plugin.d.ts +2 -20
- package/esm/plugin.js +15 -266
- package/esm/preset.d.ts +0 -1
- package/esm/preset.js +0 -1
- package/esm/types.d.ts +3 -22
- package/esm/types.js +0 -4
- package/index.d.ts +1 -8
- package/index.js +1 -8
- package/package.json +6 -5
- package/plugin.d.ts +2 -20
- package/plugin.js +15 -266
- package/preset.d.ts +0 -1
- package/preset.js +0 -1
- package/types.d.ts +3 -22
- package/types.js +0 -4
package/README.md
CHANGED
|
@@ -12,19 +12,16 @@
|
|
|
12
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
13
|
</p>
|
|
14
14
|
|
|
15
|
-
PostGraphile v5 plugin that
|
|
15
|
+
PostGraphile v5 plugin that explicitly provisions S3-compatible buckets through a GraphQL mutation using [`@constructive-io/bucket-provisioner`](../packages/bucket-provisioner).
|
|
16
16
|
|
|
17
17
|
## Features
|
|
18
18
|
|
|
19
|
-
- **
|
|
20
|
-
- **CORS update hook** — Wraps `update*` mutations to detect `allowed_origins` changes and re-apply CORS rules to the S3 bucket
|
|
19
|
+
- **Explicit `provisionBucket` mutation** — GraphQL mutation for manual/retry provisioning of any bucket
|
|
21
20
|
- **3-tier CORS resolution** — Bucket-level `allowed_origins` → storage module-level `allowed_origins` → plugin config `allowedOrigins`
|
|
22
21
|
- **Wildcard CORS** — Set `allowed_origins = ['*']` on a bucket for fully open CDN/public deployments
|
|
23
|
-
- **Explicit `provisionBucket` mutation** — GraphQL mutation for manual/retry provisioning of any bucket
|
|
24
22
|
- **Per-database overrides** — Reads `endpoint`, `provider`, `public_url_prefix`, and `allowed_origins` from the `storage_module` table for multi-tenant setups
|
|
25
23
|
- **Lazy S3 config** — Connection config can be a function (evaluated once, cached) to avoid eager env-var reads at import time
|
|
26
|
-
- **
|
|
27
|
-
- **Custom bucket naming** — Supports prefix-based naming or a fully custom `resolveBucketName` function
|
|
24
|
+
- **Deployment-controlled naming** — Requires a `resolveBucketName` policy for tenant-aware physical bucket names
|
|
28
25
|
|
|
29
26
|
## Installation
|
|
30
27
|
|
|
@@ -120,10 +117,8 @@ Creates the plugin instance. Returns a `GraphileConfig.Plugin`.
|
|
|
120
117
|
|--------|------|-------------|
|
|
121
118
|
| `connection` | `StorageConnectionConfig \| () => StorageConnectionConfig` | S3 connection config (static or lazy getter) |
|
|
122
119
|
| `allowedOrigins` | `string[]` | CORS allowed origins for bucket configuration |
|
|
123
|
-
| `
|
|
124
|
-
| `resolveBucketName` | `(bucketKey, databaseId) => string` | Custom bucket name resolver (takes precedence over prefix) |
|
|
120
|
+
| `resolveBucketName` | `(databaseId, bucketKey) => string` | Deployment policy for deriving a tenant-aware physical bucket name |
|
|
125
121
|
| `versioning` | `boolean?` | Enable S3 versioning on provisioned buckets (default: `false`) |
|
|
126
|
-
| `autoProvision` | `boolean?` | Enable auto-provisioning hook on create mutations (default: `true`) |
|
|
127
122
|
|
|
128
123
|
### `BucketProvisionerPreset(options)`
|
|
129
124
|
|
package/esm/index.d.ts
CHANGED
|
@@ -1,13 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Bucket Provisioner Plugin for PostGraphile v5
|
|
3
3
|
*
|
|
4
|
-
* Provides
|
|
5
|
-
* When bucket rows are created via GraphQL mutations, this plugin
|
|
6
|
-
* automatically provisions the corresponding S3 bucket with the
|
|
7
|
-
* correct privacy policies, CORS rules, and lifecycle settings.
|
|
8
|
-
*
|
|
9
|
-
* Also provides an explicit `provisionBucket` mutation for manual
|
|
10
|
-
* provisioning or re-provisioning of S3 buckets.
|
|
4
|
+
* Provides an explicit `provisionBucket` mutation for PostGraphile v5.
|
|
11
5
|
*
|
|
12
6
|
* @example
|
|
13
7
|
* ```typescript
|
|
@@ -31,7 +25,6 @@
|
|
|
31
25
|
* BucketProvisionerPreset({
|
|
32
26
|
* connection: getConnection, // pass function ref, NOT getConnection()
|
|
33
27
|
* allowedOrigins: ['https://app.example.com'],
|
|
34
|
-
* bucketNamePrefix: 'myapp',
|
|
35
28
|
* }),
|
|
36
29
|
* ],
|
|
37
30
|
* };
|
package/esm/index.js
CHANGED
|
@@ -1,13 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Bucket Provisioner Plugin for PostGraphile v5
|
|
3
3
|
*
|
|
4
|
-
* Provides
|
|
5
|
-
* When bucket rows are created via GraphQL mutations, this plugin
|
|
6
|
-
* automatically provisions the corresponding S3 bucket with the
|
|
7
|
-
* correct privacy policies, CORS rules, and lifecycle settings.
|
|
8
|
-
*
|
|
9
|
-
* Also provides an explicit `provisionBucket` mutation for manual
|
|
10
|
-
* provisioning or re-provisioning of S3 buckets.
|
|
4
|
+
* Provides an explicit `provisionBucket` mutation for PostGraphile v5.
|
|
11
5
|
*
|
|
12
6
|
* @example
|
|
13
7
|
* ```typescript
|
|
@@ -31,7 +25,6 @@
|
|
|
31
25
|
* BucketProvisionerPreset({
|
|
32
26
|
* connection: getConnection, // pass function ref, NOT getConnection()
|
|
33
27
|
* allowedOrigins: ['https://app.example.com'],
|
|
34
|
-
* bucketNamePrefix: 'myapp',
|
|
35
28
|
* }),
|
|
36
29
|
* ],
|
|
37
30
|
* };
|
package/esm/plugin.d.ts
CHANGED
|
@@ -7,21 +7,7 @@
|
|
|
7
7
|
* logical bucket row in the database. Reads the bucket config via RLS,
|
|
8
8
|
* then calls BucketProvisioner to create and configure the S3 bucket.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
* with `@storageBuckets` to automatically provision the S3 bucket after
|
|
12
|
-
* the database row is created.
|
|
13
|
-
*
|
|
14
|
-
* 3. CORS update hook — wraps `update*` mutations on `@storageBuckets` tables
|
|
15
|
-
* to detect changes to `allowed_origins` and re-apply CORS rules to the
|
|
16
|
-
* S3 bucket.
|
|
17
|
-
*
|
|
18
|
-
* CORS resolution hierarchy (most specific wins):
|
|
19
|
-
* 1. Bucket-level `allowed_origins` column (per-bucket override)
|
|
20
|
-
* 2. Storage-module-level `allowed_origins` column (per-database default)
|
|
21
|
-
* 3. Plugin config `allowedOrigins` (global fallback)
|
|
22
|
-
* Supports `['*']` for open/CDN mode (wildcard CORS).
|
|
23
|
-
*
|
|
24
|
-
* Both pathways use `@constructive-io/bucket-provisioner` for the actual
|
|
10
|
+
* This plugin uses `@constructive-io/bucket-provisioner` for the actual
|
|
25
11
|
* S3 operations (bucket creation, Block Public Access, CORS, policies,
|
|
26
12
|
* versioning, lifecycle rules).
|
|
27
13
|
*
|
|
@@ -35,16 +21,12 @@ import type { BucketProvisionerPluginOptions } from './types';
|
|
|
35
21
|
/**
|
|
36
22
|
* Creates the bucket provisioner plugin.
|
|
37
23
|
*
|
|
38
|
-
* This plugin provides
|
|
24
|
+
* This plugin provides one provisioning pathway:
|
|
39
25
|
*
|
|
40
26
|
* 1. **Explicit `provisionBucket` mutation** — Call this mutation with a
|
|
41
27
|
* bucket key to provision (or re-provision) the S3 bucket. Protected
|
|
42
28
|
* by RLS on the buckets table.
|
|
43
29
|
*
|
|
44
|
-
* 2. **Auto-provisioning hook** — When `autoProvision` is true (default),
|
|
45
|
-
* wraps `create*` mutation resolvers on tables tagged with `@storageBuckets`
|
|
46
|
-
* to automatically provision the S3 bucket after the row is created.
|
|
47
|
-
*
|
|
48
30
|
* @param options - Plugin configuration (S3 credentials, CORS origins, naming)
|
|
49
31
|
*/
|
|
50
32
|
export declare function createBucketProvisionerPlugin(options: BucketProvisionerPluginOptions): GraphileConfig.Plugin;
|
package/esm/plugin.js
CHANGED
|
@@ -7,21 +7,7 @@
|
|
|
7
7
|
* logical bucket row in the database. Reads the bucket config via RLS,
|
|
8
8
|
* then calls BucketProvisioner to create and configure the S3 bucket.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
* with `@storageBuckets` to automatically provision the S3 bucket after
|
|
12
|
-
* the database row is created.
|
|
13
|
-
*
|
|
14
|
-
* 3. CORS update hook — wraps `update*` mutations on `@storageBuckets` tables
|
|
15
|
-
* to detect changes to `allowed_origins` and re-apply CORS rules to the
|
|
16
|
-
* S3 bucket.
|
|
17
|
-
*
|
|
18
|
-
* CORS resolution hierarchy (most specific wins):
|
|
19
|
-
* 1. Bucket-level `allowed_origins` column (per-bucket override)
|
|
20
|
-
* 2. Storage-module-level `allowed_origins` column (per-database default)
|
|
21
|
-
* 3. Plugin config `allowedOrigins` (global fallback)
|
|
22
|
-
* Supports `['*']` for open/CDN mode (wildcard CORS).
|
|
23
|
-
*
|
|
24
|
-
* Both pathways use `@constructive-io/bucket-provisioner` for the actual
|
|
10
|
+
* This plugin uses `@constructive-io/bucket-provisioner` for the actual
|
|
25
11
|
* S3 operations (bucket creation, Block Public Access, CORS, policies,
|
|
26
12
|
* versioning, lifecycle rules).
|
|
27
13
|
*
|
|
@@ -34,33 +20,10 @@ import { BucketProvisioner, } from '@constructive-io/bucket-provisioner';
|
|
|
34
20
|
import { Logger } from '@pgpmjs/logger';
|
|
35
21
|
import { QuoteUtils } from '@pgsql/quotes';
|
|
36
22
|
import { context as grafastContext, lambda, object } from 'grafast';
|
|
23
|
+
import { recordPhysicalName as recordPhysicalBucketName } from 'graphile-storage-registry';
|
|
37
24
|
import { extendSchema, gql } from 'graphile-utils';
|
|
38
25
|
const log = new Logger('graphile-bucket-provisioner:plugin');
|
|
39
26
|
// --- Storage module queries ---
|
|
40
|
-
/**
|
|
41
|
-
* Resolve the storage module whose buckets table is the one being addressed.
|
|
42
|
-
*
|
|
43
|
-
* The buckets table's identity (schema + table, from the codec being mutated or
|
|
44
|
-
* the module row) is the fact that names the plane — never a scope literal.
|
|
45
|
-
*/
|
|
46
|
-
const STORAGE_MODULE_BY_BUCKETS_TABLE_QUERY = `
|
|
47
|
-
SELECT
|
|
48
|
-
sm.id,
|
|
49
|
-
sm.scope,
|
|
50
|
-
sm.entity_table_id,
|
|
51
|
-
bs.schema_name AS buckets_schema,
|
|
52
|
-
bt.name AS buckets_table,
|
|
53
|
-
sm.endpoint,
|
|
54
|
-
sm.public_url_prefix,
|
|
55
|
-
sm.provider,
|
|
56
|
-
sm.allowed_origins
|
|
57
|
-
FROM metaschema_modules_public.storage_module sm
|
|
58
|
-
JOIN metaschema_public.table bt ON bt.id = sm.buckets_table_id
|
|
59
|
-
JOIN metaschema_public.schema bs ON bs.id = bt.schema_id
|
|
60
|
-
WHERE sm.database_id = $1
|
|
61
|
-
AND bs.schema_name = $2
|
|
62
|
-
AND bt.name = $3
|
|
63
|
-
`;
|
|
64
27
|
/**
|
|
65
28
|
* Resolve ALL storage modules for a database (for bucket-key and ownerId-based
|
|
66
29
|
* resolution in the explicit provisionBucket mutation).
|
|
@@ -96,30 +59,6 @@ const ALL_STORAGE_MODULES_QUERY = `
|
|
|
96
59
|
function runQuery(pgClient, text, values) {
|
|
97
60
|
return pgClient.query(values === undefined ? { text } : { text, values });
|
|
98
61
|
}
|
|
99
|
-
/**
|
|
100
|
-
* Resolve the storage module whose buckets table is the one being addressed.
|
|
101
|
-
*
|
|
102
|
-
* This is the resolution the auto-provision/CORS hooks use: the codec being
|
|
103
|
-
* mutated names its table, and the table names its module. A `@storageBuckets`
|
|
104
|
-
* table with no module row (or with several) is a provisioning bug and throws.
|
|
105
|
-
*/
|
|
106
|
-
async function resolveStorageModuleByBucketsTable(pgClient, databaseId, schemaName, tableName) {
|
|
107
|
-
const result = await runQuery(pgClient, STORAGE_MODULE_BY_BUCKETS_TABLE_QUERY, [
|
|
108
|
-
databaseId,
|
|
109
|
-
schemaName,
|
|
110
|
-
tableName,
|
|
111
|
-
]);
|
|
112
|
-
const rows = result.rows;
|
|
113
|
-
if (rows.length === 0) {
|
|
114
|
-
throw new Error(`STORAGE_MODULE_NOT_FOUND: no storage module in database ${databaseId} records ` +
|
|
115
|
-
`${schemaName}.${tableName} as its buckets table`);
|
|
116
|
-
}
|
|
117
|
-
if (rows.length > 1) {
|
|
118
|
-
throw new Error(`STORAGE_MODULE_AMBIGUOUS: ${rows.length} storage modules in database ${databaseId} record ` +
|
|
119
|
-
`${schemaName}.${tableName} as their buckets table`);
|
|
120
|
-
}
|
|
121
|
-
return rows[0];
|
|
122
|
-
}
|
|
123
62
|
/**
|
|
124
63
|
* The explicit provisionBucket mutation's resolution: find the plane that
|
|
125
64
|
* actually holds the named bucket row.
|
|
@@ -185,17 +124,6 @@ async function resolveBucketByKey(pgClient, databaseId, bucketKey, ownerId) {
|
|
|
185
124
|
function storedPhysicalName(row) {
|
|
186
125
|
return row.physical_name == null ? null : row.physical_name;
|
|
187
126
|
}
|
|
188
|
-
/**
|
|
189
|
-
* Record the physical S3 bucket name on the source bucket row.
|
|
190
|
-
*
|
|
191
|
-
* Runs in the system lane (`withPgClient(null, ...)`) — server bookkeeping,
|
|
192
|
-
* RLS-independent. Idempotent via the `physical_name IS NULL` guard so a
|
|
193
|
-
* re-provision never clobbers an already-recorded coordinate.
|
|
194
|
-
*/
|
|
195
|
-
async function recordPhysicalName(withPgClient, bucketsTable, bucketId, physicalName) {
|
|
196
|
-
await withPgClient(null, (client) => runQuery(client, `UPDATE ${bucketsTable} SET physical_name = $1 WHERE id = $2 AND physical_name IS NULL`, [physicalName, bucketId]));
|
|
197
|
-
log.info(`Recorded physical_name="${physicalName}" on bucket ${bucketId}`);
|
|
198
|
-
}
|
|
199
127
|
// --- Helpers ---
|
|
200
128
|
/**
|
|
201
129
|
* Resolve the connection config from the options. If the option is a lazy
|
|
@@ -213,14 +141,14 @@ function resolveConnection(options) {
|
|
|
213
141
|
/**
|
|
214
142
|
* Resolve the S3 bucket name from a logical bucket key.
|
|
215
143
|
*/
|
|
216
|
-
function resolveBucketName(
|
|
217
|
-
if (options.resolveBucketName) {
|
|
218
|
-
|
|
144
|
+
function resolveBucketName(databaseId, bucketKey, options) {
|
|
145
|
+
if (!options.resolveBucketName) {
|
|
146
|
+
throw new Error('STORAGE_BUCKET_NAME_POLICY_MISSING: no resolveBucketName was configured, so there is ' +
|
|
147
|
+
`no name to provision for bucket "${bucketKey}" of database ${databaseId}. ` +
|
|
148
|
+
'Physical bucket naming is a deployment policy; the configured s3.bucket is a ' +
|
|
149
|
+
'connection default and is never a tenant bucket.');
|
|
219
150
|
}
|
|
220
|
-
|
|
221
|
-
return `${options.bucketNamePrefix}-${bucketKey}`;
|
|
222
|
-
}
|
|
223
|
-
return bucketKey;
|
|
151
|
+
return options.resolveBucketName(databaseId, bucketKey);
|
|
224
152
|
}
|
|
225
153
|
/**
|
|
226
154
|
* Resolve the database_id from the JWT context.
|
|
@@ -262,8 +190,7 @@ function buildProvisioner(options, storageModule, effectiveOrigins) {
|
|
|
262
190
|
});
|
|
263
191
|
}
|
|
264
192
|
/**
|
|
265
|
-
* Core provisioning logic
|
|
266
|
-
* auto-provisioning hook.
|
|
193
|
+
* Core provisioning logic for the explicit mutation.
|
|
267
194
|
*/
|
|
268
195
|
async function provisionBucketForRow(storageModule, databaseId, bucketKey, bucketType, bucketAllowedOrigins, options, s3BucketName) {
|
|
269
196
|
const accessType = bucketType;
|
|
@@ -283,40 +210,19 @@ async function provisionBucketForRow(storageModule, databaseId, bucketKey, bucke
|
|
|
283
210
|
`(provider=${result.provider}, blockPublicAccess=${result.blockPublicAccess})`);
|
|
284
211
|
return result;
|
|
285
212
|
}
|
|
286
|
-
/**
|
|
287
|
-
* Update CORS on an existing S3 bucket when allowed_origins changes.
|
|
288
|
-
*/
|
|
289
|
-
async function updateBucketCors(storageModule, databaseId, bucketKey, bucketType, bucketAllowedOrigins, options, s3BucketName) {
|
|
290
|
-
const accessType = bucketType;
|
|
291
|
-
const effectiveOrigins = resolveAllowedOrigins(bucketAllowedOrigins, storageModule?.allowed_origins, options.allowedOrigins);
|
|
292
|
-
const provisioner = buildProvisioner(options, storageModule, effectiveOrigins);
|
|
293
|
-
log.info(`Updating CORS on S3 bucket "${s3BucketName}" ` +
|
|
294
|
-
`(origins=${JSON.stringify(effectiveOrigins)}) for database ${databaseId}`);
|
|
295
|
-
await provisioner.updateCors({
|
|
296
|
-
bucketName: s3BucketName,
|
|
297
|
-
accessType,
|
|
298
|
-
allowedOrigins: effectiveOrigins,
|
|
299
|
-
});
|
|
300
|
-
log.info(`Successfully updated CORS on S3 bucket "${s3BucketName}"`);
|
|
301
|
-
}
|
|
302
213
|
// --- Plugin factory ---
|
|
303
214
|
/**
|
|
304
215
|
* Creates the bucket provisioner plugin.
|
|
305
216
|
*
|
|
306
|
-
* This plugin provides
|
|
217
|
+
* This plugin provides one provisioning pathway:
|
|
307
218
|
*
|
|
308
219
|
* 1. **Explicit `provisionBucket` mutation** — Call this mutation with a
|
|
309
220
|
* bucket key to provision (or re-provision) the S3 bucket. Protected
|
|
310
221
|
* by RLS on the buckets table.
|
|
311
222
|
*
|
|
312
|
-
* 2. **Auto-provisioning hook** — When `autoProvision` is true (default),
|
|
313
|
-
* wraps `create*` mutation resolvers on tables tagged with `@storageBuckets`
|
|
314
|
-
* to automatically provision the S3 bucket after the row is created.
|
|
315
|
-
*
|
|
316
223
|
* @param options - Plugin configuration (S3 credentials, CORS origins, naming)
|
|
317
224
|
*/
|
|
318
225
|
export function createBucketProvisionerPlugin(options) {
|
|
319
|
-
const autoProvision = options.autoProvision ?? true;
|
|
320
226
|
// The extendSchema plugin adds the explicit provisionBucket mutation
|
|
321
227
|
const mutationPlugin = extendSchema(() => ({
|
|
322
228
|
typeDefs: gql `
|
|
@@ -391,12 +297,13 @@ export function createBucketProvisionerPlugin(options) {
|
|
|
391
297
|
// is authoritative and the naming hook is never consulted again.
|
|
392
298
|
const recorded = storedPhysicalName(bucket);
|
|
393
299
|
const s3BucketName = recorded === null
|
|
394
|
-
? resolveBucketName(bucket.key,
|
|
300
|
+
? resolveBucketName(databaseId, bucket.key, options)
|
|
395
301
|
: recorded;
|
|
396
302
|
try {
|
|
397
303
|
const result = await provisionBucketForRow(storageModule, databaseId, bucket.key, bucket.type, bucket.allowed_origins, options, s3BucketName);
|
|
398
304
|
// Record the exact provisioned name on the source row.
|
|
399
|
-
await
|
|
305
|
+
await withPgClient(null, (client) => recordPhysicalBucketName((query) => runQuery(client, query.text, query.values), bucketsTable, bucket.id, result.bucketName));
|
|
306
|
+
log.info(`Recorded physical_name="${result.bucketName}" on bucket ${bucket.id}`);
|
|
400
307
|
return {
|
|
401
308
|
success: true,
|
|
402
309
|
bucketName: result.bucketName,
|
|
@@ -423,165 +330,7 @@ export function createBucketProvisionerPlugin(options) {
|
|
|
423
330
|
},
|
|
424
331
|
},
|
|
425
332
|
}));
|
|
426
|
-
|
|
427
|
-
if (!autoProvision) {
|
|
428
|
-
return mutationPlugin;
|
|
429
|
-
}
|
|
430
|
-
// Build a composite plugin that includes both the mutation and the hook
|
|
431
|
-
return {
|
|
432
|
-
...mutationPlugin,
|
|
433
|
-
name: 'BucketProvisionerPlugin',
|
|
434
|
-
version: '0.1.0',
|
|
435
|
-
description: 'Auto-provisions S3 buckets when bucket rows are created, ' +
|
|
436
|
-
'updates CORS when allowed_origins changes on update, ' +
|
|
437
|
-
'and provides a provisionBucket mutation for explicit provisioning',
|
|
438
|
-
after: ['PgAttributesPlugin', 'PgMutationCreatePlugin', 'PgMutationUpdateDeletePlugin'],
|
|
439
|
-
schema: {
|
|
440
|
-
...mutationPlugin.schema,
|
|
441
|
-
hooks: {
|
|
442
|
-
...(mutationPlugin.schema?.hooks ?? {}),
|
|
443
|
-
/**
|
|
444
|
-
* Wrap create and update mutation resolvers on tables tagged with @storageBuckets.
|
|
445
|
-
*
|
|
446
|
-
* - create*: After the row is created, provision the S3 bucket.
|
|
447
|
-
* - update*: After the row is updated, re-apply CORS if allowed_origins changed.
|
|
448
|
-
*
|
|
449
|
-
* If provisioning/CORS update fails, the DB row still exists (the mutation
|
|
450
|
-
* already committed), and the error is logged. Admin can retry via provisionBucket.
|
|
451
|
-
*/
|
|
452
|
-
GraphQLObjectType_fields_field(field, build, context) {
|
|
453
|
-
const { scope: { isRootMutation, fieldName, pgCodec }, } = context;
|
|
454
|
-
// Only wrap root mutation fields
|
|
455
|
-
if (!isRootMutation || !pgCodec || !pgCodec.attributes) {
|
|
456
|
-
return field;
|
|
457
|
-
}
|
|
458
|
-
// Check for @storageBuckets smart tag
|
|
459
|
-
const tags = pgCodec.extensions?.tags;
|
|
460
|
-
if (!tags?.storageBuckets) {
|
|
461
|
-
return field;
|
|
462
|
-
}
|
|
463
|
-
const isCreate = fieldName.startsWith('create');
|
|
464
|
-
const isUpdate = fieldName.startsWith('update');
|
|
465
|
-
// Only wrap create and update mutations (not delete)
|
|
466
|
-
if (!isCreate && !isUpdate) {
|
|
467
|
-
return field;
|
|
468
|
-
}
|
|
469
|
-
log.debug(`Wrapping mutation "${fieldName}" for ${isCreate ? 'auto-provisioning' : 'CORS update'} (codec: ${pgCodec.name})`);
|
|
470
|
-
// The codec being mutated names the buckets table — the hook always
|
|
471
|
-
// operates on the plane that table belongs to, never a guessed scope.
|
|
472
|
-
const codecSchemaName = pgCodec.extensions?.pg?.schemaName;
|
|
473
|
-
const codecTableName = pgCodec.extensions?.pg?.name;
|
|
474
|
-
const defaultResolver = (obj) => obj[fieldName];
|
|
475
|
-
const { resolve: oldResolve = defaultResolver, ...rest } = field;
|
|
476
|
-
return {
|
|
477
|
-
...rest,
|
|
478
|
-
async resolve(source, args, graphqlContext, info) {
|
|
479
|
-
// Call the original resolver first (creates/updates the DB row)
|
|
480
|
-
const result = await oldResolve(source, args, graphqlContext, info);
|
|
481
|
-
try {
|
|
482
|
-
const inputKey = Object.keys(args.input || {}).find((k) => k !== 'clientMutationId');
|
|
483
|
-
const bucketInput = inputKey ? args.input[inputKey] : null;
|
|
484
|
-
const withPgClient = graphqlContext.withPgClient;
|
|
485
|
-
const pgSettings = graphqlContext.pgSettings;
|
|
486
|
-
if (!withPgClient) {
|
|
487
|
-
log.warn(`${isCreate ? 'Auto-provision' : 'CORS update'} skipped: withPgClient not available in context`);
|
|
488
|
-
return result;
|
|
489
|
-
}
|
|
490
|
-
if (isCreate) {
|
|
491
|
-
// --- CREATE: full provisioning ---
|
|
492
|
-
if (!bucketInput?.key || !bucketInput?.type) {
|
|
493
|
-
log.warn(`Auto-provision skipped for "${fieldName}": ` +
|
|
494
|
-
`could not extract key/type from mutation input`);
|
|
495
|
-
return result;
|
|
496
|
-
}
|
|
497
|
-
if (!codecSchemaName || !codecTableName) {
|
|
498
|
-
throw new Error(`Auto-provision failed for "${fieldName}": codec ${pgCodec.name} carries no pg schema/table identity`);
|
|
499
|
-
}
|
|
500
|
-
await withPgClient(pgSettings, async (pgClient) => {
|
|
501
|
-
const databaseId = await resolveDatabaseId(pgClient);
|
|
502
|
-
if (!databaseId) {
|
|
503
|
-
log.warn('Auto-provision skipped: could not resolve database_id');
|
|
504
|
-
return;
|
|
505
|
-
}
|
|
506
|
-
// The mutated table names its module — the plane being written
|
|
507
|
-
// is the plane that gets provisioned.
|
|
508
|
-
const storageModule = await resolveStorageModuleByBucketsTable(pgClient, databaseId, codecSchemaName, codecTableName);
|
|
509
|
-
// Newly-created row has no stored coordinate yet — mint on first provision.
|
|
510
|
-
const result = await provisionBucketForRow(storageModule, databaseId, bucketInput.key, bucketInput.type, bucketInput.allowedOrigins ?? bucketInput.allowed_origins ?? null, options, resolveBucketName(bucketInput.key, databaseId, options));
|
|
511
|
-
// Record the provisioned name on the just-created row.
|
|
512
|
-
const bucketsTable = QuoteUtils.quoteQualifiedIdentifier(storageModule.buckets_schema, storageModule.buckets_table);
|
|
513
|
-
const ownerId = bucketInput.ownerId ?? bucketInput.owner_id ?? null;
|
|
514
|
-
const idResult = await runQuery(pgClient, ownerId
|
|
515
|
-
? `SELECT id FROM ${bucketsTable} WHERE key = $1 AND owner_id = $2 LIMIT 1`
|
|
516
|
-
: `SELECT id FROM ${bucketsTable} WHERE key = $1 LIMIT 1`, ownerId ? [bucketInput.key, ownerId] : [bucketInput.key]);
|
|
517
|
-
const bucketId = idResult.rows[0]?.id;
|
|
518
|
-
if (bucketId)
|
|
519
|
-
await recordPhysicalName(withPgClient, bucketsTable, bucketId, result.bucketName);
|
|
520
|
-
});
|
|
521
|
-
}
|
|
522
|
-
else {
|
|
523
|
-
// --- UPDATE: re-apply CORS if allowed_origins is in the patch ---
|
|
524
|
-
const hasOriginsUpdate = bucketInput &&
|
|
525
|
-
('allowedOrigins' in bucketInput || 'allowed_origins' in bucketInput);
|
|
526
|
-
if (!hasOriginsUpdate) {
|
|
527
|
-
// allowed_origins not being changed, nothing to do
|
|
528
|
-
return result;
|
|
529
|
-
}
|
|
530
|
-
if (!codecSchemaName || !codecTableName) {
|
|
531
|
-
throw new Error(`CORS update failed for "${fieldName}": codec ${pgCodec.name} carries no pg schema/table identity`);
|
|
532
|
-
}
|
|
533
|
-
await withPgClient(pgSettings, async (pgClient) => {
|
|
534
|
-
const databaseId = await resolveDatabaseId(pgClient);
|
|
535
|
-
if (!databaseId) {
|
|
536
|
-
log.warn('CORS update skipped: could not resolve database_id');
|
|
537
|
-
return;
|
|
538
|
-
}
|
|
539
|
-
// The mutated table names its module — CORS applies to the
|
|
540
|
-
// plane whose row was updated.
|
|
541
|
-
const storageModule = await resolveStorageModuleByBucketsTable(pgClient, databaseId, codecSchemaName, codecTableName);
|
|
542
|
-
// We need the bucket key — it may come from input or patch
|
|
543
|
-
// For updates, PostGraphile uses nodeId or the row's PK, so
|
|
544
|
-
// we read the bucket from the patch's key or from the nodeId
|
|
545
|
-
const patchKey = bucketInput?.key;
|
|
546
|
-
if (!patchKey) {
|
|
547
|
-
log.warn(`CORS update skipped for "${fieldName}": ` +
|
|
548
|
-
`could not determine bucket key from mutation input`);
|
|
549
|
-
return;
|
|
550
|
-
}
|
|
551
|
-
// Read the full bucket row (post-update) to get type + origins
|
|
552
|
-
const bucketsTable = QuoteUtils.quoteQualifiedIdentifier(storageModule.buckets_schema, storageModule.buckets_table);
|
|
553
|
-
const bucketResult = await runQuery(pgClient, `SELECT id, key, type, is_public, allowed_origins, physical_name
|
|
554
|
-
FROM ${bucketsTable}
|
|
555
|
-
WHERE key = $1
|
|
556
|
-
LIMIT 1`, [patchKey]);
|
|
557
|
-
if (bucketResult.rows.length === 0) {
|
|
558
|
-
log.warn(`CORS update skipped: bucket "${patchKey}" not found`);
|
|
559
|
-
return;
|
|
560
|
-
}
|
|
561
|
-
const bucket = bucketResult.rows[0];
|
|
562
|
-
// CORS applies to the recorded physical bucket; if the row was
|
|
563
|
-
// never provisioned there is nothing to update yet, so mint the
|
|
564
|
-
// conventional name the first provision would use.
|
|
565
|
-
const recorded = storedPhysicalName(bucket);
|
|
566
|
-
await updateBucketCors(storageModule, databaseId, bucket.key, bucket.type, bucket.allowed_origins, options, recorded === null
|
|
567
|
-
? resolveBucketName(bucket.key, databaseId, options)
|
|
568
|
-
: recorded);
|
|
569
|
-
});
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
catch (err) {
|
|
573
|
-
log.error(`${isCreate ? 'Auto-provision' : 'CORS update'} failed for "${fieldName}": ${err.message}. ` +
|
|
574
|
-
(isCreate
|
|
575
|
-
? `The bucket row was created but the S3 bucket was not provisioned. Use the provisionBucket mutation to retry.`
|
|
576
|
-
: `The bucket row was updated but CORS was not applied to the S3 bucket. Use the provisionBucket mutation to retry.`));
|
|
577
|
-
}
|
|
578
|
-
return result;
|
|
579
|
-
},
|
|
580
|
-
};
|
|
581
|
-
},
|
|
582
|
-
},
|
|
583
|
-
},
|
|
584
|
-
};
|
|
333
|
+
return mutationPlugin;
|
|
585
334
|
}
|
|
586
335
|
export const BucketProvisionerPlugin = createBucketProvisionerPlugin;
|
|
587
336
|
export default BucketProvisionerPlugin;
|
package/esm/preset.d.ts
CHANGED
|
@@ -31,7 +31,6 @@ import type { BucketProvisionerPluginOptions } from './types';
|
|
|
31
31
|
* BucketProvisionerPreset({
|
|
32
32
|
* connection: getConnection, // pass function ref, NOT getConnection()
|
|
33
33
|
* allowedOrigins: ['https://app.example.com'],
|
|
34
|
-
* bucketNamePrefix: 'myapp',
|
|
35
34
|
* }),
|
|
36
35
|
* ],
|
|
37
36
|
* };
|
package/esm/preset.js
CHANGED
|
@@ -30,7 +30,6 @@ import { createBucketProvisionerPlugin } from './plugin';
|
|
|
30
30
|
* BucketProvisionerPreset({
|
|
31
31
|
* connection: getConnection, // pass function ref, NOT getConnection()
|
|
32
32
|
* allowedOrigins: ['https://app.example.com'],
|
|
33
|
-
* bucketNamePrefix: 'myapp',
|
|
34
33
|
* }),
|
|
35
34
|
* ],
|
|
36
35
|
* };
|
package/esm/types.d.ts
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Types for the bucket provisioner plugin.
|
|
3
|
-
*
|
|
4
|
-
* Defines plugin options, connection configuration, and provisioning result
|
|
5
|
-
* types used by the Graphile plugin to auto-provision S3 buckets when
|
|
6
|
-
* bucket rows are created via GraphQL mutations.
|
|
7
3
|
*/
|
|
8
4
|
import type { BucketAccessType, ProvisionResult, StorageConnectionConfig, StorageProvider } from '@constructive-io/bucket-provisioner';
|
|
9
5
|
export type { BucketAccessType, ProvisionResult, StorageConnectionConfig, StorageProvider };
|
|
@@ -18,11 +14,11 @@ export type ConnectionConfigOrGetter = StorageConnectionConfig | (() => StorageC
|
|
|
18
14
|
/**
|
|
19
15
|
* Function to derive the actual S3 bucket name from a logical bucket key.
|
|
20
16
|
*
|
|
21
|
-
* @param bucketKey - The logical bucket key from the database (e.g., "public", "private")
|
|
22
17
|
* @param databaseId - The metaschema database UUID
|
|
18
|
+
* @param bucketKey - The logical bucket key from the database (e.g., "public", "private")
|
|
23
19
|
* @returns The S3 bucket name to create/configure
|
|
24
20
|
*/
|
|
25
|
-
export type BucketNameResolver = (
|
|
21
|
+
export type BucketNameResolver = (databaseId: string, bucketKey: string) => string;
|
|
26
22
|
/**
|
|
27
23
|
* Plugin options for the bucket provisioner plugin.
|
|
28
24
|
*/
|
|
@@ -38,15 +34,9 @@ export interface BucketProvisionerPluginOptions {
|
|
|
38
34
|
* Required for browser-based presigned URL uploads.
|
|
39
35
|
*/
|
|
40
36
|
allowedOrigins: string[];
|
|
41
|
-
/**
|
|
42
|
-
* Optional prefix for S3 bucket names.
|
|
43
|
-
* When set, the S3 bucket name becomes `{prefix}-{bucketKey}`.
|
|
44
|
-
* Example: prefix "myapp" + key "public" → S3 bucket "myapp-public"
|
|
45
|
-
*/
|
|
46
|
-
bucketNamePrefix?: string;
|
|
47
37
|
/**
|
|
48
38
|
* Optional custom function to derive S3 bucket names from logical bucket keys.
|
|
49
|
-
*
|
|
39
|
+
* Naming is a deployment policy and must be supplied by the caller.
|
|
50
40
|
*/
|
|
51
41
|
resolveBucketName?: BucketNameResolver;
|
|
52
42
|
/**
|
|
@@ -54,15 +44,6 @@ export interface BucketProvisionerPluginOptions {
|
|
|
54
44
|
* Default: false
|
|
55
45
|
*/
|
|
56
46
|
versioning?: boolean;
|
|
57
|
-
/**
|
|
58
|
-
* Whether to auto-provision S3 buckets when bucket rows are created
|
|
59
|
-
* via GraphQL mutations. When true, the plugin wraps create mutations
|
|
60
|
-
* on tables tagged with `@storageBuckets` to trigger provisioning
|
|
61
|
-
* after the mutation succeeds.
|
|
62
|
-
*
|
|
63
|
-
* Default: true
|
|
64
|
-
*/
|
|
65
|
-
autoProvision?: boolean;
|
|
66
47
|
}
|
|
67
48
|
/**
|
|
68
49
|
* Input for the provisionBucket mutation.
|
package/esm/types.js
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Types for the bucket provisioner plugin.
|
|
3
|
-
*
|
|
4
|
-
* Defines plugin options, connection configuration, and provisioning result
|
|
5
|
-
* types used by the Graphile plugin to auto-provision S3 buckets when
|
|
6
|
-
* bucket rows are created via GraphQL mutations.
|
|
7
3
|
*/
|
|
8
4
|
export {};
|
package/index.d.ts
CHANGED
|
@@ -1,13 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Bucket Provisioner Plugin for PostGraphile v5
|
|
3
3
|
*
|
|
4
|
-
* Provides
|
|
5
|
-
* When bucket rows are created via GraphQL mutations, this plugin
|
|
6
|
-
* automatically provisions the corresponding S3 bucket with the
|
|
7
|
-
* correct privacy policies, CORS rules, and lifecycle settings.
|
|
8
|
-
*
|
|
9
|
-
* Also provides an explicit `provisionBucket` mutation for manual
|
|
10
|
-
* provisioning or re-provisioning of S3 buckets.
|
|
4
|
+
* Provides an explicit `provisionBucket` mutation for PostGraphile v5.
|
|
11
5
|
*
|
|
12
6
|
* @example
|
|
13
7
|
* ```typescript
|
|
@@ -31,7 +25,6 @@
|
|
|
31
25
|
* BucketProvisionerPreset({
|
|
32
26
|
* connection: getConnection, // pass function ref, NOT getConnection()
|
|
33
27
|
* allowedOrigins: ['https://app.example.com'],
|
|
34
|
-
* bucketNamePrefix: 'myapp',
|
|
35
28
|
* }),
|
|
36
29
|
* ],
|
|
37
30
|
* };
|
package/index.js
CHANGED
|
@@ -2,13 +2,7 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Bucket Provisioner Plugin for PostGraphile v5
|
|
4
4
|
*
|
|
5
|
-
* Provides
|
|
6
|
-
* When bucket rows are created via GraphQL mutations, this plugin
|
|
7
|
-
* automatically provisions the corresponding S3 bucket with the
|
|
8
|
-
* correct privacy policies, CORS rules, and lifecycle settings.
|
|
9
|
-
*
|
|
10
|
-
* Also provides an explicit `provisionBucket` mutation for manual
|
|
11
|
-
* provisioning or re-provisioning of S3 buckets.
|
|
5
|
+
* Provides an explicit `provisionBucket` mutation for PostGraphile v5.
|
|
12
6
|
*
|
|
13
7
|
* @example
|
|
14
8
|
* ```typescript
|
|
@@ -32,7 +26,6 @@
|
|
|
32
26
|
* BucketProvisionerPreset({
|
|
33
27
|
* connection: getConnection, // pass function ref, NOT getConnection()
|
|
34
28
|
* allowedOrigins: ['https://app.example.com'],
|
|
35
|
-
* bucketNamePrefix: 'myapp',
|
|
36
29
|
* }),
|
|
37
30
|
* ],
|
|
38
31
|
* };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "graphile-bucket-provisioner-plugin",
|
|
3
|
-
"version": "1.14.
|
|
4
|
-
"description": "Bucket provisioning plugin for PostGraphile v5 —
|
|
3
|
+
"version": "1.14.2",
|
|
4
|
+
"description": "Bucket provisioning plugin for PostGraphile v5 — explicitly provisions S3 buckets via a GraphQL mutation",
|
|
5
5
|
"author": "Constructive <developers@constructive.io>",
|
|
6
6
|
"homepage": "https://github.com/constructive-io/constructive",
|
|
7
7
|
"license": "MIT",
|
|
@@ -40,9 +40,10 @@
|
|
|
40
40
|
"url": "https://github.com/constructive-io/constructive/issues"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@constructive-io/bucket-provisioner": "^0.
|
|
43
|
+
"@constructive-io/bucket-provisioner": "^0.26.0",
|
|
44
44
|
"@pgpmjs/logger": "^2.25.0",
|
|
45
|
-
"@pgsql/quotes": "^18.2.4"
|
|
45
|
+
"@pgsql/quotes": "^18.2.4",
|
|
46
|
+
"graphile-storage-registry": "^0.1.1"
|
|
46
47
|
},
|
|
47
48
|
"peerDependencies": {
|
|
48
49
|
"grafast": "^1.1.1",
|
|
@@ -57,5 +58,5 @@
|
|
|
57
58
|
"@types/node": "^22.19.11",
|
|
58
59
|
"makage": "^0.3.0"
|
|
59
60
|
},
|
|
60
|
-
"gitHead": "
|
|
61
|
+
"gitHead": "ec6638bb4a06080919cee7e9ebf48306a2535f2f"
|
|
61
62
|
}
|
package/plugin.d.ts
CHANGED
|
@@ -7,21 +7,7 @@
|
|
|
7
7
|
* logical bucket row in the database. Reads the bucket config via RLS,
|
|
8
8
|
* then calls BucketProvisioner to create and configure the S3 bucket.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
* with `@storageBuckets` to automatically provision the S3 bucket after
|
|
12
|
-
* the database row is created.
|
|
13
|
-
*
|
|
14
|
-
* 3. CORS update hook — wraps `update*` mutations on `@storageBuckets` tables
|
|
15
|
-
* to detect changes to `allowed_origins` and re-apply CORS rules to the
|
|
16
|
-
* S3 bucket.
|
|
17
|
-
*
|
|
18
|
-
* CORS resolution hierarchy (most specific wins):
|
|
19
|
-
* 1. Bucket-level `allowed_origins` column (per-bucket override)
|
|
20
|
-
* 2. Storage-module-level `allowed_origins` column (per-database default)
|
|
21
|
-
* 3. Plugin config `allowedOrigins` (global fallback)
|
|
22
|
-
* Supports `['*']` for open/CDN mode (wildcard CORS).
|
|
23
|
-
*
|
|
24
|
-
* Both pathways use `@constructive-io/bucket-provisioner` for the actual
|
|
10
|
+
* This plugin uses `@constructive-io/bucket-provisioner` for the actual
|
|
25
11
|
* S3 operations (bucket creation, Block Public Access, CORS, policies,
|
|
26
12
|
* versioning, lifecycle rules).
|
|
27
13
|
*
|
|
@@ -35,16 +21,12 @@ import type { BucketProvisionerPluginOptions } from './types';
|
|
|
35
21
|
/**
|
|
36
22
|
* Creates the bucket provisioner plugin.
|
|
37
23
|
*
|
|
38
|
-
* This plugin provides
|
|
24
|
+
* This plugin provides one provisioning pathway:
|
|
39
25
|
*
|
|
40
26
|
* 1. **Explicit `provisionBucket` mutation** — Call this mutation with a
|
|
41
27
|
* bucket key to provision (or re-provision) the S3 bucket. Protected
|
|
42
28
|
* by RLS on the buckets table.
|
|
43
29
|
*
|
|
44
|
-
* 2. **Auto-provisioning hook** — When `autoProvision` is true (default),
|
|
45
|
-
* wraps `create*` mutation resolvers on tables tagged with `@storageBuckets`
|
|
46
|
-
* to automatically provision the S3 bucket after the row is created.
|
|
47
|
-
*
|
|
48
30
|
* @param options - Plugin configuration (S3 credentials, CORS origins, naming)
|
|
49
31
|
*/
|
|
50
32
|
export declare function createBucketProvisionerPlugin(options: BucketProvisionerPluginOptions): GraphileConfig.Plugin;
|
package/plugin.js
CHANGED
|
@@ -8,21 +8,7 @@
|
|
|
8
8
|
* logical bucket row in the database. Reads the bucket config via RLS,
|
|
9
9
|
* then calls BucketProvisioner to create and configure the S3 bucket.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
* with `@storageBuckets` to automatically provision the S3 bucket after
|
|
13
|
-
* the database row is created.
|
|
14
|
-
*
|
|
15
|
-
* 3. CORS update hook — wraps `update*` mutations on `@storageBuckets` tables
|
|
16
|
-
* to detect changes to `allowed_origins` and re-apply CORS rules to the
|
|
17
|
-
* S3 bucket.
|
|
18
|
-
*
|
|
19
|
-
* CORS resolution hierarchy (most specific wins):
|
|
20
|
-
* 1. Bucket-level `allowed_origins` column (per-bucket override)
|
|
21
|
-
* 2. Storage-module-level `allowed_origins` column (per-database default)
|
|
22
|
-
* 3. Plugin config `allowedOrigins` (global fallback)
|
|
23
|
-
* Supports `['*']` for open/CDN mode (wildcard CORS).
|
|
24
|
-
*
|
|
25
|
-
* Both pathways use `@constructive-io/bucket-provisioner` for the actual
|
|
11
|
+
* This plugin uses `@constructive-io/bucket-provisioner` for the actual
|
|
26
12
|
* S3 operations (bucket creation, Block Public Access, CORS, policies,
|
|
27
13
|
* versioning, lifecycle rules).
|
|
28
14
|
*
|
|
@@ -38,33 +24,10 @@ const bucket_provisioner_1 = require("@constructive-io/bucket-provisioner");
|
|
|
38
24
|
const logger_1 = require("@pgpmjs/logger");
|
|
39
25
|
const quotes_1 = require("@pgsql/quotes");
|
|
40
26
|
const grafast_1 = require("grafast");
|
|
27
|
+
const graphile_storage_registry_1 = require("graphile-storage-registry");
|
|
41
28
|
const graphile_utils_1 = require("graphile-utils");
|
|
42
29
|
const log = new logger_1.Logger('graphile-bucket-provisioner:plugin');
|
|
43
30
|
// --- Storage module queries ---
|
|
44
|
-
/**
|
|
45
|
-
* Resolve the storage module whose buckets table is the one being addressed.
|
|
46
|
-
*
|
|
47
|
-
* The buckets table's identity (schema + table, from the codec being mutated or
|
|
48
|
-
* the module row) is the fact that names the plane — never a scope literal.
|
|
49
|
-
*/
|
|
50
|
-
const STORAGE_MODULE_BY_BUCKETS_TABLE_QUERY = `
|
|
51
|
-
SELECT
|
|
52
|
-
sm.id,
|
|
53
|
-
sm.scope,
|
|
54
|
-
sm.entity_table_id,
|
|
55
|
-
bs.schema_name AS buckets_schema,
|
|
56
|
-
bt.name AS buckets_table,
|
|
57
|
-
sm.endpoint,
|
|
58
|
-
sm.public_url_prefix,
|
|
59
|
-
sm.provider,
|
|
60
|
-
sm.allowed_origins
|
|
61
|
-
FROM metaschema_modules_public.storage_module sm
|
|
62
|
-
JOIN metaschema_public.table bt ON bt.id = sm.buckets_table_id
|
|
63
|
-
JOIN metaschema_public.schema bs ON bs.id = bt.schema_id
|
|
64
|
-
WHERE sm.database_id = $1
|
|
65
|
-
AND bs.schema_name = $2
|
|
66
|
-
AND bt.name = $3
|
|
67
|
-
`;
|
|
68
31
|
/**
|
|
69
32
|
* Resolve ALL storage modules for a database (for bucket-key and ownerId-based
|
|
70
33
|
* resolution in the explicit provisionBucket mutation).
|
|
@@ -100,30 +63,6 @@ const ALL_STORAGE_MODULES_QUERY = `
|
|
|
100
63
|
function runQuery(pgClient, text, values) {
|
|
101
64
|
return pgClient.query(values === undefined ? { text } : { text, values });
|
|
102
65
|
}
|
|
103
|
-
/**
|
|
104
|
-
* Resolve the storage module whose buckets table is the one being addressed.
|
|
105
|
-
*
|
|
106
|
-
* This is the resolution the auto-provision/CORS hooks use: the codec being
|
|
107
|
-
* mutated names its table, and the table names its module. A `@storageBuckets`
|
|
108
|
-
* table with no module row (or with several) is a provisioning bug and throws.
|
|
109
|
-
*/
|
|
110
|
-
async function resolveStorageModuleByBucketsTable(pgClient, databaseId, schemaName, tableName) {
|
|
111
|
-
const result = await runQuery(pgClient, STORAGE_MODULE_BY_BUCKETS_TABLE_QUERY, [
|
|
112
|
-
databaseId,
|
|
113
|
-
schemaName,
|
|
114
|
-
tableName,
|
|
115
|
-
]);
|
|
116
|
-
const rows = result.rows;
|
|
117
|
-
if (rows.length === 0) {
|
|
118
|
-
throw new Error(`STORAGE_MODULE_NOT_FOUND: no storage module in database ${databaseId} records ` +
|
|
119
|
-
`${schemaName}.${tableName} as its buckets table`);
|
|
120
|
-
}
|
|
121
|
-
if (rows.length > 1) {
|
|
122
|
-
throw new Error(`STORAGE_MODULE_AMBIGUOUS: ${rows.length} storage modules in database ${databaseId} record ` +
|
|
123
|
-
`${schemaName}.${tableName} as their buckets table`);
|
|
124
|
-
}
|
|
125
|
-
return rows[0];
|
|
126
|
-
}
|
|
127
66
|
/**
|
|
128
67
|
* The explicit provisionBucket mutation's resolution: find the plane that
|
|
129
68
|
* actually holds the named bucket row.
|
|
@@ -189,17 +128,6 @@ async function resolveBucketByKey(pgClient, databaseId, bucketKey, ownerId) {
|
|
|
189
128
|
function storedPhysicalName(row) {
|
|
190
129
|
return row.physical_name == null ? null : row.physical_name;
|
|
191
130
|
}
|
|
192
|
-
/**
|
|
193
|
-
* Record the physical S3 bucket name on the source bucket row.
|
|
194
|
-
*
|
|
195
|
-
* Runs in the system lane (`withPgClient(null, ...)`) — server bookkeeping,
|
|
196
|
-
* RLS-independent. Idempotent via the `physical_name IS NULL` guard so a
|
|
197
|
-
* re-provision never clobbers an already-recorded coordinate.
|
|
198
|
-
*/
|
|
199
|
-
async function recordPhysicalName(withPgClient, bucketsTable, bucketId, physicalName) {
|
|
200
|
-
await withPgClient(null, (client) => runQuery(client, `UPDATE ${bucketsTable} SET physical_name = $1 WHERE id = $2 AND physical_name IS NULL`, [physicalName, bucketId]));
|
|
201
|
-
log.info(`Recorded physical_name="${physicalName}" on bucket ${bucketId}`);
|
|
202
|
-
}
|
|
203
131
|
// --- Helpers ---
|
|
204
132
|
/**
|
|
205
133
|
* Resolve the connection config from the options. If the option is a lazy
|
|
@@ -217,14 +145,14 @@ function resolveConnection(options) {
|
|
|
217
145
|
/**
|
|
218
146
|
* Resolve the S3 bucket name from a logical bucket key.
|
|
219
147
|
*/
|
|
220
|
-
function resolveBucketName(
|
|
221
|
-
if (options.resolveBucketName) {
|
|
222
|
-
|
|
148
|
+
function resolveBucketName(databaseId, bucketKey, options) {
|
|
149
|
+
if (!options.resolveBucketName) {
|
|
150
|
+
throw new Error('STORAGE_BUCKET_NAME_POLICY_MISSING: no resolveBucketName was configured, so there is ' +
|
|
151
|
+
`no name to provision for bucket "${bucketKey}" of database ${databaseId}. ` +
|
|
152
|
+
'Physical bucket naming is a deployment policy; the configured s3.bucket is a ' +
|
|
153
|
+
'connection default and is never a tenant bucket.');
|
|
223
154
|
}
|
|
224
|
-
|
|
225
|
-
return `${options.bucketNamePrefix}-${bucketKey}`;
|
|
226
|
-
}
|
|
227
|
-
return bucketKey;
|
|
155
|
+
return options.resolveBucketName(databaseId, bucketKey);
|
|
228
156
|
}
|
|
229
157
|
/**
|
|
230
158
|
* Resolve the database_id from the JWT context.
|
|
@@ -266,8 +194,7 @@ function buildProvisioner(options, storageModule, effectiveOrigins) {
|
|
|
266
194
|
});
|
|
267
195
|
}
|
|
268
196
|
/**
|
|
269
|
-
* Core provisioning logic
|
|
270
|
-
* auto-provisioning hook.
|
|
197
|
+
* Core provisioning logic for the explicit mutation.
|
|
271
198
|
*/
|
|
272
199
|
async function provisionBucketForRow(storageModule, databaseId, bucketKey, bucketType, bucketAllowedOrigins, options, s3BucketName) {
|
|
273
200
|
const accessType = bucketType;
|
|
@@ -287,40 +214,19 @@ async function provisionBucketForRow(storageModule, databaseId, bucketKey, bucke
|
|
|
287
214
|
`(provider=${result.provider}, blockPublicAccess=${result.blockPublicAccess})`);
|
|
288
215
|
return result;
|
|
289
216
|
}
|
|
290
|
-
/**
|
|
291
|
-
* Update CORS on an existing S3 bucket when allowed_origins changes.
|
|
292
|
-
*/
|
|
293
|
-
async function updateBucketCors(storageModule, databaseId, bucketKey, bucketType, bucketAllowedOrigins, options, s3BucketName) {
|
|
294
|
-
const accessType = bucketType;
|
|
295
|
-
const effectiveOrigins = resolveAllowedOrigins(bucketAllowedOrigins, storageModule?.allowed_origins, options.allowedOrigins);
|
|
296
|
-
const provisioner = buildProvisioner(options, storageModule, effectiveOrigins);
|
|
297
|
-
log.info(`Updating CORS on S3 bucket "${s3BucketName}" ` +
|
|
298
|
-
`(origins=${JSON.stringify(effectiveOrigins)}) for database ${databaseId}`);
|
|
299
|
-
await provisioner.updateCors({
|
|
300
|
-
bucketName: s3BucketName,
|
|
301
|
-
accessType,
|
|
302
|
-
allowedOrigins: effectiveOrigins,
|
|
303
|
-
});
|
|
304
|
-
log.info(`Successfully updated CORS on S3 bucket "${s3BucketName}"`);
|
|
305
|
-
}
|
|
306
217
|
// --- Plugin factory ---
|
|
307
218
|
/**
|
|
308
219
|
* Creates the bucket provisioner plugin.
|
|
309
220
|
*
|
|
310
|
-
* This plugin provides
|
|
221
|
+
* This plugin provides one provisioning pathway:
|
|
311
222
|
*
|
|
312
223
|
* 1. **Explicit `provisionBucket` mutation** — Call this mutation with a
|
|
313
224
|
* bucket key to provision (or re-provision) the S3 bucket. Protected
|
|
314
225
|
* by RLS on the buckets table.
|
|
315
226
|
*
|
|
316
|
-
* 2. **Auto-provisioning hook** — When `autoProvision` is true (default),
|
|
317
|
-
* wraps `create*` mutation resolvers on tables tagged with `@storageBuckets`
|
|
318
|
-
* to automatically provision the S3 bucket after the row is created.
|
|
319
|
-
*
|
|
320
227
|
* @param options - Plugin configuration (S3 credentials, CORS origins, naming)
|
|
321
228
|
*/
|
|
322
229
|
function createBucketProvisionerPlugin(options) {
|
|
323
|
-
const autoProvision = options.autoProvision ?? true;
|
|
324
230
|
// The extendSchema plugin adds the explicit provisionBucket mutation
|
|
325
231
|
const mutationPlugin = (0, graphile_utils_1.extendSchema)(() => ({
|
|
326
232
|
typeDefs: (0, graphile_utils_1.gql) `
|
|
@@ -395,12 +301,13 @@ function createBucketProvisionerPlugin(options) {
|
|
|
395
301
|
// is authoritative and the naming hook is never consulted again.
|
|
396
302
|
const recorded = storedPhysicalName(bucket);
|
|
397
303
|
const s3BucketName = recorded === null
|
|
398
|
-
? resolveBucketName(bucket.key,
|
|
304
|
+
? resolveBucketName(databaseId, bucket.key, options)
|
|
399
305
|
: recorded;
|
|
400
306
|
try {
|
|
401
307
|
const result = await provisionBucketForRow(storageModule, databaseId, bucket.key, bucket.type, bucket.allowed_origins, options, s3BucketName);
|
|
402
308
|
// Record the exact provisioned name on the source row.
|
|
403
|
-
await recordPhysicalName(
|
|
309
|
+
await withPgClient(null, (client) => (0, graphile_storage_registry_1.recordPhysicalName)((query) => runQuery(client, query.text, query.values), bucketsTable, bucket.id, result.bucketName));
|
|
310
|
+
log.info(`Recorded physical_name="${result.bucketName}" on bucket ${bucket.id}`);
|
|
404
311
|
return {
|
|
405
312
|
success: true,
|
|
406
313
|
bucketName: result.bucketName,
|
|
@@ -427,165 +334,7 @@ function createBucketProvisionerPlugin(options) {
|
|
|
427
334
|
},
|
|
428
335
|
},
|
|
429
336
|
}));
|
|
430
|
-
|
|
431
|
-
if (!autoProvision) {
|
|
432
|
-
return mutationPlugin;
|
|
433
|
-
}
|
|
434
|
-
// Build a composite plugin that includes both the mutation and the hook
|
|
435
|
-
return {
|
|
436
|
-
...mutationPlugin,
|
|
437
|
-
name: 'BucketProvisionerPlugin',
|
|
438
|
-
version: '0.1.0',
|
|
439
|
-
description: 'Auto-provisions S3 buckets when bucket rows are created, ' +
|
|
440
|
-
'updates CORS when allowed_origins changes on update, ' +
|
|
441
|
-
'and provides a provisionBucket mutation for explicit provisioning',
|
|
442
|
-
after: ['PgAttributesPlugin', 'PgMutationCreatePlugin', 'PgMutationUpdateDeletePlugin'],
|
|
443
|
-
schema: {
|
|
444
|
-
...mutationPlugin.schema,
|
|
445
|
-
hooks: {
|
|
446
|
-
...(mutationPlugin.schema?.hooks ?? {}),
|
|
447
|
-
/**
|
|
448
|
-
* Wrap create and update mutation resolvers on tables tagged with @storageBuckets.
|
|
449
|
-
*
|
|
450
|
-
* - create*: After the row is created, provision the S3 bucket.
|
|
451
|
-
* - update*: After the row is updated, re-apply CORS if allowed_origins changed.
|
|
452
|
-
*
|
|
453
|
-
* If provisioning/CORS update fails, the DB row still exists (the mutation
|
|
454
|
-
* already committed), and the error is logged. Admin can retry via provisionBucket.
|
|
455
|
-
*/
|
|
456
|
-
GraphQLObjectType_fields_field(field, build, context) {
|
|
457
|
-
const { scope: { isRootMutation, fieldName, pgCodec }, } = context;
|
|
458
|
-
// Only wrap root mutation fields
|
|
459
|
-
if (!isRootMutation || !pgCodec || !pgCodec.attributes) {
|
|
460
|
-
return field;
|
|
461
|
-
}
|
|
462
|
-
// Check for @storageBuckets smart tag
|
|
463
|
-
const tags = pgCodec.extensions?.tags;
|
|
464
|
-
if (!tags?.storageBuckets) {
|
|
465
|
-
return field;
|
|
466
|
-
}
|
|
467
|
-
const isCreate = fieldName.startsWith('create');
|
|
468
|
-
const isUpdate = fieldName.startsWith('update');
|
|
469
|
-
// Only wrap create and update mutations (not delete)
|
|
470
|
-
if (!isCreate && !isUpdate) {
|
|
471
|
-
return field;
|
|
472
|
-
}
|
|
473
|
-
log.debug(`Wrapping mutation "${fieldName}" for ${isCreate ? 'auto-provisioning' : 'CORS update'} (codec: ${pgCodec.name})`);
|
|
474
|
-
// The codec being mutated names the buckets table — the hook always
|
|
475
|
-
// operates on the plane that table belongs to, never a guessed scope.
|
|
476
|
-
const codecSchemaName = pgCodec.extensions?.pg?.schemaName;
|
|
477
|
-
const codecTableName = pgCodec.extensions?.pg?.name;
|
|
478
|
-
const defaultResolver = (obj) => obj[fieldName];
|
|
479
|
-
const { resolve: oldResolve = defaultResolver, ...rest } = field;
|
|
480
|
-
return {
|
|
481
|
-
...rest,
|
|
482
|
-
async resolve(source, args, graphqlContext, info) {
|
|
483
|
-
// Call the original resolver first (creates/updates the DB row)
|
|
484
|
-
const result = await oldResolve(source, args, graphqlContext, info);
|
|
485
|
-
try {
|
|
486
|
-
const inputKey = Object.keys(args.input || {}).find((k) => k !== 'clientMutationId');
|
|
487
|
-
const bucketInput = inputKey ? args.input[inputKey] : null;
|
|
488
|
-
const withPgClient = graphqlContext.withPgClient;
|
|
489
|
-
const pgSettings = graphqlContext.pgSettings;
|
|
490
|
-
if (!withPgClient) {
|
|
491
|
-
log.warn(`${isCreate ? 'Auto-provision' : 'CORS update'} skipped: withPgClient not available in context`);
|
|
492
|
-
return result;
|
|
493
|
-
}
|
|
494
|
-
if (isCreate) {
|
|
495
|
-
// --- CREATE: full provisioning ---
|
|
496
|
-
if (!bucketInput?.key || !bucketInput?.type) {
|
|
497
|
-
log.warn(`Auto-provision skipped for "${fieldName}": ` +
|
|
498
|
-
`could not extract key/type from mutation input`);
|
|
499
|
-
return result;
|
|
500
|
-
}
|
|
501
|
-
if (!codecSchemaName || !codecTableName) {
|
|
502
|
-
throw new Error(`Auto-provision failed for "${fieldName}": codec ${pgCodec.name} carries no pg schema/table identity`);
|
|
503
|
-
}
|
|
504
|
-
await withPgClient(pgSettings, async (pgClient) => {
|
|
505
|
-
const databaseId = await resolveDatabaseId(pgClient);
|
|
506
|
-
if (!databaseId) {
|
|
507
|
-
log.warn('Auto-provision skipped: could not resolve database_id');
|
|
508
|
-
return;
|
|
509
|
-
}
|
|
510
|
-
// The mutated table names its module — the plane being written
|
|
511
|
-
// is the plane that gets provisioned.
|
|
512
|
-
const storageModule = await resolveStorageModuleByBucketsTable(pgClient, databaseId, codecSchemaName, codecTableName);
|
|
513
|
-
// Newly-created row has no stored coordinate yet — mint on first provision.
|
|
514
|
-
const result = await provisionBucketForRow(storageModule, databaseId, bucketInput.key, bucketInput.type, bucketInput.allowedOrigins ?? bucketInput.allowed_origins ?? null, options, resolveBucketName(bucketInput.key, databaseId, options));
|
|
515
|
-
// Record the provisioned name on the just-created row.
|
|
516
|
-
const bucketsTable = quotes_1.QuoteUtils.quoteQualifiedIdentifier(storageModule.buckets_schema, storageModule.buckets_table);
|
|
517
|
-
const ownerId = bucketInput.ownerId ?? bucketInput.owner_id ?? null;
|
|
518
|
-
const idResult = await runQuery(pgClient, ownerId
|
|
519
|
-
? `SELECT id FROM ${bucketsTable} WHERE key = $1 AND owner_id = $2 LIMIT 1`
|
|
520
|
-
: `SELECT id FROM ${bucketsTable} WHERE key = $1 LIMIT 1`, ownerId ? [bucketInput.key, ownerId] : [bucketInput.key]);
|
|
521
|
-
const bucketId = idResult.rows[0]?.id;
|
|
522
|
-
if (bucketId)
|
|
523
|
-
await recordPhysicalName(withPgClient, bucketsTable, bucketId, result.bucketName);
|
|
524
|
-
});
|
|
525
|
-
}
|
|
526
|
-
else {
|
|
527
|
-
// --- UPDATE: re-apply CORS if allowed_origins is in the patch ---
|
|
528
|
-
const hasOriginsUpdate = bucketInput &&
|
|
529
|
-
('allowedOrigins' in bucketInput || 'allowed_origins' in bucketInput);
|
|
530
|
-
if (!hasOriginsUpdate) {
|
|
531
|
-
// allowed_origins not being changed, nothing to do
|
|
532
|
-
return result;
|
|
533
|
-
}
|
|
534
|
-
if (!codecSchemaName || !codecTableName) {
|
|
535
|
-
throw new Error(`CORS update failed for "${fieldName}": codec ${pgCodec.name} carries no pg schema/table identity`);
|
|
536
|
-
}
|
|
537
|
-
await withPgClient(pgSettings, async (pgClient) => {
|
|
538
|
-
const databaseId = await resolveDatabaseId(pgClient);
|
|
539
|
-
if (!databaseId) {
|
|
540
|
-
log.warn('CORS update skipped: could not resolve database_id');
|
|
541
|
-
return;
|
|
542
|
-
}
|
|
543
|
-
// The mutated table names its module — CORS applies to the
|
|
544
|
-
// plane whose row was updated.
|
|
545
|
-
const storageModule = await resolveStorageModuleByBucketsTable(pgClient, databaseId, codecSchemaName, codecTableName);
|
|
546
|
-
// We need the bucket key — it may come from input or patch
|
|
547
|
-
// For updates, PostGraphile uses nodeId or the row's PK, so
|
|
548
|
-
// we read the bucket from the patch's key or from the nodeId
|
|
549
|
-
const patchKey = bucketInput?.key;
|
|
550
|
-
if (!patchKey) {
|
|
551
|
-
log.warn(`CORS update skipped for "${fieldName}": ` +
|
|
552
|
-
`could not determine bucket key from mutation input`);
|
|
553
|
-
return;
|
|
554
|
-
}
|
|
555
|
-
// Read the full bucket row (post-update) to get type + origins
|
|
556
|
-
const bucketsTable = quotes_1.QuoteUtils.quoteQualifiedIdentifier(storageModule.buckets_schema, storageModule.buckets_table);
|
|
557
|
-
const bucketResult = await runQuery(pgClient, `SELECT id, key, type, is_public, allowed_origins, physical_name
|
|
558
|
-
FROM ${bucketsTable}
|
|
559
|
-
WHERE key = $1
|
|
560
|
-
LIMIT 1`, [patchKey]);
|
|
561
|
-
if (bucketResult.rows.length === 0) {
|
|
562
|
-
log.warn(`CORS update skipped: bucket "${patchKey}" not found`);
|
|
563
|
-
return;
|
|
564
|
-
}
|
|
565
|
-
const bucket = bucketResult.rows[0];
|
|
566
|
-
// CORS applies to the recorded physical bucket; if the row was
|
|
567
|
-
// never provisioned there is nothing to update yet, so mint the
|
|
568
|
-
// conventional name the first provision would use.
|
|
569
|
-
const recorded = storedPhysicalName(bucket);
|
|
570
|
-
await updateBucketCors(storageModule, databaseId, bucket.key, bucket.type, bucket.allowed_origins, options, recorded === null
|
|
571
|
-
? resolveBucketName(bucket.key, databaseId, options)
|
|
572
|
-
: recorded);
|
|
573
|
-
});
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
catch (err) {
|
|
577
|
-
log.error(`${isCreate ? 'Auto-provision' : 'CORS update'} failed for "${fieldName}": ${err.message}. ` +
|
|
578
|
-
(isCreate
|
|
579
|
-
? `The bucket row was created but the S3 bucket was not provisioned. Use the provisionBucket mutation to retry.`
|
|
580
|
-
: `The bucket row was updated but CORS was not applied to the S3 bucket. Use the provisionBucket mutation to retry.`));
|
|
581
|
-
}
|
|
582
|
-
return result;
|
|
583
|
-
},
|
|
584
|
-
};
|
|
585
|
-
},
|
|
586
|
-
},
|
|
587
|
-
},
|
|
588
|
-
};
|
|
337
|
+
return mutationPlugin;
|
|
589
338
|
}
|
|
590
339
|
exports.BucketProvisionerPlugin = createBucketProvisionerPlugin;
|
|
591
340
|
exports.default = exports.BucketProvisionerPlugin;
|
package/preset.d.ts
CHANGED
|
@@ -31,7 +31,6 @@ import type { BucketProvisionerPluginOptions } from './types';
|
|
|
31
31
|
* BucketProvisionerPreset({
|
|
32
32
|
* connection: getConnection, // pass function ref, NOT getConnection()
|
|
33
33
|
* allowedOrigins: ['https://app.example.com'],
|
|
34
|
-
* bucketNamePrefix: 'myapp',
|
|
35
34
|
* }),
|
|
36
35
|
* ],
|
|
37
36
|
* };
|
package/preset.js
CHANGED
package/types.d.ts
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Types for the bucket provisioner plugin.
|
|
3
|
-
*
|
|
4
|
-
* Defines plugin options, connection configuration, and provisioning result
|
|
5
|
-
* types used by the Graphile plugin to auto-provision S3 buckets when
|
|
6
|
-
* bucket rows are created via GraphQL mutations.
|
|
7
3
|
*/
|
|
8
4
|
import type { BucketAccessType, ProvisionResult, StorageConnectionConfig, StorageProvider } from '@constructive-io/bucket-provisioner';
|
|
9
5
|
export type { BucketAccessType, ProvisionResult, StorageConnectionConfig, StorageProvider };
|
|
@@ -18,11 +14,11 @@ export type ConnectionConfigOrGetter = StorageConnectionConfig | (() => StorageC
|
|
|
18
14
|
/**
|
|
19
15
|
* Function to derive the actual S3 bucket name from a logical bucket key.
|
|
20
16
|
*
|
|
21
|
-
* @param bucketKey - The logical bucket key from the database (e.g., "public", "private")
|
|
22
17
|
* @param databaseId - The metaschema database UUID
|
|
18
|
+
* @param bucketKey - The logical bucket key from the database (e.g., "public", "private")
|
|
23
19
|
* @returns The S3 bucket name to create/configure
|
|
24
20
|
*/
|
|
25
|
-
export type BucketNameResolver = (
|
|
21
|
+
export type BucketNameResolver = (databaseId: string, bucketKey: string) => string;
|
|
26
22
|
/**
|
|
27
23
|
* Plugin options for the bucket provisioner plugin.
|
|
28
24
|
*/
|
|
@@ -38,15 +34,9 @@ export interface BucketProvisionerPluginOptions {
|
|
|
38
34
|
* Required for browser-based presigned URL uploads.
|
|
39
35
|
*/
|
|
40
36
|
allowedOrigins: string[];
|
|
41
|
-
/**
|
|
42
|
-
* Optional prefix for S3 bucket names.
|
|
43
|
-
* When set, the S3 bucket name becomes `{prefix}-{bucketKey}`.
|
|
44
|
-
* Example: prefix "myapp" + key "public" → S3 bucket "myapp-public"
|
|
45
|
-
*/
|
|
46
|
-
bucketNamePrefix?: string;
|
|
47
37
|
/**
|
|
48
38
|
* Optional custom function to derive S3 bucket names from logical bucket keys.
|
|
49
|
-
*
|
|
39
|
+
* Naming is a deployment policy and must be supplied by the caller.
|
|
50
40
|
*/
|
|
51
41
|
resolveBucketName?: BucketNameResolver;
|
|
52
42
|
/**
|
|
@@ -54,15 +44,6 @@ export interface BucketProvisionerPluginOptions {
|
|
|
54
44
|
* Default: false
|
|
55
45
|
*/
|
|
56
46
|
versioning?: boolean;
|
|
57
|
-
/**
|
|
58
|
-
* Whether to auto-provision S3 buckets when bucket rows are created
|
|
59
|
-
* via GraphQL mutations. When true, the plugin wraps create mutations
|
|
60
|
-
* on tables tagged with `@storageBuckets` to trigger provisioning
|
|
61
|
-
* after the mutation succeeds.
|
|
62
|
-
*
|
|
63
|
-
* Default: true
|
|
64
|
-
*/
|
|
65
|
-
autoProvision?: boolean;
|
|
66
47
|
}
|
|
67
48
|
/**
|
|
68
49
|
* Input for the provisionBucket mutation.
|
package/types.js
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
3
|
* Types for the bucket provisioner plugin.
|
|
4
|
-
*
|
|
5
|
-
* Defines plugin options, connection configuration, and provisioning result
|
|
6
|
-
* types used by the Graphile plugin to auto-provision S3 buckets when
|
|
7
|
-
* bucket rows are created via GraphQL mutations.
|
|
8
4
|
*/
|
|
9
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|