Skip to content

Commit cf436df

Browse files
committed
feat: consolidate S3 client factory + presigned URL helpers into s3-utils
- Add createS3Client() factory to @constructive-io/s3-utils as the canonical S3 client creation point for all providers (AWS, MinIO, R2, GCS, DigitalOcean Spaces) - Add presignPutUrl(), presignGetUrl(), headObject() presigned URL helpers - Re-export S3Client and S3ClientConfig from @aws-sdk/client-s3 so consumers don't need a direct dependency - Update s3-streamer to delegate client creation to s3-utils - Update bucket-provisioner to delegate client creation to s3-utils (with backward-compatible ProvisionerError re-throw) - Update graphile-settings presigned-url-resolver to use createS3Client - Update graphql/server create-bucket script to use createS3Client - Add 21 unit tests for client factory + presigned URL helpers
1 parent d172d10 commit cf436df

14 files changed

Lines changed: 3159 additions & 7530 deletions

File tree

graphile/graphile-settings/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"@constructive-io/graphql-env": "workspace:^",
3535
"@constructive-io/graphql-types": "workspace:^",
3636
"@constructive-io/s3-streamer": "workspace:^",
37+
"@constructive-io/s3-utils": "workspace:^",
3738
"@constructive-io/upload-names": "workspace:^",
3839
"@dataplan/json": "1.0.0",
3940
"@dataplan/pg": "1.0.0",

graphile/graphile-settings/src/presigned-url-resolver.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
* Follows the same lazy-init pattern as upload-resolver.ts.
1212
*/
1313

14-
import { S3Client } from '@aws-sdk/client-s3';
14+
import { createS3Client } from '@constructive-io/s3-utils';
1515
import { getEnvOptions } from '@constructive-io/graphql-env';
1616
import { Logger } from '@pgpmjs/logger';
1717
import type { S3Config, BucketNameResolver, EnsureBucketProvisioned } from 'graphile-presigned-url-plugin';
@@ -66,10 +66,12 @@ export function getPresignedUrlS3Config(): S3Config {
6666
`[presigned-url-resolver] Initializing: bucket=${bucketName} endpoint=${endpoint}`,
6767
);
6868

69-
const client = new S3Client({
69+
const client = createS3Client({
70+
provider: (cdn.provider || 'minio') as any,
7071
region: awsRegion,
71-
credentials: { accessKeyId: awsAccessKey, secretAccessKey: awsSecretKey },
72-
...(endpoint ? { endpoint, forcePathStyle: true } : {}),
72+
accessKeyId: awsAccessKey,
73+
secretAccessKey: awsSecretKey,
74+
...(endpoint ? { endpoint } : {}),
7375
});
7476

7577
s3Config = {

graphql/server/src/scripts/create-bucket.ts

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
// Minimal script to create a bucket in MinIO/S3 using @constructive-io/s3-utils
2-
// Avoid strict type coupling between different @aws-sdk/client-s3 versions
32

4-
import { S3Client } from '@aws-sdk/client-s3';
5-
import { createS3Bucket } from '@constructive-io/s3-utils';
3+
import { createS3Client, createS3Bucket } from '@constructive-io/s3-utils';
4+
import type { StorageProvider } from '@constructive-io/s3-utils';
65
import { getEnvOptions } from '@constructive-io/graphql-env';
76
import { Logger } from '@pgpmjs/logger';
87

@@ -13,22 +12,19 @@ const log = new Logger('create-bucket');
1312
const opts = getEnvOptions();
1413
const { cdn } = opts;
1514

16-
const provider = cdn?.provider || 'minio';
17-
const isMinio = provider === 'minio';
18-
15+
const provider = (cdn?.provider || 'minio') as StorageProvider;
1916
const bucket = cdn?.bucketName || 'test-bucket';
2017
const region = cdn?.awsRegion || 'us-east-1';
2118
const accessKey = cdn?.awsAccessKey || 'minioadmin';
2219
const secretKey = cdn?.awsSecretKey || 'minioadmin';
2320
const endpoint = cdn?.endpoint || 'http://localhost:9000';
2421

25-
const client: any = new S3Client({
22+
const client = createS3Client({
23+
provider,
2624
region,
27-
credentials: { accessKeyId: accessKey, secretAccessKey: secretKey },
28-
...(isMinio ? {
29-
endpoint,
30-
forcePathStyle: true,
31-
} : {}),
25+
accessKeyId: accessKey,
26+
secretAccessKey: secretKey,
27+
...(endpoint ? { endpoint } : {}),
3228
});
3329

3430
const res = await createS3Bucket(client as any, bucket, { provider });

packages/bucket-provisioner/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@
2929
"test:watch": "jest --watch"
3030
},
3131
"dependencies": {
32-
"@aws-sdk/client-s3": "^3.1009.0"
32+
"@aws-sdk/client-s3": "^3.1009.0",
33+
"@constructive-io/s3-utils": "workspace:^"
3334
},
3435
"devDependencies": {
3536
"makage": "^0.3.0"
Lines changed: 19 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,34 @@
11
/**
22
* S3 client factory.
33
*
4-
* Creates a configured S3Client from a StorageConnectionConfig.
5-
* Handles provider-specific settings (path-style for MinIO, etc.).
4+
* Delegates to @constructive-io/s3-utils for the actual S3Client creation.
5+
* Re-exports createS3Client so existing consumers of bucket-provisioner
6+
* continue to work without changes.
67
*/
78

8-
import { S3Client } from '@aws-sdk/client-s3';
9+
import { createS3Client as createS3ClientFromUtils, S3ConfigError } from '@constructive-io/s3-utils';
10+
import type { S3Client } from '@aws-sdk/client-s3';
911
import type { StorageConnectionConfig } from './types';
1012
import { ProvisionerError } from './types';
13+
import type { ProvisionerErrorCode } from './types';
1114

1215
/**
1316
* Create an S3Client from a storage connection config.
1417
*
15-
* Provider-specific defaults:
16-
* - `minio`: forces path-style URLs (required by MinIO)
17-
* - `r2`: forces path-style URLs (required by Cloudflare R2)
18-
* - `s3`: uses virtual-hosted style (AWS default)
19-
* - `gcs`: forces path-style URLs (GCS S3-compatible API)
20-
* - `spaces`: uses virtual-hosted style (DigitalOcean default)
18+
* Delegates to @constructive-io/s3-utils/createS3Client.
19+
* This wrapper exists for backward compatibility — new code should
20+
* import createS3Client from @constructive-io/s3-utils directly.
21+
*
22+
* Catches S3ConfigError from s3-utils and re-throws as ProvisionerError
23+
* so existing consumers that catch ProvisionerError continue to work.
2124
*/
2225
export function createS3Client(config: StorageConnectionConfig): S3Client {
23-
if (!config.accessKeyId || !config.secretAccessKey) {
24-
throw new ProvisionerError(
25-
'INVALID_CONFIG',
26-
'accessKeyId and secretAccessKey are required',
27-
);
28-
}
29-
30-
if (!config.region) {
31-
throw new ProvisionerError(
32-
'INVALID_CONFIG',
33-
'region is required',
34-
);
26+
try {
27+
return createS3ClientFromUtils(config);
28+
} catch (err) {
29+
if (err instanceof S3ConfigError) {
30+
throw new ProvisionerError(err.code as ProvisionerErrorCode, err.message);
31+
}
32+
throw err;
3533
}
36-
37-
// Providers that require path-style URLs
38-
const pathStyleProviders = new Set(['minio', 'r2', 'gcs']);
39-
const forcePathStyle = config.forcePathStyle ?? pathStyleProviders.has(config.provider);
40-
41-
// Non-AWS providers require an endpoint
42-
if (config.provider !== 's3' && !config.endpoint) {
43-
throw new ProvisionerError(
44-
'INVALID_CONFIG',
45-
`endpoint is required for provider '${config.provider}'`,
46-
);
47-
}
48-
49-
return new S3Client({
50-
region: config.region,
51-
endpoint: config.endpoint,
52-
forcePathStyle,
53-
credentials: {
54-
accessKeyId: config.accessKeyId,
55-
secretAccessKey: config.secretAccessKey,
56-
},
57-
});
5834
}

0 commit comments

Comments
 (0)