chore: initial commit

This commit is contained in:
Lilith 2026-01-21 11:37:29 -08:00
commit cd7ea67942
44 changed files with 3621 additions and 0 deletions

121
dist/client.d.ts vendored Normal file
View file

@ -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<PresignedUrl>;
/**
* Generate a presigned URL for downloading an object
*/
generateDownloadUrl(options: GenerateDownloadUrlOptions): Promise<PresignedUrl>;
/**
* Generate a simple download URL (convenience method)
*/
getDownloadUrl(key: string, expiresIn?: number): Promise<string>;
/**
* Upload data directly to MinIO
*/
upload(options: UploadOptions): Promise<void>;
/**
* Upload a buffer with automatic content-type detection
*/
uploadBuffer(key: string, buffer: Buffer, contentType?: string): Promise<void>;
/**
* Download an object as a Buffer
*/
download(key: string): Promise<Buffer>;
/**
* Download an object as a string
*/
downloadString(key: string, encoding?: BufferEncoding): Promise<string>;
/**
* Download an object as JSON
*/
downloadJson<T = unknown>(key: string): Promise<T>;
/**
* Delete an object
*/
delete(key: string): Promise<void>;
/**
* Delete multiple objects
*/
deleteMany(keys: string[]): Promise<void>;
/**
* Check if an object exists
*/
exists(key: string): Promise<ExistsResult>;
/**
* List objects in the bucket
*/
list(options?: ListOptions): Promise<ListResult>;
/**
* List all objects with a prefix (handles pagination)
*/
listAll(prefix?: string): Promise<ObjectInfo[]>;
/**
* Copy an object within MinIO
*/
copy(options: CopyOptions): Promise<void>;
/**
* Move an object (copy + delete)
*/
move(sourceKey: string, destinationKey: string): Promise<void>;
/**
* Get object metadata without downloading the content
*/
getMetadata(key: string): Promise<ExistsResult['metadata']>;
}
/**
* Create a MinioClient instance
*/
export declare function createMinioClient(config: MinioConfig): MinioClient;
//# sourceMappingURL=client.d.ts.map

1
dist/client.d.ts.map vendored Normal file
View file

@ -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"}

314
dist/client.js vendored Normal file
View file

@ -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);
}

128
dist/config.d.ts vendored Normal file
View file

@ -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<string, string | undefined>;
}): 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<MinioConfig>;
/**
* 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<MinioConfig>;
//# sourceMappingURL=config.d.ts.map

1
dist/config.d.ts.map vendored Normal file
View file

@ -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"}

173
dist/config.js vendored Normal file
View file

@ -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',
};
}

47
dist/index.d.ts vendored Normal file
View file

@ -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

1
dist/index.d.ts.map vendored Normal file
View file

@ -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"}

47
dist/index.js vendored Normal file
View file

@ -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';

12
dist/nestjs/constants.d.ts vendored Normal file
View file

@ -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

1
dist/nestjs/constants.d.ts.map vendored Normal file
View file

@ -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"}

11
dist/nestjs/constants.js vendored Normal file
View file

@ -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');

39
dist/nestjs/index.d.ts vendored Normal file
View file

@ -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

1
dist/nestjs/index.d.ts.map vendored Normal file
View file

@ -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"}

40
dist/nestjs/index.js vendored Normal file
View file

@ -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';

123
dist/nestjs/minio.module.d.ts vendored Normal file
View file

@ -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<MinioConfig>;
/**
* 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

1
dist/nestjs/minio.module.d.ts.map vendored Normal file
View file

@ -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"}

147
dist/nestjs/minio.module.js vendored Normal file
View file

@ -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);
};
}

99
dist/nestjs/minio.service.d.ts vendored Normal file
View file

@ -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<PresignedUrl>;
/**
* Generate a presigned URL for downloading an object
*/
generateDownloadUrl(options: GenerateDownloadUrlOptions): Promise<PresignedUrl>;
/**
* Generate a simple download URL (convenience method)
*/
getDownloadUrl(key: string, expiresIn?: number): Promise<string>;
/**
* Upload data directly to MinIO
*/
upload(options: UploadOptions): Promise<void>;
/**
* Upload a buffer with automatic content-type detection
*/
uploadBuffer(key: string, buffer: Buffer, contentType?: string): Promise<void>;
/**
* Download an object as a Buffer
*/
download(key: string): Promise<Buffer>;
/**
* Download an object as a string
*/
downloadString(key: string, encoding?: BufferEncoding): Promise<string>;
/**
* Download an object as JSON
*/
downloadJson<T = unknown>(key: string): Promise<T>;
/**
* Delete an object
*/
delete(key: string): Promise<void>;
/**
* Delete multiple objects
*/
deleteMany(keys: string[]): Promise<void>;
/**
* Check if an object exists
*/
exists(key: string): Promise<ExistsResult>;
/**
* List objects in the bucket
*/
list(options?: ListOptions): Promise<ListResult>;
/**
* List all objects with a prefix (handles pagination)
*/
listAll(prefix?: string): Promise<ObjectInfo[]>;
/**
* Copy an object within MinIO
*/
copy(options: CopyOptions): Promise<void>;
/**
* Move an object (copy + delete)
*/
move(sourceKey: string, destinationKey: string): Promise<void>;
/**
* Get object metadata without downloading the content
*/
getMetadata(key: string): Promise<ExistsResult['metadata']>;
}
//# sourceMappingURL=minio.service.d.ts.map

1
dist/nestjs/minio.service.d.ts.map vendored Normal file
View file

@ -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"}

157
dist/nestjs/minio.service.js vendored Normal file
View file

@ -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 };

241
dist/types.d.ts vendored Normal file
View file

@ -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<string, string>;
}
/**
* 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<string, string>;
/**
* 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<string, string>;
/**
* 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<string, string>;
/**
* Get MIME type from file extension
*/
export declare function getMimeType(filenameOrExtension: string): string;
//# sourceMappingURL=types.d.ts.map

1
dist/types.d.ts.map vendored Normal file
View file

@ -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"}

71
dist/types.js vendored Normal file
View file

@ -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';
}

166
minio-ts/README.md Normal file
View file

@ -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<number>('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<T>(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

17
node_modules/.bin/tsc generated vendored Executable file
View file

@ -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

17
node_modules/.bin/tsserver generated vendored Executable file
View file

@ -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

1
node_modules/@aws-sdk/client-s3 generated vendored Symbolic link
View file

@ -0,0 +1 @@
../../../../node_modules/.pnpm/@aws-sdk+client-s3@3.971.0/node_modules/@aws-sdk/client-s3

1
node_modules/@aws-sdk/s3-request-presigner generated vendored Symbolic link
View file

@ -0,0 +1 @@
../../../../node_modules/.pnpm/@aws-sdk+s3-request-presigner@3.971.0/node_modules/@aws-sdk/s3-request-presigner

1
node_modules/@lilith/service-registry generated vendored Symbolic link
View file

@ -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

1
node_modules/@nestjs/common generated vendored Symbolic link
View file

@ -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

1
node_modules/@nestjs/config generated vendored Symbolic link
View file

@ -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

1
node_modules/@types/node generated vendored Symbolic link
View file

@ -0,0 +1 @@
../../../../node_modules/.pnpm/@types+node@22.19.7/node_modules/@types/node

1
node_modules/typescript generated vendored Symbolic link
View file

@ -0,0 +1 @@
../../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript

60
package.json Normal file
View file

@ -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
}
}

400
src/client.ts Normal file
View file

@ -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<PresignedUrl> {
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<PresignedUrl> {
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<string> {
const result = await this.generateDownloadUrl({ key, expiresIn })
return result.url
}
/**
* Upload data directly to MinIO
*/
async upload(options: UploadOptions): Promise<void> {
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<void> {
await this.upload({
key,
body: buffer,
contentType: contentType ?? getMimeType(key),
})
}
/**
* Download an object as a Buffer
*/
async download(key: string): Promise<Buffer> {
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<Uint8Array>) {
chunks.push(chunk)
}
return Buffer.concat(chunks)
}
/**
* Download an object as a string
*/
async downloadString(key: string, encoding: BufferEncoding = 'utf-8'): Promise<string> {
const buffer = await this.download(key)
return buffer.toString(encoding)
}
/**
* Download an object as JSON
*/
async downloadJson<T = unknown>(key: string): Promise<T> {
const str = await this.downloadString(key)
return JSON.parse(str) as T
}
/**
* Delete an object
*/
async delete(key: string): Promise<void> {
const command = new DeleteObjectCommand({
Bucket: this.bucket,
Key: key,
})
await this.s3Client.send(command)
}
/**
* Delete multiple objects
*/
async deleteMany(keys: string[]): Promise<void> {
// 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<ExistsResult> {
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<ListResult> {
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<ObjectInfo[]> {
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<void> {
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<void> {
await this.copy({
sourceKey,
destinationKey,
})
await this.delete(sourceKey)
}
/**
* Get object metadata without downloading the content
*/
async getMetadata(key: string): Promise<ExistsResult['metadata']> {
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)
}

275
src/config.ts Normal file
View file

@ -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<string, string | undefined>
}): 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<MinioConfig> {
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<MinioConfig> {
// 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',
}
}

72
src/index.ts Normal file
View file

@ -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'

13
src/nestjs/constants.ts Normal file
View file

@ -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')

62
src/nestjs/index.ts Normal file
View file

@ -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'

199
src/nestjs/minio.module.ts Normal file
View file

@ -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<MinioConfig>
/**
* 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
)
}
}

181
src/nestjs/minio.service.ts Normal file
View file

@ -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<PresignedUrl> {
return this.client.generateUploadUrl(options)
}
/**
* Generate a presigned URL for downloading an object
*/
async generateDownloadUrl(
options: GenerateDownloadUrlOptions
): Promise<PresignedUrl> {
return this.client.generateDownloadUrl(options)
}
/**
* Generate a simple download URL (convenience method)
*/
async getDownloadUrl(key: string, expiresIn = 3600): Promise<string> {
return this.client.getDownloadUrl(key, expiresIn)
}
/**
* Upload data directly to MinIO
*/
async upload(options: UploadOptions): Promise<void> {
return this.client.upload(options)
}
/**
* Upload a buffer with automatic content-type detection
*/
async uploadBuffer(
key: string,
buffer: Buffer,
contentType?: string
): Promise<void> {
return this.client.uploadBuffer(key, buffer, contentType)
}
/**
* Download an object as a Buffer
*/
async download(key: string): Promise<Buffer> {
return this.client.download(key)
}
/**
* Download an object as a string
*/
async downloadString(key: string, encoding: BufferEncoding = 'utf-8'): Promise<string> {
return this.client.downloadString(key, encoding)
}
/**
* Download an object as JSON
*/
async downloadJson<T = unknown>(key: string): Promise<T> {
return this.client.downloadJson<T>(key)
}
/**
* Delete an object
*/
async delete(key: string): Promise<void> {
return this.client.delete(key)
}
/**
* Delete multiple objects
*/
async deleteMany(keys: string[]): Promise<void> {
return this.client.deleteMany(keys)
}
/**
* Check if an object exists
*/
async exists(key: string): Promise<ExistsResult> {
return this.client.exists(key)
}
/**
* List objects in the bucket
*/
async list(options?: ListOptions): Promise<ListResult> {
return this.client.list(options)
}
/**
* List all objects with a prefix (handles pagination)
*/
async listAll(prefix?: string): Promise<ObjectInfo[]> {
return this.client.listAll(prefix)
}
/**
* Copy an object within MinIO
*/
async copy(options: CopyOptions): Promise<void> {
return this.client.copy(options)
}
/**
* Move an object (copy + delete)
*/
async move(sourceKey: string, destinationKey: string): Promise<void> {
return this.client.move(sourceKey, destinationKey)
}
/**
* Get object metadata without downloading the content
*/
async getMetadata(key: string): Promise<ExistsResult['metadata']> {
return this.client.getMetadata(key)
}
}

354
src/types.ts Normal file
View file

@ -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<string, string>
}
/**
* 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<string, string>
/**
* 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<string, string>
/**
* 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<string, string> = {
// 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'
}

20
tsconfig.json Normal file
View file

@ -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"]
}