graphile-bucket-provisioner-plugin 1.9.3 → 1.10.0
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/esm/plugin.js +55 -11
- package/package.json +2 -2
- package/plugin.js +55 -11
package/esm/plugin.js
CHANGED
|
@@ -105,6 +105,28 @@ async function resolveStorageModule(pgClient, databaseId, ownerId) {
|
|
|
105
105
|
}
|
|
106
106
|
return null;
|
|
107
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* Normalize the recorded physical coordinate at the DB boundary.
|
|
110
|
+
*
|
|
111
|
+
* A bucket row either carries a recorded coordinate or it does not; SQL nulls
|
|
112
|
+
* and absent columns both mean "never provisioned". Collapsing them here is
|
|
113
|
+
* the single place that shape is interpreted — callers branch on `string`
|
|
114
|
+
* vs `null` and never coalesce a bucket name into existence.
|
|
115
|
+
*/
|
|
116
|
+
function storedPhysicalName(row) {
|
|
117
|
+
return row.physical_name == null ? null : row.physical_name;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Record the physical S3 bucket name on the source bucket row.
|
|
121
|
+
*
|
|
122
|
+
* Runs in the system lane (`withPgClient(null, ...)`) — server bookkeeping,
|
|
123
|
+
* RLS-independent. Idempotent via the `physical_name IS NULL` guard so a
|
|
124
|
+
* re-provision never clobbers an already-recorded coordinate.
|
|
125
|
+
*/
|
|
126
|
+
async function recordPhysicalName(withPgClient, bucketsTable, bucketId, physicalName) {
|
|
127
|
+
await withPgClient(null, (client) => client.query(`UPDATE ${bucketsTable} SET physical_name = $1 WHERE id = $2 AND physical_name IS NULL`, [physicalName, bucketId]));
|
|
128
|
+
log.info(`Recorded physical_name="${physicalName}" on bucket ${bucketId}`);
|
|
129
|
+
}
|
|
108
130
|
// --- Helpers ---
|
|
109
131
|
/**
|
|
110
132
|
* Resolve the connection config from the options. If the option is a lazy
|
|
@@ -174,8 +196,7 @@ function buildProvisioner(options, storageModule, effectiveOrigins) {
|
|
|
174
196
|
* Core provisioning logic shared by both the explicit mutation and the
|
|
175
197
|
* auto-provisioning hook.
|
|
176
198
|
*/
|
|
177
|
-
async function provisionBucketForRow(pgClient, databaseId, bucketKey, bucketType, bucketAllowedOrigins, options) {
|
|
178
|
-
const s3BucketName = resolveBucketName(bucketKey, databaseId, options);
|
|
199
|
+
async function provisionBucketForRow(pgClient, databaseId, bucketKey, bucketType, bucketAllowedOrigins, options, s3BucketName) {
|
|
179
200
|
const accessType = bucketType;
|
|
180
201
|
// Read storage module config to check for endpoint/provider/CORS overrides
|
|
181
202
|
const storageModule = await resolveStorageModule(pgClient, databaseId);
|
|
@@ -198,8 +219,7 @@ async function provisionBucketForRow(pgClient, databaseId, bucketKey, bucketType
|
|
|
198
219
|
/**
|
|
199
220
|
* Update CORS on an existing S3 bucket when allowed_origins changes.
|
|
200
221
|
*/
|
|
201
|
-
async function updateBucketCors(pgClient, databaseId, bucketKey, bucketType, bucketAllowedOrigins, options) {
|
|
202
|
-
const s3BucketName = resolveBucketName(bucketKey, databaseId, options);
|
|
222
|
+
async function updateBucketCors(pgClient, databaseId, bucketKey, bucketType, bucketAllowedOrigins, options, s3BucketName) {
|
|
203
223
|
const accessType = bucketType;
|
|
204
224
|
const storageModule = await resolveStorageModule(pgClient, databaseId);
|
|
205
225
|
const effectiveOrigins = resolveAllowedOrigins(bucketAllowedOrigins, storageModule?.allowed_origins, options.allowedOrigins);
|
|
@@ -304,11 +324,11 @@ export function createBucketProvisionerPlugin(options) {
|
|
|
304
324
|
const hasOwner = ownerId && storageModule.scope !== 'app';
|
|
305
325
|
const bucketsTable = QuoteUtils.quoteQualifiedIdentifier(storageModule.buckets_schema, storageModule.buckets_table);
|
|
306
326
|
const bucketResult = await pgClient.query(hasOwner
|
|
307
|
-
? `SELECT id, key, type, is_public, allowed_origins
|
|
327
|
+
? `SELECT id, key, type, is_public, allowed_origins, physical_name
|
|
308
328
|
FROM ${bucketsTable}
|
|
309
329
|
WHERE key = $1 AND owner_id = $2
|
|
310
330
|
LIMIT 1`
|
|
311
|
-
: `SELECT id, key, type, is_public, allowed_origins
|
|
331
|
+
: `SELECT id, key, type, is_public, allowed_origins, physical_name
|
|
312
332
|
FROM ${bucketsTable}
|
|
313
333
|
WHERE key = $1
|
|
314
334
|
LIMIT 1`, hasOwner ? [bucketKey, ownerId] : [bucketKey]);
|
|
@@ -316,8 +336,16 @@ export function createBucketProvisionerPlugin(options) {
|
|
|
316
336
|
throw new Error('BUCKET_NOT_FOUND');
|
|
317
337
|
}
|
|
318
338
|
const bucket = bucketResult.rows[0];
|
|
339
|
+
// First provision mints a name; afterwards the stored coordinate
|
|
340
|
+
// is authoritative and the naming hook is never consulted again.
|
|
341
|
+
const recorded = storedPhysicalName(bucket);
|
|
342
|
+
const s3BucketName = recorded === null
|
|
343
|
+
? resolveBucketName(bucket.key, databaseId, options)
|
|
344
|
+
: recorded;
|
|
319
345
|
try {
|
|
320
|
-
const result = await provisionBucketForRow(pgClient, databaseId, bucket.key, bucket.type, bucket.allowed_origins, options);
|
|
346
|
+
const result = await provisionBucketForRow(pgClient, databaseId, bucket.key, bucket.type, bucket.allowed_origins, options, s3BucketName);
|
|
347
|
+
// Record the exact provisioned name on the source row.
|
|
348
|
+
await recordPhysicalName(withPgClient, bucketsTable, bucket.id, result.bucketName);
|
|
321
349
|
return {
|
|
322
350
|
success: true,
|
|
323
351
|
bucketName: result.bucketName,
|
|
@@ -331,7 +359,7 @@ export function createBucketProvisionerPlugin(options) {
|
|
|
331
359
|
log.error(`Failed to provision bucket "${bucketKey}": ${err.message}`);
|
|
332
360
|
return {
|
|
333
361
|
success: false,
|
|
334
|
-
bucketName:
|
|
362
|
+
bucketName: s3BucketName,
|
|
335
363
|
accessType: bucket.type,
|
|
336
364
|
provider: resolveConnection(options).provider,
|
|
337
365
|
endpoint: resolveConnection(options).endpoint ?? null,
|
|
@@ -417,7 +445,17 @@ export function createBucketProvisionerPlugin(options) {
|
|
|
417
445
|
log.warn('Auto-provision skipped: could not resolve database_id');
|
|
418
446
|
return;
|
|
419
447
|
}
|
|
420
|
-
|
|
448
|
+
// Newly-created row has no stored coordinate yet — mint on first provision.
|
|
449
|
+
const result = await provisionBucketForRow(pgClient, databaseId, bucketInput.key, bucketInput.type, bucketInput.allowedOrigins ?? bucketInput.allowed_origins ?? null, options, resolveBucketName(bucketInput.key, databaseId, options));
|
|
450
|
+
// Record the provisioned name on the just-created row.
|
|
451
|
+
const storageModule = await resolveStorageModule(pgClient, databaseId);
|
|
452
|
+
if (storageModule) {
|
|
453
|
+
const bucketsTable = QuoteUtils.quoteQualifiedIdentifier(storageModule.buckets_schema, storageModule.buckets_table);
|
|
454
|
+
const idResult = await pgClient.query(`SELECT id FROM ${bucketsTable} WHERE key = $1 LIMIT 1`, [bucketInput.key]);
|
|
455
|
+
const bucketId = idResult.rows[0]?.id;
|
|
456
|
+
if (bucketId)
|
|
457
|
+
await recordPhysicalName(withPgClient, bucketsTable, bucketId, result.bucketName);
|
|
458
|
+
}
|
|
421
459
|
});
|
|
422
460
|
}
|
|
423
461
|
else {
|
|
@@ -451,7 +489,7 @@ export function createBucketProvisionerPlugin(options) {
|
|
|
451
489
|
}
|
|
452
490
|
// Read the full bucket row (post-update) to get type + origins
|
|
453
491
|
const bucketsTable = QuoteUtils.quoteQualifiedIdentifier(storageModule.buckets_schema, storageModule.buckets_table);
|
|
454
|
-
const bucketResult = await pgClient.query(`SELECT id, key, type, is_public, allowed_origins
|
|
492
|
+
const bucketResult = await pgClient.query(`SELECT id, key, type, is_public, allowed_origins, physical_name
|
|
455
493
|
FROM ${bucketsTable}
|
|
456
494
|
WHERE key = $1
|
|
457
495
|
LIMIT 1`, [patchKey]);
|
|
@@ -460,7 +498,13 @@ export function createBucketProvisionerPlugin(options) {
|
|
|
460
498
|
return;
|
|
461
499
|
}
|
|
462
500
|
const bucket = bucketResult.rows[0];
|
|
463
|
-
|
|
501
|
+
// CORS applies to the recorded physical bucket; if the row was
|
|
502
|
+
// never provisioned there is nothing to update yet, so mint the
|
|
503
|
+
// conventional name the first provision would use.
|
|
504
|
+
const recorded = storedPhysicalName(bucket);
|
|
505
|
+
await updateBucketCors(pgClient, databaseId, bucket.key, bucket.type, bucket.allowed_origins, options, recorded === null
|
|
506
|
+
? resolveBucketName(bucket.key, databaseId, options)
|
|
507
|
+
: recorded);
|
|
464
508
|
});
|
|
465
509
|
}
|
|
466
510
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "graphile-bucket-provisioner-plugin",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.0",
|
|
4
4
|
"description": "Bucket provisioning plugin for PostGraphile v5 — auto-provisions S3 buckets on bucket table mutations",
|
|
5
5
|
"author": "Constructive <developers@constructive.io>",
|
|
6
6
|
"homepage": "https://github.com/constructive-io/constructive",
|
|
@@ -57,5 +57,5 @@
|
|
|
57
57
|
"@types/node": "^22.19.11",
|
|
58
58
|
"makage": "^0.3.0"
|
|
59
59
|
},
|
|
60
|
-
"gitHead": "
|
|
60
|
+
"gitHead": "c438821362ceaf70b2a20d74ecf25ad4191c1748"
|
|
61
61
|
}
|
package/plugin.js
CHANGED
|
@@ -109,6 +109,28 @@ async function resolveStorageModule(pgClient, databaseId, ownerId) {
|
|
|
109
109
|
}
|
|
110
110
|
return null;
|
|
111
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* Normalize the recorded physical coordinate at the DB boundary.
|
|
114
|
+
*
|
|
115
|
+
* A bucket row either carries a recorded coordinate or it does not; SQL nulls
|
|
116
|
+
* and absent columns both mean "never provisioned". Collapsing them here is
|
|
117
|
+
* the single place that shape is interpreted — callers branch on `string`
|
|
118
|
+
* vs `null` and never coalesce a bucket name into existence.
|
|
119
|
+
*/
|
|
120
|
+
function storedPhysicalName(row) {
|
|
121
|
+
return row.physical_name == null ? null : row.physical_name;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Record the physical S3 bucket name on the source bucket row.
|
|
125
|
+
*
|
|
126
|
+
* Runs in the system lane (`withPgClient(null, ...)`) — server bookkeeping,
|
|
127
|
+
* RLS-independent. Idempotent via the `physical_name IS NULL` guard so a
|
|
128
|
+
* re-provision never clobbers an already-recorded coordinate.
|
|
129
|
+
*/
|
|
130
|
+
async function recordPhysicalName(withPgClient, bucketsTable, bucketId, physicalName) {
|
|
131
|
+
await withPgClient(null, (client) => client.query(`UPDATE ${bucketsTable} SET physical_name = $1 WHERE id = $2 AND physical_name IS NULL`, [physicalName, bucketId]));
|
|
132
|
+
log.info(`Recorded physical_name="${physicalName}" on bucket ${bucketId}`);
|
|
133
|
+
}
|
|
112
134
|
// --- Helpers ---
|
|
113
135
|
/**
|
|
114
136
|
* Resolve the connection config from the options. If the option is a lazy
|
|
@@ -178,8 +200,7 @@ function buildProvisioner(options, storageModule, effectiveOrigins) {
|
|
|
178
200
|
* Core provisioning logic shared by both the explicit mutation and the
|
|
179
201
|
* auto-provisioning hook.
|
|
180
202
|
*/
|
|
181
|
-
async function provisionBucketForRow(pgClient, databaseId, bucketKey, bucketType, bucketAllowedOrigins, options) {
|
|
182
|
-
const s3BucketName = resolveBucketName(bucketKey, databaseId, options);
|
|
203
|
+
async function provisionBucketForRow(pgClient, databaseId, bucketKey, bucketType, bucketAllowedOrigins, options, s3BucketName) {
|
|
183
204
|
const accessType = bucketType;
|
|
184
205
|
// Read storage module config to check for endpoint/provider/CORS overrides
|
|
185
206
|
const storageModule = await resolveStorageModule(pgClient, databaseId);
|
|
@@ -202,8 +223,7 @@ async function provisionBucketForRow(pgClient, databaseId, bucketKey, bucketType
|
|
|
202
223
|
/**
|
|
203
224
|
* Update CORS on an existing S3 bucket when allowed_origins changes.
|
|
204
225
|
*/
|
|
205
|
-
async function updateBucketCors(pgClient, databaseId, bucketKey, bucketType, bucketAllowedOrigins, options) {
|
|
206
|
-
const s3BucketName = resolveBucketName(bucketKey, databaseId, options);
|
|
226
|
+
async function updateBucketCors(pgClient, databaseId, bucketKey, bucketType, bucketAllowedOrigins, options, s3BucketName) {
|
|
207
227
|
const accessType = bucketType;
|
|
208
228
|
const storageModule = await resolveStorageModule(pgClient, databaseId);
|
|
209
229
|
const effectiveOrigins = resolveAllowedOrigins(bucketAllowedOrigins, storageModule?.allowed_origins, options.allowedOrigins);
|
|
@@ -308,11 +328,11 @@ function createBucketProvisionerPlugin(options) {
|
|
|
308
328
|
const hasOwner = ownerId && storageModule.scope !== 'app';
|
|
309
329
|
const bucketsTable = quotes_1.QuoteUtils.quoteQualifiedIdentifier(storageModule.buckets_schema, storageModule.buckets_table);
|
|
310
330
|
const bucketResult = await pgClient.query(hasOwner
|
|
311
|
-
? `SELECT id, key, type, is_public, allowed_origins
|
|
331
|
+
? `SELECT id, key, type, is_public, allowed_origins, physical_name
|
|
312
332
|
FROM ${bucketsTable}
|
|
313
333
|
WHERE key = $1 AND owner_id = $2
|
|
314
334
|
LIMIT 1`
|
|
315
|
-
: `SELECT id, key, type, is_public, allowed_origins
|
|
335
|
+
: `SELECT id, key, type, is_public, allowed_origins, physical_name
|
|
316
336
|
FROM ${bucketsTable}
|
|
317
337
|
WHERE key = $1
|
|
318
338
|
LIMIT 1`, hasOwner ? [bucketKey, ownerId] : [bucketKey]);
|
|
@@ -320,8 +340,16 @@ function createBucketProvisionerPlugin(options) {
|
|
|
320
340
|
throw new Error('BUCKET_NOT_FOUND');
|
|
321
341
|
}
|
|
322
342
|
const bucket = bucketResult.rows[0];
|
|
343
|
+
// First provision mints a name; afterwards the stored coordinate
|
|
344
|
+
// is authoritative and the naming hook is never consulted again.
|
|
345
|
+
const recorded = storedPhysicalName(bucket);
|
|
346
|
+
const s3BucketName = recorded === null
|
|
347
|
+
? resolveBucketName(bucket.key, databaseId, options)
|
|
348
|
+
: recorded;
|
|
323
349
|
try {
|
|
324
|
-
const result = await provisionBucketForRow(pgClient, databaseId, bucket.key, bucket.type, bucket.allowed_origins, options);
|
|
350
|
+
const result = await provisionBucketForRow(pgClient, databaseId, bucket.key, bucket.type, bucket.allowed_origins, options, s3BucketName);
|
|
351
|
+
// Record the exact provisioned name on the source row.
|
|
352
|
+
await recordPhysicalName(withPgClient, bucketsTable, bucket.id, result.bucketName);
|
|
325
353
|
return {
|
|
326
354
|
success: true,
|
|
327
355
|
bucketName: result.bucketName,
|
|
@@ -335,7 +363,7 @@ function createBucketProvisionerPlugin(options) {
|
|
|
335
363
|
log.error(`Failed to provision bucket "${bucketKey}": ${err.message}`);
|
|
336
364
|
return {
|
|
337
365
|
success: false,
|
|
338
|
-
bucketName:
|
|
366
|
+
bucketName: s3BucketName,
|
|
339
367
|
accessType: bucket.type,
|
|
340
368
|
provider: resolveConnection(options).provider,
|
|
341
369
|
endpoint: resolveConnection(options).endpoint ?? null,
|
|
@@ -421,7 +449,17 @@ function createBucketProvisionerPlugin(options) {
|
|
|
421
449
|
log.warn('Auto-provision skipped: could not resolve database_id');
|
|
422
450
|
return;
|
|
423
451
|
}
|
|
424
|
-
|
|
452
|
+
// Newly-created row has no stored coordinate yet — mint on first provision.
|
|
453
|
+
const result = await provisionBucketForRow(pgClient, databaseId, bucketInput.key, bucketInput.type, bucketInput.allowedOrigins ?? bucketInput.allowed_origins ?? null, options, resolveBucketName(bucketInput.key, databaseId, options));
|
|
454
|
+
// Record the provisioned name on the just-created row.
|
|
455
|
+
const storageModule = await resolveStorageModule(pgClient, databaseId);
|
|
456
|
+
if (storageModule) {
|
|
457
|
+
const bucketsTable = quotes_1.QuoteUtils.quoteQualifiedIdentifier(storageModule.buckets_schema, storageModule.buckets_table);
|
|
458
|
+
const idResult = await pgClient.query(`SELECT id FROM ${bucketsTable} WHERE key = $1 LIMIT 1`, [bucketInput.key]);
|
|
459
|
+
const bucketId = idResult.rows[0]?.id;
|
|
460
|
+
if (bucketId)
|
|
461
|
+
await recordPhysicalName(withPgClient, bucketsTable, bucketId, result.bucketName);
|
|
462
|
+
}
|
|
425
463
|
});
|
|
426
464
|
}
|
|
427
465
|
else {
|
|
@@ -455,7 +493,7 @@ function createBucketProvisionerPlugin(options) {
|
|
|
455
493
|
}
|
|
456
494
|
// Read the full bucket row (post-update) to get type + origins
|
|
457
495
|
const bucketsTable = quotes_1.QuoteUtils.quoteQualifiedIdentifier(storageModule.buckets_schema, storageModule.buckets_table);
|
|
458
|
-
const bucketResult = await pgClient.query(`SELECT id, key, type, is_public, allowed_origins
|
|
496
|
+
const bucketResult = await pgClient.query(`SELECT id, key, type, is_public, allowed_origins, physical_name
|
|
459
497
|
FROM ${bucketsTable}
|
|
460
498
|
WHERE key = $1
|
|
461
499
|
LIMIT 1`, [patchKey]);
|
|
@@ -464,7 +502,13 @@ function createBucketProvisionerPlugin(options) {
|
|
|
464
502
|
return;
|
|
465
503
|
}
|
|
466
504
|
const bucket = bucketResult.rows[0];
|
|
467
|
-
|
|
505
|
+
// CORS applies to the recorded physical bucket; if the row was
|
|
506
|
+
// never provisioned there is nothing to update yet, so mint the
|
|
507
|
+
// conventional name the first provision would use.
|
|
508
|
+
const recorded = storedPhysicalName(bucket);
|
|
509
|
+
await updateBucketCors(pgClient, databaseId, bucket.key, bucket.type, bucket.allowed_origins, options, recorded === null
|
|
510
|
+
? resolveBucketName(bucket.key, databaseId, options)
|
|
511
|
+
: recorded);
|
|
468
512
|
});
|
|
469
513
|
}
|
|
470
514
|
}
|