Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions graphile/graphile-settings/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"@constructive-io/graphql-env": "workspace:^",
"@constructive-io/graphql-types": "workspace:^",
"@constructive-io/s3-streamer": "workspace:^",
"@constructive-io/s3-utils": "workspace:^",
"@constructive-io/upload-names": "workspace:^",
"@dataplan/json": "1.0.0",
"@dataplan/pg": "1.0.0",
Expand Down
10 changes: 6 additions & 4 deletions graphile/graphile-settings/src/presigned-url-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
* Follows the same lazy-init pattern as upload-resolver.ts.
*/

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

const client = new S3Client({
const client = createS3Client({
provider: (cdn.provider || 'minio') as any,
region: awsRegion,
credentials: { accessKeyId: awsAccessKey, secretAccessKey: awsSecretKey },
...(endpoint ? { endpoint, forcePathStyle: true } : {}),
accessKeyId: awsAccessKey,
secretAccessKey: awsSecretKey,
...(endpoint ? { endpoint } : {}),
});

s3Config = {
Expand Down
20 changes: 8 additions & 12 deletions graphql/server/src/scripts/create-bucket.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
// Minimal script to create a bucket in MinIO/S3 using @constructive-io/s3-utils
// Avoid strict type coupling between different @aws-sdk/client-s3 versions

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

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

const provider = cdn?.provider || 'minio';
const isMinio = provider === 'minio';

const provider = (cdn?.provider || 'minio') as StorageProvider;
const bucket = cdn?.bucketName || 'test-bucket';
const region = cdn?.awsRegion || 'us-east-1';
const accessKey = cdn?.awsAccessKey || 'minioadmin';
const secretKey = cdn?.awsSecretKey || 'minioadmin';
const endpoint = cdn?.endpoint || 'http://localhost:9000';

const client: any = new S3Client({
const client = createS3Client({
provider,
region,
credentials: { accessKeyId: accessKey, secretAccessKey: secretKey },
...(isMinio ? {
endpoint,
forcePathStyle: true,
} : {}),
accessKeyId: accessKey,
secretAccessKey: secretKey,
...(endpoint ? { endpoint } : {}),
});

const res = await createS3Bucket(client as any, bucket, { provider });
Expand Down
3 changes: 2 additions & 1 deletion packages/bucket-provisioner/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@
"test:watch": "jest --watch"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.1009.0"
"@aws-sdk/client-s3": "^3.1009.0",
"@constructive-io/s3-utils": "workspace:^"
},
"devDependencies": {
"makage": "^0.3.0"
Expand Down
62 changes: 19 additions & 43 deletions packages/bucket-provisioner/src/client.ts
Original file line number Diff line number Diff line change
@@ -1,58 +1,34 @@
/**
* S3 client factory.
*
* Creates a configured S3Client from a StorageConnectionConfig.
* Handles provider-specific settings (path-style for MinIO, etc.).
* Delegates to @constructive-io/s3-utils for the actual S3Client creation.
* Re-exports createS3Client so existing consumers of bucket-provisioner
* continue to work without changes.
*/

import { S3Client } from '@aws-sdk/client-s3';
import { createS3Client as createS3ClientFromUtils, S3ConfigError } from '@constructive-io/s3-utils';
import type { S3Client } from '@aws-sdk/client-s3';
import type { StorageConnectionConfig } from './types';
import { ProvisionerError } from './types';
import type { ProvisionerErrorCode } from './types';

/**
* Create an S3Client from a storage connection config.
*
* Provider-specific defaults:
* - `minio`: forces path-style URLs (required by MinIO)
* - `r2`: forces path-style URLs (required by Cloudflare R2)
* - `s3`: uses virtual-hosted style (AWS default)
* - `gcs`: forces path-style URLs (GCS S3-compatible API)
* - `spaces`: uses virtual-hosted style (DigitalOcean default)
* Delegates to @constructive-io/s3-utils/createS3Client.
* This wrapper exists for backward compatibility — new code should
* import createS3Client from @constructive-io/s3-utils directly.
*
* Catches S3ConfigError from s3-utils and re-throws as ProvisionerError
* so existing consumers that catch ProvisionerError continue to work.
*/
export function createS3Client(config: StorageConnectionConfig): S3Client {
if (!config.accessKeyId || !config.secretAccessKey) {
throw new ProvisionerError(
'INVALID_CONFIG',
'accessKeyId and secretAccessKey are required',
);
}

if (!config.region) {
throw new ProvisionerError(
'INVALID_CONFIG',
'region is required',
);
try {
return createS3ClientFromUtils(config);
} catch (err) {
if (err instanceof S3ConfigError) {
throw new ProvisionerError(err.code as ProvisionerErrorCode, err.message);
}
throw err;
}

// Providers that require path-style URLs
const pathStyleProviders = new Set(['minio', 'r2', 'gcs']);
const forcePathStyle = config.forcePathStyle ?? pathStyleProviders.has(config.provider);

// Non-AWS providers require an endpoint
if (config.provider !== 's3' && !config.endpoint) {
throw new ProvisionerError(
'INVALID_CONFIG',
`endpoint is required for provider '${config.provider}'`,
);
}

return new S3Client({
region: config.region,
endpoint: config.endpoint,
forcePathStyle,
credentials: {
accessKeyId: config.accessKeyId,
secretAccessKey: config.secretAccessKey,
},
});
}
Loading
Loading