UNPKG

graphile-settings

Version:
155 lines (154 loc) 5.61 kB
"use strict"; /** * Upload resolver for the Constructive upload plugin. * * Reads CDN/S3/MinIO configuration from environment variables (via getEnvOptions) * and streams uploaded files to the configured storage backend. * * Lazily initializes the S3 streamer on first upload to avoid requiring * env vars at module load time. * * ENV VARS: * BUCKET_PROVIDER - 'minio' | 's3' (default: 'minio') * BUCKET_NAME - bucket name (default: 'test-bucket') * AWS_REGION - AWS region (default: 'us-east-1') * AWS_ACCESS_KEY - access key (default: 'minioadmin') * AWS_SECRET_KEY - secret key (default: 'minioadmin') * CDN_ENDPOINT - S3-compatible endpoint (default: 'http://localhost:9000') */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.constructiveUploadFieldDefinitions = void 0; const s3_streamer_1 = __importDefault(require("@constructive-io/s3-streamer")); const upload_names_1 = __importDefault(require("@constructive-io/upload-names")); const graphql_env_1 = require("@constructive-io/graphql-env"); const logger_1 = require("@pgpmjs/logger"); const crypto_1 = require("crypto"); const log = new logger_1.Logger('upload-resolver'); const DEFAULT_IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/svg+xml']; let streamer = null; let bucketName; function getStreamer() { if (streamer) return streamer; const opts = (0, graphql_env_1.getEnvOptions)(); const cdn = opts.cdn || {}; const provider = cdn.provider || 'minio'; bucketName = cdn.bucketName || 'test-bucket'; const awsRegion = cdn.awsRegion || 'us-east-1'; const awsAccessKey = cdn.awsAccessKey || 'minioadmin'; const awsSecretKey = cdn.awsSecretKey || 'minioadmin'; const endpoint = cdn.endpoint || 'http://localhost:9000'; if (process.env.NODE_ENV === 'production') { if (!cdn.awsAccessKey || !cdn.awsSecretKey) { log.warn('[upload-resolver] WARNING: Using default credentials in production.'); } } log.info(`[upload-resolver] Initializing: provider=${provider} bucket=${bucketName}`); streamer = new s3_streamer_1.default({ defaultBucket: bucketName, awsRegion, awsSecretKey, awsAccessKey, endpoint, provider, }); return streamer; } /** * Generates a randomized storage key from a filename. * Format: {random10chars}-{sanitized-filename} */ function generateKey(filename) { const rand = (0, crypto_1.randomBytes)(12).toString('hex'); return `${rand}-${(0, upload_names_1.default)(filename)}`; } /** * Upload resolver that streams files to S3/MinIO. * * Returns different shapes based on the column's type hint: * - 'image' / 'upload' → { filename, mime, url } (for jsonb domain columns) * - 'attachment' / default → url string (for text domain columns) * * MIME validation happens before persistence: content type is detected from * stream bytes, validated against smart-tag/type rules, and only then uploaded. */ async function uploadResolver(upload, _args, _context, info) { const { tags, type } = info.uploadPlugin; const s3 = getStreamer(); const { filename } = upload; const key = generateKey(filename); // MIME type validation from smart tags const typ = type || tags?.type; const VALID_MIME = /^[a-z]+\/[a-z0-9][a-z0-9!#$&\-.^_+]*$/i; const mim = tags?.mime ? String(tags.mime) .trim() .split(',') .map((a) => a.trim()) .filter((m) => VALID_MIME.test(m)) : typ === 'image' ? DEFAULT_IMAGE_MIME_TYPES : []; const detected = await s3.detectContentType({ readStream: upload.createReadStream(), filename, }); const detectedContentType = detected.contentType; if (mim.length && !mim.includes(detectedContentType)) { detected.stream.destroy(); throw new Error('UPLOAD_MIMETYPE'); } const result = await s3.uploadWithContentType({ readStream: detected.stream, contentType: detectedContentType, magic: detected.magic, key, bucket: bucketName, }); const url = result.upload.Location; const { contentType } = result; switch (typ) { case 'image': case 'upload': return { filename, mime: contentType, url }; case 'attachment': default: return url; } } /** * Upload field definitions for Constructive's three upload domain types. * * These match columns whose PostgreSQL type is one of the domains defined * in constructive-db/pgpm-modules/types/: * * - `image` (public schema) — jsonb domain for images with versions * - `upload` (public schema) — jsonb domain for generic file uploads * - `attachment` (public schema) — text domain for simple URL attachments * * These domain types are part of the platform's core type system, deployed * to every application database. They rarely change, so this config is stable. */ exports.constructiveUploadFieldDefinitions = [ { name: 'image', namespaceName: 'public', type: 'image', resolve: uploadResolver, }, { name: 'upload', namespaceName: 'public', type: 'upload', resolve: uploadResolver, }, { name: 'attachment', namespaceName: 'public', type: 'attachment', resolve: uploadResolver, }, ];