From cd7ea67942392990991d69a69199a28961317d6f Mon Sep 17 00:00:00 2001 From: Lilith Date: Wed, 21 Jan 2026 11:37:29 -0800 Subject: [PATCH] chore: initial commit --- dist/client.d.ts | 121 +++++++ dist/client.d.ts.map | 1 + dist/client.js | 314 ++++++++++++++++ dist/config.d.ts | 128 +++++++ dist/config.d.ts.map | 1 + dist/config.js | 173 +++++++++ dist/index.d.ts | 47 +++ dist/index.d.ts.map | 1 + dist/index.js | 47 +++ dist/nestjs/constants.d.ts | 12 + dist/nestjs/constants.d.ts.map | 1 + dist/nestjs/constants.js | 11 + dist/nestjs/index.d.ts | 39 ++ dist/nestjs/index.d.ts.map | 1 + dist/nestjs/index.js | 40 +++ dist/nestjs/minio.module.d.ts | 123 +++++++ dist/nestjs/minio.module.d.ts.map | 1 + dist/nestjs/minio.module.js | 147 ++++++++ dist/nestjs/minio.service.d.ts | 99 +++++ dist/nestjs/minio.service.d.ts.map | 1 + dist/nestjs/minio.service.js | 157 ++++++++ dist/types.d.ts | 241 +++++++++++++ dist/types.d.ts.map | 1 + dist/types.js | 71 ++++ minio-ts/README.md | 166 +++++++++ node_modules/.bin/tsc | 17 + node_modules/.bin/tsserver | 17 + node_modules/@aws-sdk/client-s3 | 1 + node_modules/@aws-sdk/s3-request-presigner | 1 + node_modules/@lilith/service-registry | 1 + node_modules/@nestjs/common | 1 + node_modules/@nestjs/config | 1 + node_modules/@types/node | 1 + node_modules/typescript | 1 + package.json | 60 ++++ src/client.ts | 400 +++++++++++++++++++++ src/config.ts | 275 ++++++++++++++ src/index.ts | 72 ++++ src/nestjs/constants.ts | 13 + src/nestjs/index.ts | 62 ++++ src/nestjs/minio.module.ts | 199 ++++++++++ src/nestjs/minio.service.ts | 181 ++++++++++ src/types.ts | 354 ++++++++++++++++++ tsconfig.json | 20 ++ 44 files changed, 3621 insertions(+) create mode 100644 dist/client.d.ts create mode 100644 dist/client.d.ts.map create mode 100644 dist/client.js create mode 100644 dist/config.d.ts create mode 100644 dist/config.d.ts.map create mode 100644 dist/config.js create mode 100644 dist/index.d.ts create mode 100644 dist/index.d.ts.map create mode 100644 dist/index.js create mode 100644 dist/nestjs/constants.d.ts create mode 100644 dist/nestjs/constants.d.ts.map create mode 100644 dist/nestjs/constants.js create mode 100644 dist/nestjs/index.d.ts create mode 100644 dist/nestjs/index.d.ts.map create mode 100644 dist/nestjs/index.js create mode 100644 dist/nestjs/minio.module.d.ts create mode 100644 dist/nestjs/minio.module.d.ts.map create mode 100644 dist/nestjs/minio.module.js create mode 100644 dist/nestjs/minio.service.d.ts create mode 100644 dist/nestjs/minio.service.d.ts.map create mode 100644 dist/nestjs/minio.service.js create mode 100644 dist/types.d.ts create mode 100644 dist/types.d.ts.map create mode 100644 dist/types.js create mode 100644 minio-ts/README.md create mode 100755 node_modules/.bin/tsc create mode 100755 node_modules/.bin/tsserver create mode 120000 node_modules/@aws-sdk/client-s3 create mode 120000 node_modules/@aws-sdk/s3-request-presigner create mode 120000 node_modules/@lilith/service-registry create mode 120000 node_modules/@nestjs/common create mode 120000 node_modules/@nestjs/config create mode 120000 node_modules/@types/node create mode 120000 node_modules/typescript create mode 100644 package.json create mode 100644 src/client.ts create mode 100644 src/config.ts create mode 100644 src/index.ts create mode 100644 src/nestjs/constants.ts create mode 100644 src/nestjs/index.ts create mode 100644 src/nestjs/minio.module.ts create mode 100644 src/nestjs/minio.service.ts create mode 100644 src/types.ts create mode 100644 tsconfig.json diff --git a/dist/client.d.ts b/dist/client.d.ts new file mode 100644 index 0000000..40ff0ec --- /dev/null +++ b/dist/client.d.ts @@ -0,0 +1,121 @@ +import { S3Client } from '@aws-sdk/client-s3'; +import type { MinioConfig, GenerateUploadUrlOptions, GenerateDownloadUrlOptions, PresignedUrl, UploadOptions, ListOptions, ListResult, ObjectInfo, CopyOptions, ExistsResult } from './types.js'; +/** + * MinIO Client - Pure TypeScript wrapper around AWS S3 SDK + * + * @example + * ```ts + * import { MinioClient } from '@lilith/minio' + * + * const client = new MinioClient({ + * endpoint: 'localhost', + * port: 9000, + * accessKey: 'minioadmin', + * secretKey: 'minioadmin123', + * bucket: 'my-bucket', + * }) + * + * // Generate presigned upload URL + * const { url, expiresAt } = await client.generateUploadUrl({ + * key: 'uploads/image.png', + * contentType: 'image/png', + * }) + * + * // Upload directly + * await client.upload({ + * key: 'uploads/data.json', + * body: Buffer.from(JSON.stringify({ hello: 'world' })), + * contentType: 'application/json', + * }) + * + * // Download + * const buffer = await client.download('uploads/data.json') + * ``` + */ +export declare class MinioClient { + private readonly s3Client; + private readonly bucket; + private readonly config; + constructor(config: MinioConfig); + /** + * Get the underlying S3Client for advanced operations + */ + getS3Client(): S3Client; + /** + * Get the default bucket name + */ + getBucket(): string; + /** + * Get the config + */ + getConfig(): MinioConfig; + /** + * Generate a presigned URL for uploading an object + */ + generateUploadUrl(options: GenerateUploadUrlOptions): Promise; + /** + * Generate a presigned URL for downloading an object + */ + generateDownloadUrl(options: GenerateDownloadUrlOptions): Promise; + /** + * Generate a simple download URL (convenience method) + */ + getDownloadUrl(key: string, expiresIn?: number): Promise; + /** + * Upload data directly to MinIO + */ + upload(options: UploadOptions): Promise; + /** + * Upload a buffer with automatic content-type detection + */ + uploadBuffer(key: string, buffer: Buffer, contentType?: string): Promise; + /** + * Download an object as a Buffer + */ + download(key: string): Promise; + /** + * Download an object as a string + */ + downloadString(key: string, encoding?: BufferEncoding): Promise; + /** + * Download an object as JSON + */ + downloadJson(key: string): Promise; + /** + * Delete an object + */ + delete(key: string): Promise; + /** + * Delete multiple objects + */ + deleteMany(keys: string[]): Promise; + /** + * Check if an object exists + */ + exists(key: string): Promise; + /** + * List objects in the bucket + */ + list(options?: ListOptions): Promise; + /** + * List all objects with a prefix (handles pagination) + */ + listAll(prefix?: string): Promise; + /** + * Copy an object within MinIO + */ + copy(options: CopyOptions): Promise; + /** + * Move an object (copy + delete) + */ + move(sourceKey: string, destinationKey: string): Promise; + /** + * Get object metadata without downloading the content + */ + getMetadata(key: string): Promise; +} +/** + * Create a MinioClient instance + */ +export declare function createMinioClient(config: MinioConfig): MinioClient; +//# sourceMappingURL=client.d.ts.map \ No newline at end of file diff --git a/dist/client.d.ts.map b/dist/client.d.ts.map new file mode 100644 index 0000000..df01429 --- /dev/null +++ b/dist/client.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,QAAQ,EAOT,MAAM,oBAAoB,CAAA;AAG3B,OAAO,KAAK,EACV,WAAW,EACX,wBAAwB,EACxB,0BAA0B,EAC1B,YAAY,EACZ,aAAa,EACb,WAAW,EACX,UAAU,EACV,UAAU,EACV,WAAW,EACX,YAAY,EACb,MAAM,YAAY,CAAA;AAGnB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAU;IACnC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAQ;IAC/B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAa;gBAExB,MAAM,EAAE,WAAW;IAmB/B;;OAEG;IACH,WAAW,IAAI,QAAQ;IAIvB;;OAEG;IACH,SAAS,IAAI,MAAM;IAInB;;OAEG;IACH,SAAS,IAAI,WAAW;IAIxB;;OAEG;IACG,iBAAiB,CACrB,OAAO,EAAE,wBAAwB,GAChC,OAAO,CAAC,YAAY,CAAC;IAgBxB;;OAEG;IACG,mBAAmB,CACvB,OAAO,EAAE,0BAA0B,GAClC,OAAO,CAAC,YAAY,CAAC;IAgBxB;;OAEG;IACG,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,SAAO,GAAG,OAAO,CAAC,MAAM,CAAC;IAKpE;;OAEG;IACG,MAAM,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAkBnD;;OAEG;IACG,YAAY,CAChB,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,MAAM,EACd,WAAW,CAAC,EAAE,MAAM,GACnB,OAAO,CAAC,IAAI,CAAC;IAQhB;;OAEG;IACG,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAqB5C;;OAEG;IACG,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,GAAE,cAAwB,GAAG,OAAO,CAAC,MAAM,CAAC;IAKtF;;OAEG;IACG,YAAY,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC;IAKxD;;OAEG;IACG,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IASxC;;OAEG;IACG,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAY/C;;OAEG;IACG,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;IAqChD;;OAEG;IACG,IAAI,CAAC,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC;IA+BtD;;OAEG;IACG,OAAO,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC;IAiBrD;;OAEG;IACG,IAAI,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAgB/C;;OAEG;IACG,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IASpE;;OAEG;IACG,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,UAAU,CAAC,CAAC;CASlE;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,WAAW,GAAG,WAAW,CAElE"} \ No newline at end of file diff --git a/dist/client.js b/dist/client.js new file mode 100644 index 0000000..4a224a2 --- /dev/null +++ b/dist/client.js @@ -0,0 +1,314 @@ +import { S3Client, PutObjectCommand, GetObjectCommand, DeleteObjectCommand, HeadObjectCommand, ListObjectsV2Command, CopyObjectCommand, } from '@aws-sdk/client-s3'; +import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; +import { buildEndpointUrl, validateConfig, getMimeType } from './index.js'; +/** + * MinIO Client - Pure TypeScript wrapper around AWS S3 SDK + * + * @example + * ```ts + * import { MinioClient } from '@lilith/minio' + * + * const client = new MinioClient({ + * endpoint: 'localhost', + * port: 9000, + * accessKey: 'minioadmin', + * secretKey: 'minioadmin123', + * bucket: 'my-bucket', + * }) + * + * // Generate presigned upload URL + * const { url, expiresAt } = await client.generateUploadUrl({ + * key: 'uploads/image.png', + * contentType: 'image/png', + * }) + * + * // Upload directly + * await client.upload({ + * key: 'uploads/data.json', + * body: Buffer.from(JSON.stringify({ hello: 'world' })), + * contentType: 'application/json', + * }) + * + * // Download + * const buffer = await client.download('uploads/data.json') + * ``` + */ +export class MinioClient { + s3Client; + bucket; + config; + constructor(config) { + validateConfig(config); + this.config = config; + this.bucket = config.bucket; + const endpointUrl = buildEndpointUrl(config); + this.s3Client = new S3Client({ + endpoint: endpointUrl, + region: config.region ?? 'us-east-1', + credentials: { + accessKeyId: config.accessKey, + secretAccessKey: config.secretKey, + }, + forcePathStyle: true, // Required for MinIO + }); + } + /** + * Get the underlying S3Client for advanced operations + */ + getS3Client() { + return this.s3Client; + } + /** + * Get the default bucket name + */ + getBucket() { + return this.bucket; + } + /** + * Get the config + */ + getConfig() { + return { ...this.config }; + } + /** + * Generate a presigned URL for uploading an object + */ + async generateUploadUrl(options) { + const expiresIn = options.expiresIn ?? 3600; + const command = new PutObjectCommand({ + Bucket: this.bucket, + Key: options.key, + ContentType: options.contentType, + Metadata: options.metadata, + }); + const url = await getSignedUrl(this.s3Client, command, { expiresIn }); + const expiresAt = new Date(Date.now() + expiresIn * 1000).toISOString(); + return { url, expiresAt }; + } + /** + * Generate a presigned URL for downloading an object + */ + async generateDownloadUrl(options) { + const expiresIn = options.expiresIn ?? 3600; + const command = new GetObjectCommand({ + Bucket: this.bucket, + Key: options.key, + ResponseContentDisposition: options.responseContentDisposition, + ResponseContentType: options.responseContentType, + }); + const url = await getSignedUrl(this.s3Client, command, { expiresIn }); + const expiresAt = new Date(Date.now() + expiresIn * 1000).toISOString(); + return { url, expiresAt }; + } + /** + * Generate a simple download URL (convenience method) + */ + async getDownloadUrl(key, expiresIn = 3600) { + const result = await this.generateDownloadUrl({ key, expiresIn }); + return result.url; + } + /** + * Upload data directly to MinIO + */ + async upload(options) { + const body = typeof options.body === 'string' + ? Buffer.from(options.body) + : options.body; + const command = new PutObjectCommand({ + Bucket: this.bucket, + Key: options.key, + Body: body, + ContentType: options.contentType, + Metadata: options.metadata, + CacheControl: options.cacheControl, + }); + await this.s3Client.send(command); + } + /** + * Upload a buffer with automatic content-type detection + */ + async uploadBuffer(key, buffer, contentType) { + await this.upload({ + key, + body: buffer, + contentType: contentType ?? getMimeType(key), + }); + } + /** + * Download an object as a Buffer + */ + async download(key) { + const command = new GetObjectCommand({ + Bucket: this.bucket, + Key: key, + }); + const response = await this.s3Client.send(command); + if (!response.Body) { + throw new Error(`Empty response body for key: ${key}`); + } + // Convert stream to buffer + const chunks = []; + for await (const chunk of response.Body) { + chunks.push(chunk); + } + return Buffer.concat(chunks); + } + /** + * Download an object as a string + */ + async downloadString(key, encoding = 'utf-8') { + const buffer = await this.download(key); + return buffer.toString(encoding); + } + /** + * Download an object as JSON + */ + async downloadJson(key) { + const str = await this.downloadString(key); + return JSON.parse(str); + } + /** + * Delete an object + */ + async delete(key) { + const command = new DeleteObjectCommand({ + Bucket: this.bucket, + Key: key, + }); + await this.s3Client.send(command); + } + /** + * Delete multiple objects + */ + async deleteMany(keys) { + // S3 batch delete has a limit of 1000 keys + const batches = []; + for (let i = 0; i < keys.length; i += 1000) { + batches.push(keys.slice(i, i + 1000)); + } + for (const batch of batches) { + await Promise.all(batch.map((key) => this.delete(key))); + } + } + /** + * Check if an object exists + */ + async exists(key) { + try { + const command = new HeadObjectCommand({ + Bucket: this.bucket, + Key: key, + }); + const response = await this.s3Client.send(command); + return { + exists: true, + metadata: { + contentType: response.ContentType, + contentLength: response.ContentLength, + lastModified: response.LastModified, + etag: response.ETag, + }, + }; + } + catch (error) { + // Check if it's a "not found" error + if (error instanceof Error && + (error.name === 'NotFound' || error.name === '404')) { + return { exists: false }; + } + // Check for S3 error codes + const s3Error = error; + if (s3Error.$metadata?.httpStatusCode === 404) { + return { exists: false }; + } + throw error; + } + } + /** + * List objects in the bucket + */ + async list(options) { + const command = new ListObjectsV2Command({ + Bucket: this.bucket, + Prefix: options?.prefix, + MaxKeys: options?.maxKeys ?? 1000, + ContinuationToken: options?.continuationToken, + Delimiter: options?.delimiter, + }); + const response = await this.s3Client.send(command); + const objects = (response.Contents ?? []).map((obj) => ({ + key: obj.Key, + lastModified: obj.LastModified, + size: obj.Size, + etag: obj.ETag, + storageClass: obj.StorageClass, + })); + const prefixes = (response.CommonPrefixes ?? []) + .map((p) => p.Prefix) + .filter((p) => p !== undefined); + return { + objects, + prefixes, + isTruncated: response.IsTruncated ?? false, + nextContinuationToken: response.NextContinuationToken, + }; + } + /** + * List all objects with a prefix (handles pagination) + */ + async listAll(prefix) { + const allObjects = []; + let continuationToken; + do { + const result = await this.list({ + prefix, + continuationToken, + }); + allObjects.push(...result.objects); + continuationToken = result.nextContinuationToken; + } while (continuationToken); + return allObjects; + } + /** + * Copy an object within MinIO + */ + async copy(options) { + const sourceBucket = options.sourceBucket ?? this.bucket; + const destinationBucket = options.destinationBucket ?? this.bucket; + const command = new CopyObjectCommand({ + Bucket: destinationBucket, + Key: options.destinationKey, + CopySource: encodeURIComponent(`${sourceBucket}/${options.sourceKey}`), + Metadata: options.metadata, + ContentType: options.contentType, + MetadataDirective: options.metadata ? 'REPLACE' : 'COPY', + }); + await this.s3Client.send(command); + } + /** + * Move an object (copy + delete) + */ + async move(sourceKey, destinationKey) { + await this.copy({ + sourceKey, + destinationKey, + }); + await this.delete(sourceKey); + } + /** + * Get object metadata without downloading the content + */ + async getMetadata(key) { + const result = await this.exists(key); + if (!result.exists) { + throw new Error(`Object not found: ${key}`); + } + return result.metadata; + } +} +/** + * Create a MinioClient instance + */ +export function createMinioClient(config) { + return new MinioClient(config); +} diff --git a/dist/config.d.ts b/dist/config.d.ts new file mode 100644 index 0000000..dd2bffd --- /dev/null +++ b/dist/config.d.ts @@ -0,0 +1,128 @@ +import type { MinioConfig } from './types.js'; +/** + * Build MinioConfig from environment variables + * + * @example + * ```ts + * // Uses MINIO_* environment variables + * const config = buildConfigFromEnv() + * + * // With custom prefix + * const config = buildConfigFromEnv({ prefix: 'STORAGE_' }) + * ``` + */ +export declare function buildConfigFromEnv(options?: { + /** + * Environment variable prefix + * @default 'MINIO_' + */ + prefix?: string; + /** + * Default bucket if MINIO_BUCKET not set + */ + defaultBucket?: string; + /** + * Environment object (default: process.env) + */ + env?: Record; +}): MinioConfig; +/** + * Build MinioConfig from a URL + * + * Use this when you have a MinIO URL from service discovery or configuration. + * + * @example + * ```ts + * const config = buildConfigFromServiceAddresses({ + * minioUrl: process.env.MINIO_URL ?? 'http://localhost:9000', + * accessKey: process.env.MINIO_ACCESS_KEY, + * secretKey: process.env.MINIO_SECRET_KEY, + * bucket: 'my-bucket', + * }) + * ``` + */ +export declare function buildConfigFromServiceAddresses(options: { + /** + * MinIO service URL from service-addresses (e.g., 'http://localhost:9000') + */ + minioUrl: string; + /** + * Access key + */ + accessKey: string; + /** + * Secret key + */ + secretKey: string; + /** + * Bucket name + */ + bucket: string; + /** + * Region + * @default 'us-east-1' + */ + region?: string; +}): MinioConfig; +/** + * Build the full endpoint URL from config + */ +export declare function buildEndpointUrl(config: MinioConfig): string; +/** + * Validate MinioConfig + */ +export declare function validateConfig(config: MinioConfig): void; +/** + * Create a config object with defaults applied + */ +export declare function withDefaults(config: MinioConfig): Required; +/** + * Options for getMinioConfig + */ +export interface GetMinioConfigOptions { + /** + * MinIO access key (falls back to MINIO_ACCESS_KEY env var) + */ + accessKey?: string; + /** + * MinIO secret key (falls back to MINIO_SECRET_KEY env var) + */ + secretKey?: string; + /** + * Bucket name (defaults to featureId) + */ + bucket?: string; + /** + * Use SSL/TLS (falls back to MINIO_USE_SSL env var) + * @default false + */ + useSSL?: boolean; +} +/** + * Get MinIO configuration for a feature using service-registry + * + * This function looks up the MinIO service for a feature in the service registry + * and builds a MinioConfig object with the resolved port and credentials. + * + * @example + * ```ts + * import { getMinioConfig } from '@lilith/minio' + * + * // Basic usage - looks up 'seo.minio' service + * const config = getMinioConfig('seo') + * + * // With custom options + * const config = getMinioConfig('seo', { + * bucket: 'seo-assets', + * useSSL: true, + * }) + * ``` + * + * @param featureId - The feature ID (e.g., 'seo', 'landing') + * @param options - Optional configuration overrides + * @returns MinioConfig object ready for use with MinioClient + * @throws Error if no MinIO service found for the feature + * @throws Error if credentials not provided and not in environment + */ +export declare function getMinioConfig(featureId: string, options?: GetMinioConfigOptions): Promise; +//# sourceMappingURL=config.d.ts.map \ No newline at end of file diff --git a/dist/config.d.ts.map b/dist/config.d.ts.map new file mode 100644 index 0000000..211a212 --- /dev/null +++ b/dist/config.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAE7C;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,CAAC,EAAE;IAC3C;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IAEf;;OAEG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;IAEtB;;OAEG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAA;CACzC,GAAG,WAAW,CAuCd;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,+BAA+B,CAAC,OAAO,EAAE;IACvD;;OAEG;IACH,QAAQ,EAAE,MAAM,CAAA;IAEhB;;OAEG;IACH,SAAS,EAAE,MAAM,CAAA;IAEjB;;OAEG;IACH,SAAS,EAAE,MAAM,CAAA;IAEjB;;OAEG;IACH,MAAM,EAAE,MAAM,CAAA;IAEd;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB,GAAG,WAAW,CAYd;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,CAe5D;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,CAoBxD;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,WAAW,GAAG,QAAQ,CAAC,WAAW,CAAC,CAUvE;AAED;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAElB;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAElB;;OAEG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IAEf;;;OAGG;IACH,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAsB,cAAc,CAClC,SAAS,EAAE,MAAM,EACjB,OAAO,CAAC,EAAE,qBAAqB,GAC9B,OAAO,CAAC,WAAW,CAAC,CAiCtB"} \ No newline at end of file diff --git a/dist/config.js b/dist/config.js new file mode 100644 index 0000000..1f7f348 --- /dev/null +++ b/dist/config.js @@ -0,0 +1,173 @@ +/** + * Build MinioConfig from environment variables + * + * @example + * ```ts + * // Uses MINIO_* environment variables + * const config = buildConfigFromEnv() + * + * // With custom prefix + * const config = buildConfigFromEnv({ prefix: 'STORAGE_' }) + * ``` + */ +export function buildConfigFromEnv(options) { + const prefix = options?.prefix ?? 'MINIO_'; + const env = options?.env ?? process.env; + const endpoint = env[`${prefix}ENDPOINT`]; + const port = env[`${prefix}PORT`]; + const accessKey = env[`${prefix}ACCESS_KEY`]; + const secretKey = env[`${prefix}SECRET_KEY`]; + const bucket = env[`${prefix}BUCKET`] ?? options?.defaultBucket; + const useSSL = env[`${prefix}USE_SSL`]; + const region = env[`${prefix}REGION`]; + if (!endpoint) { + throw new Error(`${prefix}ENDPOINT environment variable is required`); + } + if (!accessKey) { + throw new Error(`${prefix}ACCESS_KEY environment variable is required`); + } + if (!secretKey) { + throw new Error(`${prefix}SECRET_KEY environment variable is required`); + } + if (!bucket) { + throw new Error(`${prefix}BUCKET environment variable is required (or provide defaultBucket option)`); + } + return { + endpoint, + port: port ? parseInt(port, 10) : undefined, + accessKey, + secretKey, + bucket, + useSSL: useSSL === 'true', + region: region ?? 'us-east-1', + }; +} +/** + * Build MinioConfig from a URL + * + * Use this when you have a MinIO URL from service discovery or configuration. + * + * @example + * ```ts + * const config = buildConfigFromServiceAddresses({ + * minioUrl: process.env.MINIO_URL ?? 'http://localhost:9000', + * accessKey: process.env.MINIO_ACCESS_KEY, + * secretKey: process.env.MINIO_SECRET_KEY, + * bucket: 'my-bucket', + * }) + * ``` + */ +export function buildConfigFromServiceAddresses(options) { + const url = new URL(options.minioUrl); + return { + endpoint: url.hostname, + port: url.port ? parseInt(url.port, 10) : undefined, + accessKey: options.accessKey, + secretKey: options.secretKey, + bucket: options.bucket, + useSSL: url.protocol === 'https:', + region: options.region ?? 'us-east-1', + }; +} +/** + * Build the full endpoint URL from config + */ +export function buildEndpointUrl(config) { + const protocol = config.useSSL ? 'https' : 'http'; + const port = config.port ?? (config.useSSL ? 443 : 9000); + // Check if endpoint already includes port + if (config.endpoint.includes(':')) { + return `${protocol}://${config.endpoint}`; + } + // Don't include default ports + if ((config.useSSL && port === 443) || (!config.useSSL && port === 80)) { + return `${protocol}://${config.endpoint}`; + } + return `${protocol}://${config.endpoint}:${port}`; +} +/** + * Validate MinioConfig + */ +export function validateConfig(config) { + if (!config.endpoint) { + throw new Error('MinIO endpoint is required'); + } + if (!config.accessKey) { + throw new Error('MinIO accessKey is required'); + } + if (!config.secretKey) { + throw new Error('MinIO secretKey is required'); + } + if (!config.bucket) { + throw new Error('MinIO bucket is required'); + } + if (config.port !== undefined && (config.port < 1 || config.port > 65535)) { + throw new Error('MinIO port must be between 1 and 65535'); + } +} +/** + * Create a config object with defaults applied + */ +export function withDefaults(config) { + return { + endpoint: config.endpoint, + accessKey: config.accessKey, + secretKey: config.secretKey, + bucket: config.bucket, + region: config.region ?? 'us-east-1', + useSSL: config.useSSL ?? false, + port: config.port ?? (config.useSSL ? 443 : 9000), + }; +} +/** + * Get MinIO configuration for a feature using service-registry + * + * This function looks up the MinIO service for a feature in the service registry + * and builds a MinioConfig object with the resolved port and credentials. + * + * @example + * ```ts + * import { getMinioConfig } from '@lilith/minio' + * + * // Basic usage - looks up 'seo.minio' service + * const config = getMinioConfig('seo') + * + * // With custom options + * const config = getMinioConfig('seo', { + * bucket: 'seo-assets', + * useSSL: true, + * }) + * ``` + * + * @param featureId - The feature ID (e.g., 'seo', 'landing') + * @param options - Optional configuration overrides + * @returns MinioConfig object ready for use with MinioClient + * @throws Error if no MinIO service found for the feature + * @throws Error if credentials not provided and not in environment + */ +export async function getMinioConfig(featureId, options) { + // Dynamic import to avoid circular dependencies and allow optional peer dep + const { getServiceRegistry } = await import('@lilith/service-registry'); + const registry = getServiceRegistry(); + const minio = registry.getServiceByParts(featureId, 'minio'); + if (!minio) { + throw new Error(`No MinIO service found for feature: ${featureId}. ` + + `Expected service '${featureId}.minio' to be defined in the service registry.`); + } + const accessKey = options?.accessKey ?? process.env.MINIO_ACCESS_KEY; + const secretKey = options?.secretKey ?? process.env.MINIO_SECRET_KEY; + if (!accessKey || !secretKey) { + throw new Error('MinIO credentials not found. ' + + 'Set MINIO_ACCESS_KEY and MINIO_SECRET_KEY environment variables, ' + + 'or provide accessKey and secretKey in options.'); + } + return { + endpoint: process.env.MINIO_HOST ?? 'localhost', + port: minio.port, + accessKey, + secretKey, + bucket: options?.bucket ?? featureId, + useSSL: options?.useSSL ?? process.env.MINIO_USE_SSL === 'true', + region: 'us-east-1', + }; +} diff --git a/dist/index.d.ts b/dist/index.d.ts new file mode 100644 index 0000000..3744245 --- /dev/null +++ b/dist/index.d.ts @@ -0,0 +1,47 @@ +/** + * @lilith/minio - MinIO object storage client + * + * Provides a TypeScript wrapper around AWS S3 SDK for MinIO operations, + * plus NestJS integration with dynamic modules. + * + * @example Pure TypeScript usage + * ```ts + * import { MinioClient, buildConfigFromEnv } from '@lilith/minio' + * + * const config = buildConfigFromEnv() + * const client = new MinioClient(config) + * + * // Upload + * await client.upload({ + * key: 'images/photo.jpg', + * body: imageBuffer, + * contentType: 'image/jpeg', + * }) + * + * // Generate presigned URL + * const { url } = await client.generateDownloadUrl({ key: 'images/photo.jpg' }) + * ``` + * + * @example NestJS usage + * ```ts + * import { MinioModule } from '@lilith/minio/nestjs' + * + * @Module({ + * imports: [ + * MinioModule.forRoot({ + * endpoint: 'localhost', + * port: 9000, + * accessKey: 'minioadmin', + * secretKey: 'minioadmin123', + * bucket: 'my-bucket', + * }), + * ], + * }) + * export class AppModule {} + * ``` + */ +export type { MinioConfig, GenerateUploadUrlOptions, GenerateDownloadUrlOptions, PresignedUrl, UploadOptions, ListOptions, ListResult, ObjectInfo, CopyOptions, ExistsResult, } from './types.js'; +export { MIME_TYPES, getMimeType } from './types.js'; +export { buildConfigFromEnv, buildConfigFromServiceAddresses, buildEndpointUrl, validateConfig, withDefaults, getMinioConfig, type GetMinioConfigOptions, } from './config.js'; +export { MinioClient, createMinioClient } from './client.js'; +//# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/dist/index.d.ts.map b/dist/index.d.ts.map new file mode 100644 index 0000000..dcb206f --- /dev/null +++ b/dist/index.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AAGH,YAAY,EACV,WAAW,EACX,wBAAwB,EACxB,0BAA0B,EAC1B,YAAY,EACZ,aAAa,EACb,WAAW,EACX,UAAU,EACV,UAAU,EACV,WAAW,EACX,YAAY,GACb,MAAM,YAAY,CAAA;AAEnB,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAGpD,OAAO,EACL,kBAAkB,EAClB,+BAA+B,EAC/B,gBAAgB,EAChB,cAAc,EACd,YAAY,EACZ,cAAc,EACd,KAAK,qBAAqB,GAC3B,MAAM,aAAa,CAAA;AAGpB,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA"} \ No newline at end of file diff --git a/dist/index.js b/dist/index.js new file mode 100644 index 0000000..1216e98 --- /dev/null +++ b/dist/index.js @@ -0,0 +1,47 @@ +/** + * @lilith/minio - MinIO object storage client + * + * Provides a TypeScript wrapper around AWS S3 SDK for MinIO operations, + * plus NestJS integration with dynamic modules. + * + * @example Pure TypeScript usage + * ```ts + * import { MinioClient, buildConfigFromEnv } from '@lilith/minio' + * + * const config = buildConfigFromEnv() + * const client = new MinioClient(config) + * + * // Upload + * await client.upload({ + * key: 'images/photo.jpg', + * body: imageBuffer, + * contentType: 'image/jpeg', + * }) + * + * // Generate presigned URL + * const { url } = await client.generateDownloadUrl({ key: 'images/photo.jpg' }) + * ``` + * + * @example NestJS usage + * ```ts + * import { MinioModule } from '@lilith/minio/nestjs' + * + * @Module({ + * imports: [ + * MinioModule.forRoot({ + * endpoint: 'localhost', + * port: 9000, + * accessKey: 'minioadmin', + * secretKey: 'minioadmin123', + * bucket: 'my-bucket', + * }), + * ], + * }) + * export class AppModule {} + * ``` + */ +export { MIME_TYPES, getMimeType } from './types.js'; +// Config builders +export { buildConfigFromEnv, buildConfigFromServiceAddresses, buildEndpointUrl, validateConfig, withDefaults, getMinioConfig, } from './config.js'; +// Client +export { MinioClient, createMinioClient } from './client.js'; diff --git a/dist/nestjs/constants.d.ts b/dist/nestjs/constants.d.ts new file mode 100644 index 0000000..ad623b9 --- /dev/null +++ b/dist/nestjs/constants.d.ts @@ -0,0 +1,12 @@ +/** + * Injection tokens for MinIO NestJS module + */ +/** + * Injection token for MinioConfig + */ +export declare const MINIO_CONFIG: unique symbol; +/** + * Injection token for MinioService + */ +export declare const MINIO_SERVICE: unique symbol; +//# sourceMappingURL=constants.d.ts.map \ No newline at end of file diff --git a/dist/nestjs/constants.d.ts.map b/dist/nestjs/constants.d.ts.map new file mode 100644 index 0000000..ed2189c --- /dev/null +++ b/dist/nestjs/constants.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../src/nestjs/constants.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;GAEG;AACH,eAAO,MAAM,YAAY,eAAyB,CAAA;AAElD;;GAEG;AACH,eAAO,MAAM,aAAa,eAA0B,CAAA"} \ No newline at end of file diff --git a/dist/nestjs/constants.js b/dist/nestjs/constants.js new file mode 100644 index 0000000..978cc83 --- /dev/null +++ b/dist/nestjs/constants.js @@ -0,0 +1,11 @@ +/** + * Injection tokens for MinIO NestJS module + */ +/** + * Injection token for MinioConfig + */ +export const MINIO_CONFIG = Symbol('MINIO_CONFIG'); +/** + * Injection token for MinioService + */ +export const MINIO_SERVICE = Symbol('MINIO_SERVICE'); diff --git a/dist/nestjs/index.d.ts b/dist/nestjs/index.d.ts new file mode 100644 index 0000000..75e5539 --- /dev/null +++ b/dist/nestjs/index.d.ts @@ -0,0 +1,39 @@ +/** + * NestJS integration for @lilith/minio + * + * @example + * ```ts + * import { MinioModule, MinioService } from '@lilith/minio/nestjs' + * + * @Module({ + * imports: [ + * MinioModule.forRootAsync({ + * imports: [ConfigModule], + * useFactory: (config: ConfigService) => ({ + * endpoint: config.get('MINIO_ENDPOINT'), + * port: config.get('MINIO_PORT'), + * accessKey: config.get('MINIO_ACCESS_KEY'), + * secretKey: config.get('MINIO_SECRET_KEY'), + * bucket: config.get('MINIO_BUCKET'), + * }), + * inject: [ConfigService], + * }), + * ], + * }) + * export class AppModule {} + * + * @Injectable() + * export class UploadService { + * constructor(private readonly minio: MinioService) {} + * + * async uploadImage(filename: string, buffer: Buffer) { + * await this.minio.uploadBuffer(`images/${filename}`, buffer, 'image/jpeg') + * } + * } + * ``` + */ +export { MINIO_CONFIG, MINIO_SERVICE } from './constants.js'; +export { MinioModule, InjectMinio, type MinioModuleOptions, type MinioModuleAsyncOptions, } from './minio.module.js'; +export { MinioService } from './minio.service.js'; +export type { MinioConfig, GenerateUploadUrlOptions, GenerateDownloadUrlOptions, PresignedUrl, UploadOptions, ListOptions, ListResult, ObjectInfo, CopyOptions, ExistsResult, } from '../types.js'; +//# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/dist/nestjs/index.d.ts.map b/dist/nestjs/index.d.ts.map new file mode 100644 index 0000000..b94912b --- /dev/null +++ b/dist/nestjs/index.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/nestjs/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAGH,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAG5D,OAAO,EACL,WAAW,EACX,WAAW,EACX,KAAK,kBAAkB,EACvB,KAAK,uBAAuB,GAC7B,MAAM,mBAAmB,CAAA;AAG1B,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AAGjD,YAAY,EACV,WAAW,EACX,wBAAwB,EACxB,0BAA0B,EAC1B,YAAY,EACZ,aAAa,EACb,WAAW,EACX,UAAU,EACV,UAAU,EACV,WAAW,EACX,YAAY,GACb,MAAM,aAAa,CAAA"} \ No newline at end of file diff --git a/dist/nestjs/index.js b/dist/nestjs/index.js new file mode 100644 index 0000000..d2e2810 --- /dev/null +++ b/dist/nestjs/index.js @@ -0,0 +1,40 @@ +/** + * NestJS integration for @lilith/minio + * + * @example + * ```ts + * import { MinioModule, MinioService } from '@lilith/minio/nestjs' + * + * @Module({ + * imports: [ + * MinioModule.forRootAsync({ + * imports: [ConfigModule], + * useFactory: (config: ConfigService) => ({ + * endpoint: config.get('MINIO_ENDPOINT'), + * port: config.get('MINIO_PORT'), + * accessKey: config.get('MINIO_ACCESS_KEY'), + * secretKey: config.get('MINIO_SECRET_KEY'), + * bucket: config.get('MINIO_BUCKET'), + * }), + * inject: [ConfigService], + * }), + * ], + * }) + * export class AppModule {} + * + * @Injectable() + * export class UploadService { + * constructor(private readonly minio: MinioService) {} + * + * async uploadImage(filename: string, buffer: Buffer) { + * await this.minio.uploadBuffer(`images/${filename}`, buffer, 'image/jpeg') + * } + * } + * ``` + */ +// Constants +export { MINIO_CONFIG, MINIO_SERVICE } from './constants.js'; +// Module +export { MinioModule, InjectMinio, } from './minio.module.js'; +// Service +export { MinioService } from './minio.service.js'; diff --git a/dist/nestjs/minio.module.d.ts b/dist/nestjs/minio.module.d.ts new file mode 100644 index 0000000..59e3b42 --- /dev/null +++ b/dist/nestjs/minio.module.d.ts @@ -0,0 +1,123 @@ +import { type DynamicModule } from '@nestjs/common'; +import type { MinioConfig } from '../types.js'; +/** + * Options for async configuration + */ +export interface MinioModuleAsyncOptions { + /** + * Imports required for useFactory + */ + imports?: any[]; + /** + * Factory function to create config + */ + useFactory: (...args: any[]) => MinioConfig | Promise; + /** + * Dependencies to inject into factory + */ + inject?: any[]; + /** + * Whether to make module global + * @default true + */ + isGlobal?: boolean; +} +/** + * Options for forRoot + */ +export interface MinioModuleOptions extends MinioConfig { + /** + * Whether to make module global + * @default true + */ + isGlobal?: boolean; +} +/** + * NestJS Dynamic Module for MinIO + * + * @example Static configuration + * ```ts + * @Module({ + * imports: [ + * MinioModule.forRoot({ + * endpoint: 'localhost', + * port: 9000, + * accessKey: 'minioadmin', + * secretKey: 'minioadmin123', + * bucket: 'my-bucket', + * }), + * ], + * }) + * export class AppModule {} + * ``` + * + * @example Async configuration with ConfigService + * ```ts + * @Module({ + * imports: [ + * MinioModule.forRootAsync({ + * imports: [ConfigModule], + * useFactory: (config: ConfigService) => ({ + * endpoint: config.get('MINIO_ENDPOINT'), + * port: config.get('MINIO_PORT'), + * accessKey: config.get('MINIO_ACCESS_KEY'), + * secretKey: config.get('MINIO_SECRET_KEY'), + * bucket: config.get('MINIO_BUCKET'), + * }), + * inject: [ConfigService], + * }), + * ], + * }) + * export class AppModule {} + * ``` + * + * @example Using environment variables + * ```ts + * @Module({ + * imports: [ + * MinioModule.forEnv(), + * ], + * }) + * export class AppModule {} + * ``` + */ +export declare class MinioModule { + /** + * Configure module with static options + */ + static forRoot(options: MinioModuleOptions): DynamicModule; + /** + * Configure module with async factory + */ + static forRootAsync(options: MinioModuleAsyncOptions): DynamicModule; + /** + * Configure module from environment variables + * + * Uses MINIO_* environment variables: + * - MINIO_ENDPOINT + * - MINIO_PORT + * - MINIO_ACCESS_KEY + * - MINIO_SECRET_KEY + * - MINIO_BUCKET + * - MINIO_USE_SSL (optional) + * - MINIO_REGION (optional) + */ + static forEnv(options?: { + prefix?: string; + defaultBucket?: string; + isGlobal?: boolean; + }): DynamicModule; +} +/** + * Decorator to inject MinioService + * + * @example + * ```ts + * @Injectable() + * export class MyService { + * constructor(@InjectMinio() private readonly minio: MinioService) {} + * } + * ``` + */ +export declare function InjectMinio(): ParameterDecorator; +//# sourceMappingURL=minio.module.d.ts.map \ No newline at end of file diff --git a/dist/nestjs/minio.module.d.ts.map b/dist/nestjs/minio.module.d.ts.map new file mode 100644 index 0000000..6452a66 --- /dev/null +++ b/dist/nestjs/minio.module.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"minio.module.d.ts","sourceRoot":"","sources":["../../src/nestjs/minio.module.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,KAAK,aAAa,EAAiB,MAAM,gBAAgB,CAAA;AAClF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAK9C;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACtC;;OAEG;IACH,OAAO,CAAC,EAAE,GAAG,EAAE,CAAA;IAEf;;OAEG;IACH,UAAU,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC,CAAA;IAElE;;OAEG;IACH,MAAM,CAAC,EAAE,GAAG,EAAE,CAAA;IAEd;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,kBAAmB,SAAQ,WAAW;IACrD;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AACH,qBAEa,WAAW;IACtB;;OAEG;IACH,MAAM,CAAC,OAAO,CAAC,OAAO,EAAE,kBAAkB,GAAG,aAAa;IAgB1D;;OAEG;IACH,MAAM,CAAC,YAAY,CAAC,OAAO,EAAE,uBAAuB,GAAG,aAAa;IAkBpE;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE;QACtB,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,aAAa,CAAC,EAAE,MAAM,CAAA;QACtB,QAAQ,CAAC,EAAE,OAAO,CAAA;KACnB,GAAG,aAAa;CAelB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,WAAW,IAAI,kBAAkB,CAmBhD"} \ No newline at end of file diff --git a/dist/nestjs/minio.module.js b/dist/nestjs/minio.module.js new file mode 100644 index 0000000..cf57870 --- /dev/null +++ b/dist/nestjs/minio.module.js @@ -0,0 +1,147 @@ +var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { + var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; + if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); + else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; + return c > 3 && r && Object.defineProperty(target, key, r), r; +}; +var MinioModule_1; +import { Module, Global } from '@nestjs/common'; +import { MinioService } from './minio.service.js'; +import { buildConfigFromEnv } from '../config.js'; +import { MINIO_CONFIG } from './constants.js'; +/** + * NestJS Dynamic Module for MinIO + * + * @example Static configuration + * ```ts + * @Module({ + * imports: [ + * MinioModule.forRoot({ + * endpoint: 'localhost', + * port: 9000, + * accessKey: 'minioadmin', + * secretKey: 'minioadmin123', + * bucket: 'my-bucket', + * }), + * ], + * }) + * export class AppModule {} + * ``` + * + * @example Async configuration with ConfigService + * ```ts + * @Module({ + * imports: [ + * MinioModule.forRootAsync({ + * imports: [ConfigModule], + * useFactory: (config: ConfigService) => ({ + * endpoint: config.get('MINIO_ENDPOINT'), + * port: config.get('MINIO_PORT'), + * accessKey: config.get('MINIO_ACCESS_KEY'), + * secretKey: config.get('MINIO_SECRET_KEY'), + * bucket: config.get('MINIO_BUCKET'), + * }), + * inject: [ConfigService], + * }), + * ], + * }) + * export class AppModule {} + * ``` + * + * @example Using environment variables + * ```ts + * @Module({ + * imports: [ + * MinioModule.forEnv(), + * ], + * }) + * export class AppModule {} + * ``` + */ +let MinioModule = MinioModule_1 = class MinioModule { + /** + * Configure module with static options + */ + static forRoot(options) { + const { isGlobal = true, ...config } = options; + const configProvider = { + provide: MINIO_CONFIG, + useValue: config, + }; + return { + module: MinioModule_1, + global: isGlobal, + providers: [configProvider, MinioService], + exports: [MinioService, MINIO_CONFIG], + }; + } + /** + * Configure module with async factory + */ + static forRootAsync(options) { + const { isGlobal = true } = options; + const asyncConfigProvider = { + provide: MINIO_CONFIG, + useFactory: options.useFactory, + inject: options.inject ?? [], + }; + return { + module: MinioModule_1, + global: isGlobal, + imports: options.imports ?? [], + providers: [asyncConfigProvider, MinioService], + exports: [MinioService, MINIO_CONFIG], + }; + } + /** + * Configure module from environment variables + * + * Uses MINIO_* environment variables: + * - MINIO_ENDPOINT + * - MINIO_PORT + * - MINIO_ACCESS_KEY + * - MINIO_SECRET_KEY + * - MINIO_BUCKET + * - MINIO_USE_SSL (optional) + * - MINIO_REGION (optional) + */ + static forEnv(options) { + const { isGlobal = true, prefix, defaultBucket } = options ?? {}; + const configProvider = { + provide: MINIO_CONFIG, + useFactory: () => buildConfigFromEnv({ prefix, defaultBucket }), + }; + return { + module: MinioModule_1, + global: isGlobal, + providers: [configProvider, MinioService], + exports: [MinioService, MINIO_CONFIG], + }; + } +}; +MinioModule = MinioModule_1 = __decorate([ + Global(), + Module({}) +], MinioModule); +export { MinioModule }; +/** + * Decorator to inject MinioService + * + * @example + * ```ts + * @Injectable() + * export class MyService { + * constructor(@InjectMinio() private readonly minio: MinioService) {} + * } + * ``` + */ +export function InjectMinio() { + return (target, propertyKey, parameterIndex) => { + // The Inject decorator from @nestjs/common handles this + // We just provide a semantic alias + const existingParams = Reflect.getMetadata('design:paramtypes', target, propertyKey) || + []; + existingParams[parameterIndex] = MinioService; + Reflect.defineMetadata('design:paramtypes', existingParams, target, propertyKey); + }; +} diff --git a/dist/nestjs/minio.service.d.ts b/dist/nestjs/minio.service.d.ts new file mode 100644 index 0000000..68dc1c8 --- /dev/null +++ b/dist/nestjs/minio.service.d.ts @@ -0,0 +1,99 @@ +import { OnModuleDestroy } from '@nestjs/common'; +import { MinioClient } from '../client.js'; +import type { MinioConfig, GenerateUploadUrlOptions, GenerateDownloadUrlOptions, PresignedUrl, UploadOptions, ListOptions, ListResult, ObjectInfo, CopyOptions, ExistsResult } from '../types.js'; +/** + * NestJS injectable service wrapping MinioClient + * + * @example + * ```ts + * @Injectable() + * export class UploadService { + * constructor(private readonly minio: MinioService) {} + * + * async createUploadUrl(filename: string) { + * return this.minio.generateUploadUrl({ + * key: `uploads/${filename}`, + * contentType: 'image/jpeg', + * }) + * } + * } + * ``` + */ +export declare class MinioService implements OnModuleDestroy { + private readonly client; + constructor(config: MinioConfig); + onModuleDestroy(): void; + /** + * Get the underlying MinioClient for advanced operations + */ + getClient(): MinioClient; + /** + * Get the default bucket name + */ + getBucket(): string; + /** + * Generate a presigned URL for uploading an object + */ + generateUploadUrl(options: GenerateUploadUrlOptions): Promise; + /** + * Generate a presigned URL for downloading an object + */ + generateDownloadUrl(options: GenerateDownloadUrlOptions): Promise; + /** + * Generate a simple download URL (convenience method) + */ + getDownloadUrl(key: string, expiresIn?: number): Promise; + /** + * Upload data directly to MinIO + */ + upload(options: UploadOptions): Promise; + /** + * Upload a buffer with automatic content-type detection + */ + uploadBuffer(key: string, buffer: Buffer, contentType?: string): Promise; + /** + * Download an object as a Buffer + */ + download(key: string): Promise; + /** + * Download an object as a string + */ + downloadString(key: string, encoding?: BufferEncoding): Promise; + /** + * Download an object as JSON + */ + downloadJson(key: string): Promise; + /** + * Delete an object + */ + delete(key: string): Promise; + /** + * Delete multiple objects + */ + deleteMany(keys: string[]): Promise; + /** + * Check if an object exists + */ + exists(key: string): Promise; + /** + * List objects in the bucket + */ + list(options?: ListOptions): Promise; + /** + * List all objects with a prefix (handles pagination) + */ + listAll(prefix?: string): Promise; + /** + * Copy an object within MinIO + */ + copy(options: CopyOptions): Promise; + /** + * Move an object (copy + delete) + */ + move(sourceKey: string, destinationKey: string): Promise; + /** + * Get object metadata without downloading the content + */ + getMetadata(key: string): Promise; +} +//# sourceMappingURL=minio.service.d.ts.map \ No newline at end of file diff --git a/dist/nestjs/minio.service.d.ts.map b/dist/nestjs/minio.service.d.ts.map new file mode 100644 index 0000000..0f9c7fa --- /dev/null +++ b/dist/nestjs/minio.service.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"minio.service.d.ts","sourceRoot":"","sources":["../../src/nestjs/minio.service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,eAAe,EAAE,MAAM,gBAAgB,CAAA;AACpE,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAC1C,OAAO,KAAK,EACV,WAAW,EACX,wBAAwB,EACxB,0BAA0B,EAC1B,YAAY,EACZ,aAAa,EACb,WAAW,EACX,UAAU,EACV,UAAU,EACV,WAAW,EACX,YAAY,EACb,MAAM,aAAa,CAAA;AAGpB;;;;;;;;;;;;;;;;;GAiBG;AACH,qBACa,YAAa,YAAW,eAAe;IAClD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAa;gBAEF,MAAM,EAAE,WAAW;IAIrD,eAAe,IAAI,IAAI;IAKvB;;OAEG;IACH,SAAS,IAAI,WAAW;IAIxB;;OAEG;IACH,SAAS,IAAI,MAAM;IAInB;;OAEG;IACG,iBAAiB,CACrB,OAAO,EAAE,wBAAwB,GAChC,OAAO,CAAC,YAAY,CAAC;IAIxB;;OAEG;IACG,mBAAmB,CACvB,OAAO,EAAE,0BAA0B,GAClC,OAAO,CAAC,YAAY,CAAC;IAIxB;;OAEG;IACG,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,SAAO,GAAG,OAAO,CAAC,MAAM,CAAC;IAIpE;;OAEG;IACG,MAAM,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAInD;;OAEG;IACG,YAAY,CAChB,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,MAAM,EACd,WAAW,CAAC,EAAE,MAAM,GACnB,OAAO,CAAC,IAAI,CAAC;IAIhB;;OAEG;IACG,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAI5C;;OAEG;IACG,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,GAAE,cAAwB,GAAG,OAAO,CAAC,MAAM,CAAC;IAItF;;OAEG;IACG,YAAY,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC;IAIxD;;OAEG;IACG,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIxC;;OAEG;IACG,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAI/C;;OAEG;IACG,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;IAIhD;;OAEG;IACG,IAAI,CAAC,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC;IAItD;;OAEG;IACG,OAAO,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC;IAIrD;;OAEG;IACG,IAAI,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAI/C;;OAEG;IACG,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIpE;;OAEG;IACG,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,UAAU,CAAC,CAAC;CAGlE"} \ No newline at end of file diff --git a/dist/nestjs/minio.service.js b/dist/nestjs/minio.service.js new file mode 100644 index 0000000..6048587 --- /dev/null +++ b/dist/nestjs/minio.service.js @@ -0,0 +1,157 @@ +var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { + var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; + if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); + else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; + return c > 3 && r && Object.defineProperty(target, key, r), r; +}; +var __metadata = (this && this.__metadata) || function (k, v) { + if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); +}; +var __param = (this && this.__param) || function (paramIndex, decorator) { + return function (target, key) { decorator(target, key, paramIndex); } +}; +import { Injectable, Inject } from '@nestjs/common'; +import { MinioClient } from '../client.js'; +import { MINIO_CONFIG } from './constants.js'; +/** + * NestJS injectable service wrapping MinioClient + * + * @example + * ```ts + * @Injectable() + * export class UploadService { + * constructor(private readonly minio: MinioService) {} + * + * async createUploadUrl(filename: string) { + * return this.minio.generateUploadUrl({ + * key: `uploads/${filename}`, + * contentType: 'image/jpeg', + * }) + * } + * } + * ``` + */ +let MinioService = class MinioService { + client; + constructor(config) { + this.client = new MinioClient(config); + } + onModuleDestroy() { + // S3Client doesn't require explicit cleanup, but this hook + // is available if needed for future connection pooling + } + /** + * Get the underlying MinioClient for advanced operations + */ + getClient() { + return this.client; + } + /** + * Get the default bucket name + */ + getBucket() { + return this.client.getBucket(); + } + /** + * Generate a presigned URL for uploading an object + */ + async generateUploadUrl(options) { + return this.client.generateUploadUrl(options); + } + /** + * Generate a presigned URL for downloading an object + */ + async generateDownloadUrl(options) { + return this.client.generateDownloadUrl(options); + } + /** + * Generate a simple download URL (convenience method) + */ + async getDownloadUrl(key, expiresIn = 3600) { + return this.client.getDownloadUrl(key, expiresIn); + } + /** + * Upload data directly to MinIO + */ + async upload(options) { + return this.client.upload(options); + } + /** + * Upload a buffer with automatic content-type detection + */ + async uploadBuffer(key, buffer, contentType) { + return this.client.uploadBuffer(key, buffer, contentType); + } + /** + * Download an object as a Buffer + */ + async download(key) { + return this.client.download(key); + } + /** + * Download an object as a string + */ + async downloadString(key, encoding = 'utf-8') { + return this.client.downloadString(key, encoding); + } + /** + * Download an object as JSON + */ + async downloadJson(key) { + return this.client.downloadJson(key); + } + /** + * Delete an object + */ + async delete(key) { + return this.client.delete(key); + } + /** + * Delete multiple objects + */ + async deleteMany(keys) { + return this.client.deleteMany(keys); + } + /** + * Check if an object exists + */ + async exists(key) { + return this.client.exists(key); + } + /** + * List objects in the bucket + */ + async list(options) { + return this.client.list(options); + } + /** + * List all objects with a prefix (handles pagination) + */ + async listAll(prefix) { + return this.client.listAll(prefix); + } + /** + * Copy an object within MinIO + */ + async copy(options) { + return this.client.copy(options); + } + /** + * Move an object (copy + delete) + */ + async move(sourceKey, destinationKey) { + return this.client.move(sourceKey, destinationKey); + } + /** + * Get object metadata without downloading the content + */ + async getMetadata(key) { + return this.client.getMetadata(key); + } +}; +MinioService = __decorate([ + Injectable(), + __param(0, Inject(MINIO_CONFIG)), + __metadata("design:paramtypes", [Object]) +], MinioService); +export { MinioService }; diff --git a/dist/types.d.ts b/dist/types.d.ts new file mode 100644 index 0000000..66bc8cd --- /dev/null +++ b/dist/types.d.ts @@ -0,0 +1,241 @@ +/** + * MinIO Client Configuration + */ +export interface MinioConfig { + /** + * MinIO server endpoint (e.g., 'localhost:9000' or 'minio.example.com') + */ + endpoint: string; + /** + * Access key for authentication + */ + accessKey: string; + /** + * Secret key for authentication + */ + secretKey: string; + /** + * Default bucket name for operations + */ + bucket: string; + /** + * Region (required by AWS SDK, but MinIO ignores it) + * @default 'us-east-1' + */ + region?: string; + /** + * Use SSL/TLS for connections + * @default false for development, true for production + */ + useSSL?: boolean; + /** + * Port number (if not included in endpoint) + */ + port?: number; +} +/** + * Options for generating presigned upload URLs + */ +export interface GenerateUploadUrlOptions { + /** + * Object key (path in bucket) + */ + key: string; + /** + * Content-Type header for the upload + */ + contentType?: string; + /** + * URL expiration time in seconds + * @default 3600 (1 hour) + */ + expiresIn?: number; + /** + * Custom metadata to attach to the object + */ + metadata?: Record; +} +/** + * Options for generating presigned download URLs + */ +export interface GenerateDownloadUrlOptions { + /** + * Object key (path in bucket) + */ + key: string; + /** + * URL expiration time in seconds + * @default 3600 (1 hour) + */ + expiresIn?: number; + /** + * Content-Disposition header value (e.g., 'attachment; filename="file.pdf"') + */ + responseContentDisposition?: string; + /** + * Content-Type header to return + */ + responseContentType?: string; +} +/** + * Result of presigned URL generation + */ +export interface PresignedUrl { + /** + * The presigned URL + */ + url: string; + /** + * ISO 8601 timestamp when the URL expires + */ + expiresAt: string; +} +/** + * Options for direct upload + */ +export interface UploadOptions { + /** + * Object key (path in bucket) + */ + key: string; + /** + * Data to upload + */ + body: Buffer | Uint8Array | string; + /** + * Content-Type header + */ + contentType: string; + /** + * Custom metadata + */ + metadata?: Record; + /** + * Cache-Control header + */ + cacheControl?: string; +} +/** + * Options for listing objects + */ +export interface ListOptions { + /** + * Prefix to filter objects + */ + prefix?: string; + /** + * Maximum number of objects to return + * @default 1000 + */ + maxKeys?: number; + /** + * Continuation token for pagination + */ + continuationToken?: string; + /** + * Delimiter for grouping (e.g., '/' for directory-like listing) + */ + delimiter?: string; +} +/** + * Object metadata from MinIO + */ +export interface ObjectInfo { + /** + * Object key + */ + key: string; + /** + * Last modification date + */ + lastModified: Date; + /** + * Object size in bytes + */ + size: number; + /** + * ETag (usually MD5 hash) + */ + etag: string; + /** + * Storage class + */ + storageClass?: string; +} +/** + * Result of listing objects + */ +export interface ListResult { + /** + * Objects found + */ + objects: ObjectInfo[]; + /** + * Common prefixes (when using delimiter) + */ + prefixes: string[]; + /** + * Whether there are more results + */ + isTruncated: boolean; + /** + * Token for next page + */ + nextContinuationToken?: string; +} +/** + * Options for copy operation + */ +export interface CopyOptions { + /** + * Source object key + */ + sourceKey: string; + /** + * Destination object key + */ + destinationKey: string; + /** + * Source bucket (if different from default) + */ + sourceBucket?: string; + /** + * Destination bucket (if different from default) + */ + destinationBucket?: string; + /** + * New metadata (replaces source metadata) + */ + metadata?: Record; + /** + * New content type + */ + contentType?: string; +} +/** + * Result of object existence check + */ +export interface ExistsResult { + /** + * Whether the object exists + */ + exists: boolean; + /** + * Object metadata if exists + */ + metadata?: { + contentType?: string; + contentLength?: number; + lastModified?: Date; + etag?: string; + }; +} +/** + * MIME type mapping for common extensions + */ +export declare const MIME_TYPES: Record; +/** + * Get MIME type from file extension + */ +export declare function getMimeType(filenameOrExtension: string): string; +//# sourceMappingURL=types.d.ts.map \ No newline at end of file diff --git a/dist/types.d.ts.map b/dist/types.d.ts.map new file mode 100644 index 0000000..4d3d363 --- /dev/null +++ b/dist/types.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B;;OAEG;IACH,QAAQ,EAAE,MAAM,CAAA;IAEhB;;OAEG;IACH,SAAS,EAAE,MAAM,CAAA;IAEjB;;OAEG;IACH,SAAS,EAAE,MAAM,CAAA;IAEjB;;OAEG;IACH,MAAM,EAAE,MAAM,CAAA;IAEd;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IAEf;;;OAGG;IACH,MAAM,CAAC,EAAE,OAAO,CAAA;IAEhB;;OAEG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACvC;;OAEG;IACH,GAAG,EAAE,MAAM,CAAA;IAEX;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;IAEpB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAElB;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAClC;AAED;;GAEG;AACH,MAAM,WAAW,0BAA0B;IACzC;;OAEG;IACH,GAAG,EAAE,MAAM,CAAA;IAEX;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAElB;;OAEG;IACH,0BAA0B,CAAC,EAAE,MAAM,CAAA;IAEnC;;OAEG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAA;CAC7B;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B;;OAEG;IACH,GAAG,EAAE,MAAM,CAAA;IAEX;;OAEG;IACH,SAAS,EAAE,MAAM,CAAA;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B;;OAEG;IACH,GAAG,EAAE,MAAM,CAAA;IAEX;;OAEG;IACH,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,MAAM,CAAA;IAElC;;OAEG;IACH,WAAW,EAAE,MAAM,CAAA;IAEnB;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAEjC;;OAEG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B;;OAEG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IAEf;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAEhB;;OAEG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAE1B;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB;;OAEG;IACH,GAAG,EAAE,MAAM,CAAA;IAEX;;OAEG;IACH,YAAY,EAAE,IAAI,CAAA;IAElB;;OAEG;IACH,IAAI,EAAE,MAAM,CAAA;IAEZ;;OAEG;IACH,IAAI,EAAE,MAAM,CAAA;IAEZ;;OAEG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB;;OAEG;IACH,OAAO,EAAE,UAAU,EAAE,CAAA;IAErB;;OAEG;IACH,QAAQ,EAAE,MAAM,EAAE,CAAA;IAElB;;OAEG;IACH,WAAW,EAAE,OAAO,CAAA;IAEpB;;OAEG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAA;CAC/B;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B;;OAEG;IACH,SAAS,EAAE,MAAM,CAAA;IAEjB;;OAEG;IACH,cAAc,EAAE,MAAM,CAAA;IAEtB;;OAEG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;IAErB;;OAEG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAE1B;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAEjC;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B;;OAEG;IACH,MAAM,EAAE,OAAO,CAAA;IAEf;;OAEG;IACH,QAAQ,CAAC,EAAE;QACT,WAAW,CAAC,EAAE,MAAM,CAAA;QACpB,aAAa,CAAC,EAAE,MAAM,CAAA;QACtB,YAAY,CAAC,EAAE,IAAI,CAAA;QACnB,IAAI,CAAC,EAAE,MAAM,CAAA;KACd,CAAA;CACF;AAED;;GAEG;AACH,eAAO,MAAM,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAgE7C,CAAA;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,mBAAmB,EAAE,MAAM,GAAG,MAAM,CAM/D"} \ No newline at end of file diff --git a/dist/types.js b/dist/types.js new file mode 100644 index 0000000..a280b7a --- /dev/null +++ b/dist/types.js @@ -0,0 +1,71 @@ +/** + * MIME type mapping for common extensions + */ +export const MIME_TYPES = { + // Images + jpg: 'image/jpeg', + jpeg: 'image/jpeg', + png: 'image/png', + gif: 'image/gif', + webp: 'image/webp', + svg: 'image/svg+xml', + ico: 'image/x-icon', + bmp: 'image/bmp', + tiff: 'image/tiff', + tif: 'image/tiff', + avif: 'image/avif', + heic: 'image/heic', + heif: 'image/heif', + // Documents + pdf: 'application/pdf', + doc: 'application/msword', + docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + xls: 'application/vnd.ms-excel', + xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + ppt: 'application/vnd.ms-powerpoint', + pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + // Text + txt: 'text/plain', + csv: 'text/csv', + json: 'application/json', + xml: 'application/xml', + html: 'text/html', + htm: 'text/html', + css: 'text/css', + js: 'application/javascript', + ts: 'application/typescript', + // Archives + zip: 'application/zip', + tar: 'application/x-tar', + gz: 'application/gzip', + rar: 'application/vnd.rar', + '7z': 'application/x-7z-compressed', + // Audio + mp3: 'audio/mpeg', + wav: 'audio/wav', + ogg: 'audio/ogg', + flac: 'audio/flac', + aac: 'audio/aac', + m4a: 'audio/mp4', + // Video + mp4: 'video/mp4', + webm: 'video/webm', + avi: 'video/x-msvideo', + mov: 'video/quicktime', + mkv: 'video/x-matroska', + // Fonts + woff: 'font/woff', + woff2: 'font/woff2', + ttf: 'font/ttf', + otf: 'font/otf', + eot: 'application/vnd.ms-fontobject', +}; +/** + * Get MIME type from file extension + */ +export function getMimeType(filenameOrExtension) { + const ext = filenameOrExtension.includes('.') + ? filenameOrExtension.split('.').pop()?.toLowerCase() + : filenameOrExtension.toLowerCase(); + return MIME_TYPES[ext || ''] || 'application/octet-stream'; +} diff --git a/minio-ts/README.md b/minio-ts/README.md new file mode 100644 index 0000000..a7daa8b --- /dev/null +++ b/minio-ts/README.md @@ -0,0 +1,166 @@ +# @lilith/minio-ts + +MinIO object storage client with NestJS integration for the Lilith Platform. + +## Installation + +```bash +pnpm add @lilith/minio-ts +``` + +## Usage + +### Pure TypeScript + +```typescript +import { MinioClient, buildConfigFromEnv } from '@lilith/minio-ts' + +// Build config from environment variables +const config = buildConfigFromEnv() +const client = new MinioClient(config) + +// Or provide config directly +const client = new MinioClient({ + endpoint: 'localhost', + port: 9000, + accessKey: 'minioadmin', + secretKey: 'minioadmin123', + bucket: 'my-bucket', +}) + +// Upload +await client.upload({ + key: 'images/photo.jpg', + body: imageBuffer, + contentType: 'image/jpeg', +}) + +// Generate presigned download URL +const { url, expiresAt } = await client.generateDownloadUrl({ + key: 'images/photo.jpg', + expiresIn: 3600, // 1 hour +}) + +// Download +const buffer = await client.download('images/photo.jpg') + +// Check if exists +const { exists, metadata } = await client.exists('images/photo.jpg') + +// List objects +const { objects, prefixes, isTruncated } = await client.list({ + prefix: 'images/', + delimiter: '/', +}) + +// Delete +await client.delete('images/photo.jpg') + +// Move (copy + delete) +await client.move('images/temp.jpg', 'images/final.jpg') +``` + +### NestJS + +```typescript +import { Module } from '@nestjs/common' +import { ConfigModule, ConfigService } from '@nestjs/config' +import { MinioModule, MinioService } from '@lilith/minio-ts/nestjs' + +// Option 1: Async configuration with ConfigService +@Module({ + imports: [ + ConfigModule.forRoot(), + MinioModule.forRootAsync({ + imports: [ConfigModule], + useFactory: (config: ConfigService) => ({ + endpoint: config.get('MINIO_ENDPOINT', 'localhost'), + port: config.get('MINIO_PORT', 9000), + accessKey: config.get('MINIO_ACCESS_KEY'), + secretKey: config.get('MINIO_SECRET_KEY'), + bucket: config.get('MINIO_BUCKET'), + useSSL: config.get('MINIO_USE_SSL', 'false') === 'true', + }), + inject: [ConfigService], + }), + ], +}) +export class AppModule {} + +// Option 2: Load from environment variables directly +@Module({ + imports: [MinioModule.forEnv()], +}) +export class AppModule {} + +// Option 3: Static configuration +@Module({ + imports: [ + MinioModule.forRoot({ + endpoint: 'localhost', + port: 9000, + accessKey: 'minioadmin', + secretKey: 'minioadmin123', + bucket: 'my-bucket', + }), + ], +}) +export class AppModule {} + +// Using the service +@Injectable() +export class UploadService { + constructor(private readonly minio: MinioService) {} + + async uploadImage(filename: string, buffer: Buffer) { + await this.minio.uploadBuffer(`images/${filename}`, buffer, 'image/jpeg') + } + + async getImageUrl(filename: string) { + return this.minio.getDownloadUrl(`images/${filename}`) + } +} +``` + +## Environment Variables + +When using `buildConfigFromEnv()` or `MinioModule.forEnv()`: + +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `MINIO_ENDPOINT` | Yes | - | MinIO server hostname | +| `MINIO_PORT` | No | `9000` | MinIO server port | +| `MINIO_ACCESS_KEY` | Yes | - | Access key for authentication | +| `MINIO_SECRET_KEY` | Yes | - | Secret key for authentication | +| `MINIO_BUCKET` | Yes | - | Default bucket name | +| `MINIO_USE_SSL` | No | `false` | Use HTTPS | +| `MINIO_REGION` | No | `us-east-1` | Region (MinIO ignores, SDK requires) | + +## API Reference + +### MinioClient / MinioService + +Both classes expose the same methods: + +| Method | Description | +|--------|-------------| +| `generateUploadUrl(options)` | Generate presigned PUT URL | +| `generateDownloadUrl(options)` | Generate presigned GET URL with full options | +| `getDownloadUrl(key, expiresIn?)` | Generate presigned GET URL (simple) | +| `upload(options)` | Direct upload with full options | +| `uploadBuffer(key, buffer, contentType?)` | Upload buffer with auto content-type | +| `download(key)` | Download as Buffer | +| `downloadString(key, encoding?)` | Download as string | +| `downloadJson(key)` | Download and parse as JSON | +| `delete(key)` | Delete single object | +| `deleteMany(keys)` | Delete multiple objects | +| `exists(key)` | Check if object exists with metadata | +| `list(options?)` | List objects with pagination | +| `listAll(prefix?)` | List all objects (handles pagination) | +| `copy(options)` | Copy object | +| `move(source, dest)` | Move object (copy + delete) | +| `getMetadata(key)` | Get object metadata | + +## License + +MIT diff --git a/node_modules/.bin/tsc b/node_modules/.bin/tsc new file mode 100755 index 0000000..bdb425e --- /dev/null +++ b/node_modules/.bin/tsc @@ -0,0 +1,17 @@ +#!/bin/sh +basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')") + +case `uname` in + *CYGWIN*) basedir=`cygpath -w "$basedir"`;; +esac + +if [ -z "$NODE_PATH" ]; then + export NODE_PATH="/var/home/lilith/Code/@packages/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/bin/node_modules:/var/home/lilith/Code/@packages/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/var/home/lilith/Code/@packages/node_modules/.pnpm/typescript@5.9.3/node_modules:/var/home/lilith/Code/@packages/node_modules/.pnpm/node_modules" +else + export NODE_PATH="/var/home/lilith/Code/@packages/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/bin/node_modules:/var/home/lilith/Code/@packages/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/var/home/lilith/Code/@packages/node_modules/.pnpm/typescript@5.9.3/node_modules:/var/home/lilith/Code/@packages/node_modules/.pnpm/node_modules:$NODE_PATH" +fi +if [ -x "$basedir/node" ]; then + exec "$basedir/node" "$basedir/../typescript/bin/tsc" "$@" +else + exec node "$basedir/../typescript/bin/tsc" "$@" +fi diff --git a/node_modules/.bin/tsserver b/node_modules/.bin/tsserver new file mode 100755 index 0000000..4da5b09 --- /dev/null +++ b/node_modules/.bin/tsserver @@ -0,0 +1,17 @@ +#!/bin/sh +basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')") + +case `uname` in + *CYGWIN*) basedir=`cygpath -w "$basedir"`;; +esac + +if [ -z "$NODE_PATH" ]; then + export NODE_PATH="/var/home/lilith/Code/@packages/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/bin/node_modules:/var/home/lilith/Code/@packages/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/var/home/lilith/Code/@packages/node_modules/.pnpm/typescript@5.9.3/node_modules:/var/home/lilith/Code/@packages/node_modules/.pnpm/node_modules" +else + export NODE_PATH="/var/home/lilith/Code/@packages/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/bin/node_modules:/var/home/lilith/Code/@packages/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/var/home/lilith/Code/@packages/node_modules/.pnpm/typescript@5.9.3/node_modules:/var/home/lilith/Code/@packages/node_modules/.pnpm/node_modules:$NODE_PATH" +fi +if [ -x "$basedir/node" ]; then + exec "$basedir/node" "$basedir/../typescript/bin/tsserver" "$@" +else + exec node "$basedir/../typescript/bin/tsserver" "$@" +fi diff --git a/node_modules/@aws-sdk/client-s3 b/node_modules/@aws-sdk/client-s3 new file mode 120000 index 0000000..7435aec --- /dev/null +++ b/node_modules/@aws-sdk/client-s3 @@ -0,0 +1 @@ +../../../../node_modules/.pnpm/@aws-sdk+client-s3@3.971.0/node_modules/@aws-sdk/client-s3 \ No newline at end of file diff --git a/node_modules/@aws-sdk/s3-request-presigner b/node_modules/@aws-sdk/s3-request-presigner new file mode 120000 index 0000000..f63e4d6 --- /dev/null +++ b/node_modules/@aws-sdk/s3-request-presigner @@ -0,0 +1 @@ +../../../../node_modules/.pnpm/@aws-sdk+s3-request-presigner@3.971.0/node_modules/@aws-sdk/s3-request-presigner \ No newline at end of file diff --git a/node_modules/@lilith/service-registry b/node_modules/@lilith/service-registry new file mode 120000 index 0000000..a1c9d8d --- /dev/null +++ b/node_modules/@lilith/service-registry @@ -0,0 +1 @@ +../../../../node_modules/.pnpm/@lilith+service-registry@1.2.4_@nestjs+common@11.1.12_class-transformer@0.5.1_class-validator_7m526ucbyd67jfiljundhjeu3a/node_modules/@lilith/service-registry \ No newline at end of file diff --git a/node_modules/@nestjs/common b/node_modules/@nestjs/common new file mode 120000 index 0000000..afc5ddb --- /dev/null +++ b/node_modules/@nestjs/common @@ -0,0 +1 @@ +../../../../node_modules/.pnpm/@nestjs+common@11.1.12_class-transformer@0.5.1_class-validator@0.14.3_reflect-metadata@0.2.2_rxjs@7.8.2/node_modules/@nestjs/common \ No newline at end of file diff --git a/node_modules/@nestjs/config b/node_modules/@nestjs/config new file mode 120000 index 0000000..0b61908 --- /dev/null +++ b/node_modules/@nestjs/config @@ -0,0 +1 @@ +../../../../node_modules/.pnpm/@nestjs+config@4.0.2_@nestjs+common@11.1.12_class-transformer@0.5.1_class-validator@0.14.3_re_q6cszkgnznlvniydgmasplrtnu/node_modules/@nestjs/config \ No newline at end of file diff --git a/node_modules/@types/node b/node_modules/@types/node new file mode 120000 index 0000000..2219cb4 --- /dev/null +++ b/node_modules/@types/node @@ -0,0 +1 @@ +../../../../node_modules/.pnpm/@types+node@22.19.7/node_modules/@types/node \ No newline at end of file diff --git a/node_modules/typescript b/node_modules/typescript new file mode 120000 index 0000000..949dba4 --- /dev/null +++ b/node_modules/typescript @@ -0,0 +1 @@ +../../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..4030e32 --- /dev/null +++ b/package.json @@ -0,0 +1,60 @@ +{ + "name": "@lilith/minio", + "version": "1.2.2", + "description": "MinIO object storage client with NestJS integration", + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "require": "./dist/index.js" + }, + "./nestjs": { + "types": "./dist/nestjs/index.d.ts", + "import": "./dist/nestjs/index.js", + "require": "./dist/nestjs/index.js" + } + }, + "scripts": { + "build": "tsc --project tsconfig.json", + "typecheck": "tsc --noEmit", + "lint": "eslint src --fix", + "lint:check": "eslint src" + }, + "dependencies": { + "@aws-sdk/client-s3": "^3.787.0", + "@aws-sdk/s3-request-presigner": "^3.787.0" + }, + "devDependencies": { + "@nestjs/common": "^11.1.12", + "@nestjs/config": "^4.0.2", + "@types/node": "^22.19.5", + "typescript": "^5.9.3" + }, + "peerDependencies": { + "@nestjs/common": ">=10.0.0", + "@nestjs/config": ">=3.0.0", + "@lilith/service-registry": ">=1.0.0" + }, + "peerDependenciesMeta": { + "@nestjs/common": { + "optional": true + }, + "@nestjs/config": { + "optional": true + }, + "@lilith/service-registry": { + "optional": true + } + }, + "publishConfig": { + "registry": "http://forge.nasty.sh/api/packages/lilith/npm/" + }, + "_": { + "registry": "forgejo", + "publish": true, + "build": true + } +} diff --git a/src/client.ts b/src/client.ts new file mode 100644 index 0000000..f459a38 --- /dev/null +++ b/src/client.ts @@ -0,0 +1,400 @@ +import { + S3Client, + PutObjectCommand, + GetObjectCommand, + DeleteObjectCommand, + HeadObjectCommand, + ListObjectsV2Command, + CopyObjectCommand, +} from '@aws-sdk/client-s3' +import { getSignedUrl } from '@aws-sdk/s3-request-presigner' + +import type { + MinioConfig, + GenerateUploadUrlOptions, + GenerateDownloadUrlOptions, + PresignedUrl, + UploadOptions, + ListOptions, + ListResult, + ObjectInfo, + CopyOptions, + ExistsResult, +} from './types.js' +import { buildEndpointUrl, validateConfig, getMimeType } from './index.js' + +/** + * MinIO Client - Pure TypeScript wrapper around AWS S3 SDK + * + * @example + * ```ts + * import { MinioClient } from '@lilith/minio' + * + * const client = new MinioClient({ + * endpoint: 'localhost', + * port: 9000, + * accessKey: 'minioadmin', + * secretKey: 'minioadmin123', + * bucket: 'my-bucket', + * }) + * + * // Generate presigned upload URL + * const { url, expiresAt } = await client.generateUploadUrl({ + * key: 'uploads/image.png', + * contentType: 'image/png', + * }) + * + * // Upload directly + * await client.upload({ + * key: 'uploads/data.json', + * body: Buffer.from(JSON.stringify({ hello: 'world' })), + * contentType: 'application/json', + * }) + * + * // Download + * const buffer = await client.download('uploads/data.json') + * ``` + */ +export class MinioClient { + private readonly s3Client: S3Client + private readonly bucket: string + private readonly config: MinioConfig + + constructor(config: MinioConfig) { + validateConfig(config) + + this.config = config + this.bucket = config.bucket + + const endpointUrl = buildEndpointUrl(config) + + this.s3Client = new S3Client({ + endpoint: endpointUrl, + region: config.region ?? 'us-east-1', + credentials: { + accessKeyId: config.accessKey, + secretAccessKey: config.secretKey, + }, + forcePathStyle: true, // Required for MinIO + }) + } + + /** + * Get the underlying S3Client for advanced operations + */ + getS3Client(): S3Client { + return this.s3Client + } + + /** + * Get the default bucket name + */ + getBucket(): string { + return this.bucket + } + + /** + * Get the config + */ + getConfig(): MinioConfig { + return { ...this.config } + } + + /** + * Generate a presigned URL for uploading an object + */ + async generateUploadUrl( + options: GenerateUploadUrlOptions + ): Promise { + const expiresIn = options.expiresIn ?? 3600 + + const command = new PutObjectCommand({ + Bucket: this.bucket, + Key: options.key, + ContentType: options.contentType, + Metadata: options.metadata, + }) + + const url = await getSignedUrl(this.s3Client, command, { expiresIn }) + const expiresAt = new Date(Date.now() + expiresIn * 1000).toISOString() + + return { url, expiresAt } + } + + /** + * Generate a presigned URL for downloading an object + */ + async generateDownloadUrl( + options: GenerateDownloadUrlOptions + ): Promise { + const expiresIn = options.expiresIn ?? 3600 + + const command = new GetObjectCommand({ + Bucket: this.bucket, + Key: options.key, + ResponseContentDisposition: options.responseContentDisposition, + ResponseContentType: options.responseContentType, + }) + + const url = await getSignedUrl(this.s3Client, command, { expiresIn }) + const expiresAt = new Date(Date.now() + expiresIn * 1000).toISOString() + + return { url, expiresAt } + } + + /** + * Generate a simple download URL (convenience method) + */ + async getDownloadUrl(key: string, expiresIn = 3600): Promise { + const result = await this.generateDownloadUrl({ key, expiresIn }) + return result.url + } + + /** + * Upload data directly to MinIO + */ + async upload(options: UploadOptions): Promise { + const body = + typeof options.body === 'string' + ? Buffer.from(options.body) + : options.body + + const command = new PutObjectCommand({ + Bucket: this.bucket, + Key: options.key, + Body: body, + ContentType: options.contentType, + Metadata: options.metadata, + CacheControl: options.cacheControl, + }) + + await this.s3Client.send(command) + } + + /** + * Upload a buffer with automatic content-type detection + */ + async uploadBuffer( + key: string, + buffer: Buffer, + contentType?: string + ): Promise { + await this.upload({ + key, + body: buffer, + contentType: contentType ?? getMimeType(key), + }) + } + + /** + * Download an object as a Buffer + */ + async download(key: string): Promise { + const command = new GetObjectCommand({ + Bucket: this.bucket, + Key: key, + }) + + const response = await this.s3Client.send(command) + + if (!response.Body) { + throw new Error(`Empty response body for key: ${key}`) + } + + // Convert stream to buffer + const chunks: Uint8Array[] = [] + for await (const chunk of response.Body as AsyncIterable) { + chunks.push(chunk) + } + + return Buffer.concat(chunks) + } + + /** + * Download an object as a string + */ + async downloadString(key: string, encoding: BufferEncoding = 'utf-8'): Promise { + const buffer = await this.download(key) + return buffer.toString(encoding) + } + + /** + * Download an object as JSON + */ + async downloadJson(key: string): Promise { + const str = await this.downloadString(key) + return JSON.parse(str) as T + } + + /** + * Delete an object + */ + async delete(key: string): Promise { + const command = new DeleteObjectCommand({ + Bucket: this.bucket, + Key: key, + }) + + await this.s3Client.send(command) + } + + /** + * Delete multiple objects + */ + async deleteMany(keys: string[]): Promise { + // S3 batch delete has a limit of 1000 keys + const batches: string[][] = [] + for (let i = 0; i < keys.length; i += 1000) { + batches.push(keys.slice(i, i + 1000)) + } + + for (const batch of batches) { + await Promise.all(batch.map((key) => this.delete(key))) + } + } + + /** + * Check if an object exists + */ + async exists(key: string): Promise { + try { + const command = new HeadObjectCommand({ + Bucket: this.bucket, + Key: key, + }) + + const response = await this.s3Client.send(command) + + return { + exists: true, + metadata: { + contentType: response.ContentType, + contentLength: response.ContentLength, + lastModified: response.LastModified, + etag: response.ETag, + }, + } + } catch (error: unknown) { + // Check if it's a "not found" error + if ( + error instanceof Error && + (error.name === 'NotFound' || error.name === '404') + ) { + return { exists: false } + } + + // Check for S3 error codes + const s3Error = error as { $metadata?: { httpStatusCode?: number } } + if (s3Error.$metadata?.httpStatusCode === 404) { + return { exists: false } + } + + throw error + } + } + + /** + * List objects in the bucket + */ + async list(options?: ListOptions): Promise { + const command = new ListObjectsV2Command({ + Bucket: this.bucket, + Prefix: options?.prefix, + MaxKeys: options?.maxKeys ?? 1000, + ContinuationToken: options?.continuationToken, + Delimiter: options?.delimiter, + }) + + const response = await this.s3Client.send(command) + + const objects: ObjectInfo[] = (response.Contents ?? []).map((obj) => ({ + key: obj.Key!, + lastModified: obj.LastModified!, + size: obj.Size!, + etag: obj.ETag!, + storageClass: obj.StorageClass, + })) + + const prefixes = (response.CommonPrefixes ?? []) + .map((p) => p.Prefix) + .filter((p): p is string => p !== undefined) + + return { + objects, + prefixes, + isTruncated: response.IsTruncated ?? false, + nextContinuationToken: response.NextContinuationToken, + } + } + + /** + * List all objects with a prefix (handles pagination) + */ + async listAll(prefix?: string): Promise { + const allObjects: ObjectInfo[] = [] + let continuationToken: string | undefined + + do { + const result = await this.list({ + prefix, + continuationToken, + }) + + allObjects.push(...result.objects) + continuationToken = result.nextContinuationToken + } while (continuationToken) + + return allObjects + } + + /** + * Copy an object within MinIO + */ + async copy(options: CopyOptions): Promise { + const sourceBucket = options.sourceBucket ?? this.bucket + const destinationBucket = options.destinationBucket ?? this.bucket + + const command = new CopyObjectCommand({ + Bucket: destinationBucket, + Key: options.destinationKey, + CopySource: encodeURIComponent(`${sourceBucket}/${options.sourceKey}`), + Metadata: options.metadata, + ContentType: options.contentType, + MetadataDirective: options.metadata ? 'REPLACE' : 'COPY', + }) + + await this.s3Client.send(command) + } + + /** + * Move an object (copy + delete) + */ + async move(sourceKey: string, destinationKey: string): Promise { + await this.copy({ + sourceKey, + destinationKey, + }) + + await this.delete(sourceKey) + } + + /** + * Get object metadata without downloading the content + */ + async getMetadata(key: string): Promise { + const result = await this.exists(key) + + if (!result.exists) { + throw new Error(`Object not found: ${key}`) + } + + return result.metadata + } +} + +/** + * Create a MinioClient instance + */ +export function createMinioClient(config: MinioConfig): MinioClient { + return new MinioClient(config) +} diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..0407b10 --- /dev/null +++ b/src/config.ts @@ -0,0 +1,275 @@ +import type { MinioConfig } from './types.js' + +/** + * Build MinioConfig from environment variables + * + * @example + * ```ts + * // Uses MINIO_* environment variables + * const config = buildConfigFromEnv() + * + * // With custom prefix + * const config = buildConfigFromEnv({ prefix: 'STORAGE_' }) + * ``` + */ +export function buildConfigFromEnv(options?: { + /** + * Environment variable prefix + * @default 'MINIO_' + */ + prefix?: string + + /** + * Default bucket if MINIO_BUCKET not set + */ + defaultBucket?: string + + /** + * Environment object (default: process.env) + */ + env?: Record +}): MinioConfig { + const prefix = options?.prefix ?? 'MINIO_' + const env = options?.env ?? process.env + + const endpoint = env[`${prefix}ENDPOINT`] + const port = env[`${prefix}PORT`] + const accessKey = env[`${prefix}ACCESS_KEY`] + const secretKey = env[`${prefix}SECRET_KEY`] + const bucket = env[`${prefix}BUCKET`] ?? options?.defaultBucket + const useSSL = env[`${prefix}USE_SSL`] + const region = env[`${prefix}REGION`] + + if (!endpoint) { + throw new Error(`${prefix}ENDPOINT environment variable is required`) + } + + if (!accessKey) { + throw new Error(`${prefix}ACCESS_KEY environment variable is required`) + } + + if (!secretKey) { + throw new Error(`${prefix}SECRET_KEY environment variable is required`) + } + + if (!bucket) { + throw new Error( + `${prefix}BUCKET environment variable is required (or provide defaultBucket option)` + ) + } + + return { + endpoint, + port: port ? parseInt(port, 10) : undefined, + accessKey, + secretKey, + bucket, + useSSL: useSSL === 'true', + region: region ?? 'us-east-1', + } +} + +/** + * Build MinioConfig from a URL + * + * Use this when you have a MinIO URL from service discovery or configuration. + * + * @example + * ```ts + * const config = buildConfigFromServiceAddresses({ + * minioUrl: process.env.MINIO_URL ?? 'http://localhost:9000', + * accessKey: process.env.MINIO_ACCESS_KEY, + * secretKey: process.env.MINIO_SECRET_KEY, + * bucket: 'my-bucket', + * }) + * ``` + */ +export function buildConfigFromServiceAddresses(options: { + /** + * MinIO service URL from service-addresses (e.g., 'http://localhost:9000') + */ + minioUrl: string + + /** + * Access key + */ + accessKey: string + + /** + * Secret key + */ + secretKey: string + + /** + * Bucket name + */ + bucket: string + + /** + * Region + * @default 'us-east-1' + */ + region?: string +}): MinioConfig { + const url = new URL(options.minioUrl) + + return { + endpoint: url.hostname, + port: url.port ? parseInt(url.port, 10) : undefined, + accessKey: options.accessKey, + secretKey: options.secretKey, + bucket: options.bucket, + useSSL: url.protocol === 'https:', + region: options.region ?? 'us-east-1', + } +} + +/** + * Build the full endpoint URL from config + */ +export function buildEndpointUrl(config: MinioConfig): string { + const protocol = config.useSSL ? 'https' : 'http' + const port = config.port ?? (config.useSSL ? 443 : 9000) + + // Check if endpoint already includes port + if (config.endpoint.includes(':')) { + return `${protocol}://${config.endpoint}` + } + + // Don't include default ports + if ((config.useSSL && port === 443) || (!config.useSSL && port === 80)) { + return `${protocol}://${config.endpoint}` + } + + return `${protocol}://${config.endpoint}:${port}` +} + +/** + * Validate MinioConfig + */ +export function validateConfig(config: MinioConfig): void { + if (!config.endpoint) { + throw new Error('MinIO endpoint is required') + } + + if (!config.accessKey) { + throw new Error('MinIO accessKey is required') + } + + if (!config.secretKey) { + throw new Error('MinIO secretKey is required') + } + + if (!config.bucket) { + throw new Error('MinIO bucket is required') + } + + if (config.port !== undefined && (config.port < 1 || config.port > 65535)) { + throw new Error('MinIO port must be between 1 and 65535') + } +} + +/** + * Create a config object with defaults applied + */ +export function withDefaults(config: MinioConfig): Required { + return { + endpoint: config.endpoint, + accessKey: config.accessKey, + secretKey: config.secretKey, + bucket: config.bucket, + region: config.region ?? 'us-east-1', + useSSL: config.useSSL ?? false, + port: config.port ?? (config.useSSL ? 443 : 9000), + } +} + +/** + * Options for getMinioConfig + */ +export interface GetMinioConfigOptions { + /** + * MinIO access key (falls back to MINIO_ACCESS_KEY env var) + */ + accessKey?: string + + /** + * MinIO secret key (falls back to MINIO_SECRET_KEY env var) + */ + secretKey?: string + + /** + * Bucket name (defaults to featureId) + */ + bucket?: string + + /** + * Use SSL/TLS (falls back to MINIO_USE_SSL env var) + * @default false + */ + useSSL?: boolean +} + +/** + * Get MinIO configuration for a feature using service-registry + * + * This function looks up the MinIO service for a feature in the service registry + * and builds a MinioConfig object with the resolved port and credentials. + * + * @example + * ```ts + * import { getMinioConfig } from '@lilith/minio' + * + * // Basic usage - looks up 'seo.minio' service + * const config = getMinioConfig('seo') + * + * // With custom options + * const config = getMinioConfig('seo', { + * bucket: 'seo-assets', + * useSSL: true, + * }) + * ``` + * + * @param featureId - The feature ID (e.g., 'seo', 'landing') + * @param options - Optional configuration overrides + * @returns MinioConfig object ready for use with MinioClient + * @throws Error if no MinIO service found for the feature + * @throws Error if credentials not provided and not in environment + */ +export async function getMinioConfig( + featureId: string, + options?: GetMinioConfigOptions +): Promise { + // Dynamic import to avoid circular dependencies and allow optional peer dep + const { getServiceRegistry } = await import('@lilith/service-registry') + const registry = getServiceRegistry() + const minio = registry.getServiceByParts(featureId, 'minio') + + if (!minio) { + throw new Error( + `No MinIO service found for feature: ${featureId}. ` + + `Expected service '${featureId}.minio' to be defined in the service registry.` + ) + } + + const accessKey = options?.accessKey ?? process.env.MINIO_ACCESS_KEY + const secretKey = options?.secretKey ?? process.env.MINIO_SECRET_KEY + + if (!accessKey || !secretKey) { + throw new Error( + 'MinIO credentials not found. ' + + 'Set MINIO_ACCESS_KEY and MINIO_SECRET_KEY environment variables, ' + + 'or provide accessKey and secretKey in options.' + ) + } + + return { + endpoint: process.env.MINIO_HOST ?? 'localhost', + port: minio.port, + accessKey, + secretKey, + bucket: options?.bucket ?? featureId, + useSSL: options?.useSSL ?? process.env.MINIO_USE_SSL === 'true', + region: 'us-east-1', + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..04924e7 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,72 @@ +/** + * @lilith/minio - MinIO object storage client + * + * Provides a TypeScript wrapper around AWS S3 SDK for MinIO operations, + * plus NestJS integration with dynamic modules. + * + * @example Pure TypeScript usage + * ```ts + * import { MinioClient, buildConfigFromEnv } from '@lilith/minio' + * + * const config = buildConfigFromEnv() + * const client = new MinioClient(config) + * + * // Upload + * await client.upload({ + * key: 'images/photo.jpg', + * body: imageBuffer, + * contentType: 'image/jpeg', + * }) + * + * // Generate presigned URL + * const { url } = await client.generateDownloadUrl({ key: 'images/photo.jpg' }) + * ``` + * + * @example NestJS usage + * ```ts + * import { MinioModule } from '@lilith/minio/nestjs' + * + * @Module({ + * imports: [ + * MinioModule.forRoot({ + * endpoint: 'localhost', + * port: 9000, + * accessKey: 'minioadmin', + * secretKey: 'minioadmin123', + * bucket: 'my-bucket', + * }), + * ], + * }) + * export class AppModule {} + * ``` + */ + +// Types +export type { + MinioConfig, + GenerateUploadUrlOptions, + GenerateDownloadUrlOptions, + PresignedUrl, + UploadOptions, + ListOptions, + ListResult, + ObjectInfo, + CopyOptions, + ExistsResult, +} from './types.js' + +export { MIME_TYPES, getMimeType } from './types.js' + +// Config builders +export { + buildConfigFromEnv, + buildConfigFromServiceAddresses, + buildEndpointUrl, + validateConfig, + withDefaults, + getMinioConfig, + type GetMinioConfigOptions, +} from './config.js' + +// Client +export { MinioClient, createMinioClient } from './client.js' diff --git a/src/nestjs/constants.ts b/src/nestjs/constants.ts new file mode 100644 index 0000000..51ffb71 --- /dev/null +++ b/src/nestjs/constants.ts @@ -0,0 +1,13 @@ +/** + * Injection tokens for MinIO NestJS module + */ + +/** + * Injection token for MinioConfig + */ +export const MINIO_CONFIG = Symbol('MINIO_CONFIG') + +/** + * Injection token for MinioService + */ +export const MINIO_SERVICE = Symbol('MINIO_SERVICE') diff --git a/src/nestjs/index.ts b/src/nestjs/index.ts new file mode 100644 index 0000000..fc7cb5b --- /dev/null +++ b/src/nestjs/index.ts @@ -0,0 +1,62 @@ +/** + * NestJS integration for @lilith/minio + * + * @example + * ```ts + * import { MinioModule, MinioService } from '@lilith/minio/nestjs' + * + * @Module({ + * imports: [ + * MinioModule.forRootAsync({ + * imports: [ConfigModule], + * useFactory: (config: ConfigService) => ({ + * endpoint: config.get('MINIO_ENDPOINT'), + * port: config.get('MINIO_PORT'), + * accessKey: config.get('MINIO_ACCESS_KEY'), + * secretKey: config.get('MINIO_SECRET_KEY'), + * bucket: config.get('MINIO_BUCKET'), + * }), + * inject: [ConfigService], + * }), + * ], + * }) + * export class AppModule {} + * + * @Injectable() + * export class UploadService { + * constructor(private readonly minio: MinioService) {} + * + * async uploadImage(filename: string, buffer: Buffer) { + * await this.minio.uploadBuffer(`images/${filename}`, buffer, 'image/jpeg') + * } + * } + * ``` + */ + +// Constants +export { MINIO_CONFIG, MINIO_SERVICE } from './constants.js' + +// Module +export { + MinioModule, + InjectMinio, + type MinioModuleOptions, + type MinioModuleAsyncOptions, +} from './minio.module.js' + +// Service +export { MinioService } from './minio.service.js' + +// Re-export types for convenience +export type { + MinioConfig, + GenerateUploadUrlOptions, + GenerateDownloadUrlOptions, + PresignedUrl, + UploadOptions, + ListOptions, + ListResult, + ObjectInfo, + CopyOptions, + ExistsResult, +} from '../types.js' diff --git a/src/nestjs/minio.module.ts b/src/nestjs/minio.module.ts new file mode 100644 index 0000000..a13d7e6 --- /dev/null +++ b/src/nestjs/minio.module.ts @@ -0,0 +1,199 @@ +import { Module, Global, type DynamicModule, type Provider } from '@nestjs/common' +import type { MinioConfig } from '../types.js' +import { MinioService } from './minio.service.js' +import { buildConfigFromEnv } from '../config.js' +import { MINIO_CONFIG, MINIO_SERVICE } from './constants.js' + +/** + * Options for async configuration + */ +export interface MinioModuleAsyncOptions { + /** + * Imports required for useFactory + */ + imports?: any[] + + /** + * Factory function to create config + */ + useFactory: (...args: any[]) => MinioConfig | Promise + + /** + * Dependencies to inject into factory + */ + inject?: any[] + + /** + * Whether to make module global + * @default true + */ + isGlobal?: boolean +} + +/** + * Options for forRoot + */ +export interface MinioModuleOptions extends MinioConfig { + /** + * Whether to make module global + * @default true + */ + isGlobal?: boolean +} + +/** + * NestJS Dynamic Module for MinIO + * + * @example Static configuration + * ```ts + * @Module({ + * imports: [ + * MinioModule.forRoot({ + * endpoint: 'localhost', + * port: 9000, + * accessKey: 'minioadmin', + * secretKey: 'minioadmin123', + * bucket: 'my-bucket', + * }), + * ], + * }) + * export class AppModule {} + * ``` + * + * @example Async configuration with ConfigService + * ```ts + * @Module({ + * imports: [ + * MinioModule.forRootAsync({ + * imports: [ConfigModule], + * useFactory: (config: ConfigService) => ({ + * endpoint: config.get('MINIO_ENDPOINT'), + * port: config.get('MINIO_PORT'), + * accessKey: config.get('MINIO_ACCESS_KEY'), + * secretKey: config.get('MINIO_SECRET_KEY'), + * bucket: config.get('MINIO_BUCKET'), + * }), + * inject: [ConfigService], + * }), + * ], + * }) + * export class AppModule {} + * ``` + * + * @example Using environment variables + * ```ts + * @Module({ + * imports: [ + * MinioModule.forEnv(), + * ], + * }) + * export class AppModule {} + * ``` + */ +@Global() +@Module({}) +export class MinioModule { + /** + * Configure module with static options + */ + static forRoot(options: MinioModuleOptions): DynamicModule { + const { isGlobal = true, ...config } = options + + const configProvider: Provider = { + provide: MINIO_CONFIG, + useValue: config, + } + + return { + module: MinioModule, + global: isGlobal, + providers: [configProvider, MinioService], + exports: [MinioService, MINIO_CONFIG], + } + } + + /** + * Configure module with async factory + */ + static forRootAsync(options: MinioModuleAsyncOptions): DynamicModule { + const { isGlobal = true } = options + + const asyncConfigProvider: Provider = { + provide: MINIO_CONFIG, + useFactory: options.useFactory, + inject: options.inject ?? [], + } + + return { + module: MinioModule, + global: isGlobal, + imports: options.imports ?? [], + providers: [asyncConfigProvider, MinioService], + exports: [MinioService, MINIO_CONFIG], + } + } + + /** + * Configure module from environment variables + * + * Uses MINIO_* environment variables: + * - MINIO_ENDPOINT + * - MINIO_PORT + * - MINIO_ACCESS_KEY + * - MINIO_SECRET_KEY + * - MINIO_BUCKET + * - MINIO_USE_SSL (optional) + * - MINIO_REGION (optional) + */ + static forEnv(options?: { + prefix?: string + defaultBucket?: string + isGlobal?: boolean + }): DynamicModule { + const { isGlobal = true, prefix, defaultBucket } = options ?? {} + + const configProvider: Provider = { + provide: MINIO_CONFIG, + useFactory: () => buildConfigFromEnv({ prefix, defaultBucket }), + } + + return { + module: MinioModule, + global: isGlobal, + providers: [configProvider, MinioService], + exports: [MinioService, MINIO_CONFIG], + } + } +} + +/** + * Decorator to inject MinioService + * + * @example + * ```ts + * @Injectable() + * export class MyService { + * constructor(@InjectMinio() private readonly minio: MinioService) {} + * } + * ``` + */ +export function InjectMinio(): ParameterDecorator { + return ( + target: object, + propertyKey: string | symbol | undefined, + parameterIndex: number + ) => { + // The Inject decorator from @nestjs/common handles this + // We just provide a semantic alias + const existingParams = + Reflect.getMetadata('design:paramtypes', target, propertyKey as string) || + [] + existingParams[parameterIndex] = MinioService + Reflect.defineMetadata( + 'design:paramtypes', + existingParams, + target, + propertyKey as string + ) + } +} diff --git a/src/nestjs/minio.service.ts b/src/nestjs/minio.service.ts new file mode 100644 index 0000000..023d236 --- /dev/null +++ b/src/nestjs/minio.service.ts @@ -0,0 +1,181 @@ +import { Injectable, Inject, OnModuleDestroy } from '@nestjs/common' +import { MinioClient } from '../client.js' +import type { + MinioConfig, + GenerateUploadUrlOptions, + GenerateDownloadUrlOptions, + PresignedUrl, + UploadOptions, + ListOptions, + ListResult, + ObjectInfo, + CopyOptions, + ExistsResult, +} from '../types.js' +import { MINIO_CONFIG } from './constants.js' + +/** + * NestJS injectable service wrapping MinioClient + * + * @example + * ```ts + * @Injectable() + * export class UploadService { + * constructor(private readonly minio: MinioService) {} + * + * async createUploadUrl(filename: string) { + * return this.minio.generateUploadUrl({ + * key: `uploads/${filename}`, + * contentType: 'image/jpeg', + * }) + * } + * } + * ``` + */ +@Injectable() +export class MinioService implements OnModuleDestroy { + private readonly client: MinioClient + + constructor(@Inject(MINIO_CONFIG) config: MinioConfig) { + this.client = new MinioClient(config) + } + + onModuleDestroy(): void { + // S3Client doesn't require explicit cleanup, but this hook + // is available if needed for future connection pooling + } + + /** + * Get the underlying MinioClient for advanced operations + */ + getClient(): MinioClient { + return this.client + } + + /** + * Get the default bucket name + */ + getBucket(): string { + return this.client.getBucket() + } + + /** + * Generate a presigned URL for uploading an object + */ + async generateUploadUrl( + options: GenerateUploadUrlOptions + ): Promise { + return this.client.generateUploadUrl(options) + } + + /** + * Generate a presigned URL for downloading an object + */ + async generateDownloadUrl( + options: GenerateDownloadUrlOptions + ): Promise { + return this.client.generateDownloadUrl(options) + } + + /** + * Generate a simple download URL (convenience method) + */ + async getDownloadUrl(key: string, expiresIn = 3600): Promise { + return this.client.getDownloadUrl(key, expiresIn) + } + + /** + * Upload data directly to MinIO + */ + async upload(options: UploadOptions): Promise { + return this.client.upload(options) + } + + /** + * Upload a buffer with automatic content-type detection + */ + async uploadBuffer( + key: string, + buffer: Buffer, + contentType?: string + ): Promise { + return this.client.uploadBuffer(key, buffer, contentType) + } + + /** + * Download an object as a Buffer + */ + async download(key: string): Promise { + return this.client.download(key) + } + + /** + * Download an object as a string + */ + async downloadString(key: string, encoding: BufferEncoding = 'utf-8'): Promise { + return this.client.downloadString(key, encoding) + } + + /** + * Download an object as JSON + */ + async downloadJson(key: string): Promise { + return this.client.downloadJson(key) + } + + /** + * Delete an object + */ + async delete(key: string): Promise { + return this.client.delete(key) + } + + /** + * Delete multiple objects + */ + async deleteMany(keys: string[]): Promise { + return this.client.deleteMany(keys) + } + + /** + * Check if an object exists + */ + async exists(key: string): Promise { + return this.client.exists(key) + } + + /** + * List objects in the bucket + */ + async list(options?: ListOptions): Promise { + return this.client.list(options) + } + + /** + * List all objects with a prefix (handles pagination) + */ + async listAll(prefix?: string): Promise { + return this.client.listAll(prefix) + } + + /** + * Copy an object within MinIO + */ + async copy(options: CopyOptions): Promise { + return this.client.copy(options) + } + + /** + * Move an object (copy + delete) + */ + async move(sourceKey: string, destinationKey: string): Promise { + return this.client.move(sourceKey, destinationKey) + } + + /** + * Get object metadata without downloading the content + */ + async getMetadata(key: string): Promise { + return this.client.getMetadata(key) + } +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..6b97e3e --- /dev/null +++ b/src/types.ts @@ -0,0 +1,354 @@ +/** + * MinIO Client Configuration + */ +export interface MinioConfig { + /** + * MinIO server endpoint (e.g., 'localhost:9000' or 'minio.example.com') + */ + endpoint: string + + /** + * Access key for authentication + */ + accessKey: string + + /** + * Secret key for authentication + */ + secretKey: string + + /** + * Default bucket name for operations + */ + bucket: string + + /** + * Region (required by AWS SDK, but MinIO ignores it) + * @default 'us-east-1' + */ + region?: string + + /** + * Use SSL/TLS for connections + * @default false for development, true for production + */ + useSSL?: boolean + + /** + * Port number (if not included in endpoint) + */ + port?: number +} + +/** + * Options for generating presigned upload URLs + */ +export interface GenerateUploadUrlOptions { + /** + * Object key (path in bucket) + */ + key: string + + /** + * Content-Type header for the upload + */ + contentType?: string + + /** + * URL expiration time in seconds + * @default 3600 (1 hour) + */ + expiresIn?: number + + /** + * Custom metadata to attach to the object + */ + metadata?: Record +} + +/** + * Options for generating presigned download URLs + */ +export interface GenerateDownloadUrlOptions { + /** + * Object key (path in bucket) + */ + key: string + + /** + * URL expiration time in seconds + * @default 3600 (1 hour) + */ + expiresIn?: number + + /** + * Content-Disposition header value (e.g., 'attachment; filename="file.pdf"') + */ + responseContentDisposition?: string + + /** + * Content-Type header to return + */ + responseContentType?: string +} + +/** + * Result of presigned URL generation + */ +export interface PresignedUrl { + /** + * The presigned URL + */ + url: string + + /** + * ISO 8601 timestamp when the URL expires + */ + expiresAt: string +} + +/** + * Options for direct upload + */ +export interface UploadOptions { + /** + * Object key (path in bucket) + */ + key: string + + /** + * Data to upload + */ + body: Buffer | Uint8Array | string + + /** + * Content-Type header + */ + contentType: string + + /** + * Custom metadata + */ + metadata?: Record + + /** + * Cache-Control header + */ + cacheControl?: string +} + +/** + * Options for listing objects + */ +export interface ListOptions { + /** + * Prefix to filter objects + */ + prefix?: string + + /** + * Maximum number of objects to return + * @default 1000 + */ + maxKeys?: number + + /** + * Continuation token for pagination + */ + continuationToken?: string + + /** + * Delimiter for grouping (e.g., '/' for directory-like listing) + */ + delimiter?: string +} + +/** + * Object metadata from MinIO + */ +export interface ObjectInfo { + /** + * Object key + */ + key: string + + /** + * Last modification date + */ + lastModified: Date + + /** + * Object size in bytes + */ + size: number + + /** + * ETag (usually MD5 hash) + */ + etag: string + + /** + * Storage class + */ + storageClass?: string +} + +/** + * Result of listing objects + */ +export interface ListResult { + /** + * Objects found + */ + objects: ObjectInfo[] + + /** + * Common prefixes (when using delimiter) + */ + prefixes: string[] + + /** + * Whether there are more results + */ + isTruncated: boolean + + /** + * Token for next page + */ + nextContinuationToken?: string +} + +/** + * Options for copy operation + */ +export interface CopyOptions { + /** + * Source object key + */ + sourceKey: string + + /** + * Destination object key + */ + destinationKey: string + + /** + * Source bucket (if different from default) + */ + sourceBucket?: string + + /** + * Destination bucket (if different from default) + */ + destinationBucket?: string + + /** + * New metadata (replaces source metadata) + */ + metadata?: Record + + /** + * New content type + */ + contentType?: string +} + +/** + * Result of object existence check + */ +export interface ExistsResult { + /** + * Whether the object exists + */ + exists: boolean + + /** + * Object metadata if exists + */ + metadata?: { + contentType?: string + contentLength?: number + lastModified?: Date + etag?: string + } +} + +/** + * MIME type mapping for common extensions + */ +export const MIME_TYPES: Record = { + // Images + jpg: 'image/jpeg', + jpeg: 'image/jpeg', + png: 'image/png', + gif: 'image/gif', + webp: 'image/webp', + svg: 'image/svg+xml', + ico: 'image/x-icon', + bmp: 'image/bmp', + tiff: 'image/tiff', + tif: 'image/tiff', + avif: 'image/avif', + heic: 'image/heic', + heif: 'image/heif', + + // Documents + pdf: 'application/pdf', + doc: 'application/msword', + docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + xls: 'application/vnd.ms-excel', + xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + ppt: 'application/vnd.ms-powerpoint', + pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + + // Text + txt: 'text/plain', + csv: 'text/csv', + json: 'application/json', + xml: 'application/xml', + html: 'text/html', + htm: 'text/html', + css: 'text/css', + js: 'application/javascript', + ts: 'application/typescript', + + // Archives + zip: 'application/zip', + tar: 'application/x-tar', + gz: 'application/gzip', + rar: 'application/vnd.rar', + '7z': 'application/x-7z-compressed', + + // Audio + mp3: 'audio/mpeg', + wav: 'audio/wav', + ogg: 'audio/ogg', + flac: 'audio/flac', + aac: 'audio/aac', + m4a: 'audio/mp4', + + // Video + mp4: 'video/mp4', + webm: 'video/webm', + avi: 'video/x-msvideo', + mov: 'video/quicktime', + mkv: 'video/x-matroska', + + // Fonts + woff: 'font/woff', + woff2: 'font/woff2', + ttf: 'font/ttf', + otf: 'font/otf', + eot: 'application/vnd.ms-fontobject', +} + +/** + * Get MIME type from file extension + */ +export function getMimeType(filenameOrExtension: string): string { + const ext = filenameOrExtension.includes('.') + ? filenameOrExtension.split('.').pop()?.toLowerCase() + : filenameOrExtension.toLowerCase() + + return MIME_TYPES[ext || ''] || 'application/octet-stream' +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..5ac956b --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2022"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "rootDir": "./src", + "resolveJsonModule": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +}