graphile-settings
Version:
graphile settings
131 lines (130 loc) • 5.29 kB
JavaScript
/**
* Presigned URL resolver for the Constructive presigned URL plugin.
*
* Reads CDN/S3 configuration from the standard env system
* (getEnvOptions → pgpmDefaults + config files + env vars) and lazily
* initializes an S3Client on first use.
*
* Also provides a per-database bucket name resolver that derives the
* S3 bucket name from the database UUID + a configurable prefix.
*
* Follows the same lazy-init pattern as upload-resolver.ts.
*/
import { createS3Client } from '@constructive-io/s3-utils';
import { getEnvOptions } from '@constructive-io/graphql-env';
import { Logger } from '@pgpmjs/logger';
import { BucketProvisioner } from '@constructive-io/bucket-provisioner';
import { getBucketProvisionerConnection } from './bucket-provisioner-resolver';
const log = new Logger('presigned-url-resolver');
let s3Config = null;
/**
* Lazily initialize and return the S3Config for the presigned URL plugin.
*
* Reads CDN config on first call via getEnvOptions() (which already merges
* pgpmDefaults → config file → env vars), creates an S3Client, and caches
* the result. Same CDN config as upload-resolver.ts.
*
* NOTE: The `bucket` field here is the global fallback bucket name
* (from BUCKET_NAME env var). When `resolveBucketName` is provided,
* per-database bucket names take precedence for all S3 operations.
*/
export function getPresignedUrlS3Config() {
if (s3Config)
return s3Config;
const { cdn } = getEnvOptions();
if (!cdn) {
throw new Error('[presigned-url-resolver] CDN config not found. ' +
'Ensure CDN environment variables (AWS_ACCESS_KEY, AWS_SECRET_KEY, etc.) ' +
'are set or that pgpmDefaults provides CDN fields.');
}
const { bucketName, awsRegion, awsAccessKey, awsSecretKey, endpoint, publicUrlPrefix } = cdn;
if (!awsAccessKey || !awsSecretKey) {
throw new Error('[presigned-url-resolver] Missing S3 credentials. ' +
'Set AWS_ACCESS_KEY and AWS_SECRET_KEY environment variables.');
}
if (!bucketName) {
throw new Error('[presigned-url-resolver] Missing CDN bucket name. ' +
'Set CDN_BUCKET_NAME environment variable.');
}
log.info(`[presigned-url-resolver] Initializing: bucket=${bucketName} endpoint=${endpoint}`);
const client = createS3Client({
provider: (cdn.provider || 'minio'),
region: awsRegion,
accessKeyId: awsAccessKey,
secretAccessKey: awsSecretKey,
...(endpoint ? { endpoint } : {}),
});
s3Config = {
client,
bucket: bucketName,
region: awsRegion,
publicUrlPrefix,
...(endpoint ? { endpoint, forcePathStyle: true } : {}),
};
return s3Config;
}
/**
* Create a per-(database, bucketKey) bucket name resolver.
*
* Uses the BUCKET_NAME env var as a prefix. For each (database, bucketKey)
* pair, the S3 bucket name becomes `{prefix}-{bucketKey}-{databaseId}`
* (e.g., "myapp-public-abc123def456").
*
* This aligns with the bucket provisioner plugin which creates separate
* S3 buckets per logical bucket key.
*/
export function createBucketNameResolver() {
const { cdn } = getEnvOptions();
const prefix = cdn?.bucketName || 'test-bucket';
return (databaseId, bucketKey) => {
return `${prefix}-${bucketKey}-${databaseId}`;
};
}
/**
* Resolve CORS allowed origins from the env/config system.
*
* Reads SERVER_ORIGIN from the standard env hierarchy
* (pgpmDefaults → config file → env vars) and wraps it in an array.
* Falls back to ['http://localhost:3000'] for local development.
*/
export function getAllowedOrigins() {
const { server } = getEnvOptions();
if (server?.origin)
return [server.origin];
return ['*'];
}
/**
* Create a lazy bucket provisioner callback for the presigned URL plugin.
*
* On the first upload to an S3 bucket that doesn't exist yet, this callback
* uses the BucketProvisioner to create and fully configure the bucket
* (Block Public Access, CORS, policies, lifecycle rules for temp buckets).
*
* Uses the same S3 connection config as the bucket provisioner plugin
* (getBucketProvisionerConnection) and reads CORS origins from
* SERVER_ORIGIN env var (falls back to localhost for local dev).
*/
export function createEnsureBucketProvisioned() {
let provisioner = null;
return async (bucketName, accessType, databaseId, allowedOrigins) => {
// Per-database origins from storage_module, falling back to global SERVER_ORIGIN
const effectiveOrigins = (allowedOrigins && allowedOrigins.length > 0)
? allowedOrigins
: getAllowedOrigins();
if (!provisioner) {
provisioner = new BucketProvisioner({
connection: getBucketProvisionerConnection(),
allowedOrigins: effectiveOrigins,
});
}
log.info(`[lazy-provision] Provisioning S3 bucket "${bucketName}" ` +
`(type=${accessType}) for database ${databaseId}`);
await provisioner.provision({
bucketName,
accessType,
versioning: false,
allowedOrigins: effectiveOrigins,
});
log.info(`[lazy-provision] S3 bucket "${bucketName}" provisioned successfully`);
};
}