@ccci/micro-server 1.1.3 → 1.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=Uploader.integration.test.d.ts.map
@@ -32,5 +32,6 @@ export type ApplicationOptionType = {
32
32
  s3region?: string;
33
33
  s3AccessKeyId?: string;
34
34
  s3SecretAccessKey?: string;
35
+ appVersion?: string;
35
36
  };
36
37
  //# sourceMappingURL=ApplicationOptionType.d.ts.map
@@ -8,4 +8,61 @@ export type UploadedFileType = {
8
8
  location?: string;
9
9
  filesize?: number;
10
10
  };
11
+ export type InitUpload = {
12
+ uploadId: string;
13
+ minioUploadId: string;
14
+ pathName: string;
15
+ message: string;
16
+ };
17
+ export type UploadPartConfig = {
18
+ bucketName: string;
19
+ objectName: string;
20
+ uploadID: string;
21
+ partNumber: number;
22
+ headers: unknown;
23
+ };
24
+ export type UploadSession = {
25
+ bucket: string;
26
+ uploadId: string;
27
+ fileName: string;
28
+ objectName: string;
29
+ minioUploadId: string;
30
+ parts: Array<{
31
+ part: number;
32
+ etag: string;
33
+ }>;
34
+ metadata?: Record<string, string>;
35
+ createdAt: Date;
36
+ };
37
+ export type UploadChunk = {
38
+ uploadId: string;
39
+ chunkNumber: number;
40
+ etag: string;
41
+ key: string;
42
+ part: number;
43
+ uploadedPart: number;
44
+ message: string;
45
+ };
46
+ export type CompleteUploadResult = {
47
+ etag: {
48
+ versionId: string;
49
+ etag: string;
50
+ };
51
+ path: string;
52
+ bucket: string;
53
+ location: string;
54
+ fileSize: number;
55
+ mimeType: string;
56
+ objectName: string;
57
+ };
58
+ export type UploadTypes = {
59
+ bucket: string;
60
+ objectKey: string;
61
+ objectName: string;
62
+ };
63
+ export type UploadPart = {
64
+ etag: string;
65
+ key: string;
66
+ part: number;
67
+ };
11
68
  //# sourceMappingURL=UploadedFileType.d.ts.map
@@ -0,0 +1,36 @@
1
+ import type { RedisClientType } from "redis";
2
+ import { BaseRedisConnector } from "./BaseRedisConnector";
3
+ import { type PaginatedResult, type QueryOptions } from "./RedisQueryHelper";
4
+ export declare class BaseRedis extends BaseRedisConnector {
5
+ private clientReady?;
6
+ protected constructor();
7
+ protected ready: () => Promise<RedisClientType>;
8
+ private chunk;
9
+ /**
10
+ * Bounded, index-driven collection sync:
11
+ * - Stores IDs in a sorted set (score from scoreSelector)
12
+ * - Stores each item in a per-item hash
13
+ * - Trims oldest items beyond maxItems and unlinks their hashes
14
+ */
15
+ protected syncIndexedCollection: <T>(params: {
16
+ items: T[];
17
+ idSelector: (item: T) => string | number;
18
+ scoreSelector: (item: T) => number | Date;
19
+ indexKey: string;
20
+ itemPrefix: string;
21
+ maxItems: number;
22
+ ttlSeconds?: number;
23
+ batchSize?: number;
24
+ }) => Promise<void>;
25
+ /**
26
+ * Read indexed collection into memory (bounded by maxItems).
27
+ * Applies QueryHelper for search/sort/paginate.
28
+ */
29
+ protected getIndexedItems: <T>(params: {
30
+ indexKey: string;
31
+ itemPrefix: string;
32
+ options?: QueryOptions;
33
+ defaultSearchFields?: string[];
34
+ }) => Promise<T[] | PaginatedResult<T>>;
35
+ }
36
+ //# sourceMappingURL=BaseRedis.d.ts.map
@@ -0,0 +1,10 @@
1
+ import { type RedisClientType } from "redis";
2
+ export declare class BaseRedisConnector {
3
+ private static client;
4
+ private static connectPromise;
5
+ protected client: RedisClientType;
6
+ protected constructor();
7
+ ensureConnected(): Promise<void>;
8
+ disconnect(): Promise<void>;
9
+ }
10
+ //# sourceMappingURL=BaseRedisConnector.d.ts.map
@@ -1,3 +1,4 @@
1
+ /// <reference types="node" />
1
2
  /**
2
3
  * Retrieves the MIME type for a given file name based on its extension.
3
4
  * @param fileName - The name of the file to extract the MIME type for.
@@ -36,4 +37,36 @@ export declare const getCurrentWeek: () => {
36
37
  startOfWeek: Date;
37
38
  endOfWeek: Date;
38
39
  };
40
+ /**
41
+ * Consumes a readable stream and aggregates all data chunks into a single Buffer.
42
+ * * @param {stream.Readable} res - The incoming data stream.
43
+ * @returns {Promise<Buffer>} A promise that resolves to the complete binary data.
44
+ */
45
+ export declare function readAsBuffer(res: any): Promise<Buffer>;
46
+ /**
47
+ * Reads an HTTP response body in its entirety and converts it to a string.
48
+ * Useful for parsing XML error messages or small metadata responses.
49
+ * * @param {http.IncomingMessage} res - The HTTP response object.
50
+ * @returns {Promise<string>} The body content decoded as a string.
51
+ */
52
+ export declare function readAsString(res: any): Promise<string>;
53
+ /**
54
+ * Parses an XML string into a JavaScript object using fast-xml-parser.
55
+ * * @throws {S3XmlError} If the XML contains an <Error> tag, it throws the error body.
56
+ */
57
+ export declare const parseXml: (xml: string) => any;
58
+ /**
59
+ * Specifically extracts the CopyPartResult from an S3 XML response.
60
+ * Used during multipart copy operations or part uploads that return XML.
61
+ */
62
+ export declare const uploadPartParser: (xml: string) => any;
63
+ /**
64
+ * Removes various encodings of double quotes from the start and end of an ETag.
65
+ * * @description
66
+ * S3-compatible storage often returns ETags wrapped in literal quotes or
67
+ * HTML entities. This regex-based sanitizer ensures only the raw hash remains.
68
+ * @param {string} [etag=''] - The raw ETag string from headers or XML.
69
+ * @returns {string} The cleaned ETag.
70
+ */
71
+ export declare const sanitizeETag: (etag: string) => string;
39
72
  //# sourceMappingURL=Mixins.d.ts.map
@@ -0,0 +1,38 @@
1
+ export type QueryOptions = {
2
+ forceRefresh?: boolean;
3
+ paginate?: boolean;
4
+ limit?: number;
5
+ page?: number;
6
+ sort?: string;
7
+ search?: string;
8
+ fulltext?: boolean;
9
+ defaultSearchFields?: string[];
10
+ };
11
+ export type PaginatedResult<T> = {
12
+ rows: T[];
13
+ count: number;
14
+ meta: {
15
+ total: number;
16
+ limit: number;
17
+ page: number;
18
+ totalPages: number;
19
+ hasNextPage: boolean;
20
+ hasPrevPage: boolean;
21
+ sort: string;
22
+ };
23
+ };
24
+ type SearchOptions = {
25
+ fulltext?: boolean;
26
+ defaultFields?: string[];
27
+ };
28
+ export declare class QueryHelper {
29
+ private static getFieldValue;
30
+ private static buildHaystack;
31
+ private static matchesSearch;
32
+ static filterBySearch<T>(items: T[], search: string, options?: SearchOptions): T[];
33
+ static sort<T>(items: T[], sort: string): T[];
34
+ static paginate<T>(items: T[], limit: number, page: number, sort: string): PaginatedResult<T>;
35
+ static applyQuery<T>(items: T[], options?: QueryOptions): T[] | PaginatedResult<T>;
36
+ }
37
+ export {};
38
+ //# sourceMappingURL=RedisQueryHelper.d.ts.map
@@ -1,10 +1,12 @@
1
+ /// <reference types="node" />
1
2
  import { UploadedFile } from 'express-fileupload';
2
3
  import * as minio from 'minio';
3
4
  import { ApplicationOptionType } from '..';
4
- import { UploadedFileType } from '@/types/UploadedFileType';
5
+ import { UploadedFileType, InitUpload, UploadChunk, CompleteUploadResult, UploadTypes, UploadPart } from '@/types/UploadedFileType';
5
6
  import { RemoveOptions } from 'minio';
6
7
  export default class Uploader {
7
8
  static client: minio.Client;
9
+ static clientEndpoint: string;
8
10
  constructor();
9
11
  /**
10
12
  * Initializes the minIO client instance
@@ -29,5 +31,97 @@ export default class Uploader {
29
31
  */
30
32
  preview(bucket: string, file: string): Promise<void>;
31
33
  static delete(bucketName: string, objectName: string, removeOpts?: RemoveOptions): Promise<void>;
34
+ /**
35
+ * Permanently cancels all pending multipart uploads within a specified S3/MinIO bucket.
36
+ * @description
37
+ * This is a cleanup utility. Incomplete multipart uploads occupy storage space but are not visible as objects. This method iterates through the bucket's "incomplete" stream and issues an abort command for each. Use this to recover storage or clear stuck uploads after a service interruption.
38
+ * @param {string} bucketName - The unique identifier of the target MinIO bucket.
39
+ * @returns {Promise<void>} Resolves when the stream has finished processing all abort requests.
40
+ * @throws {Error} If the bucket is inaccessible or the list operation fails.
41
+ * @example
42
+ * await Uploader.clearAllMultipartUploads('temp-uploads');
43
+ */
44
+ static clearAllMultipartUploads(bucketName: string): Promise<void>;
45
+ /**
46
+ * Starts a new multipart upload session and registers it in the local session cache.
47
+ * @description
48
+ * This method performs two actions:
49
+ * 1. Requests a new Multipart Upload ID from the MinIO/S3 server.
50
+ * 2. Initializes an internal tracking session in `uploadSessions` for part management.
51
+ * @param {string} bucket - Target bucket name.
52
+ * @param {string} fileName - The final destination filename/path.
53
+ * @param {Record<string, string>} [metadata={}] - Key-value pairs for S3 metadata.
54
+ * - Standard headers: `Content-Type`, `Cache-Control`, etc.
55
+ * - Custom tags: Use `X-Amz-meta-` prefix for custom attributes.
56
+ * @returns {Promise<InitUpload>}
57
+ * Returns the internal session ID and the provider's upload ID.
58
+ * @throws {Error} If the connection to MinIO fails or the bucket does not exist.
59
+ */
60
+ static initiateUpload(bucket: string, fileName: string, metadata?: Record<string, string>): Promise<InitUpload>;
61
+ /**
62
+ * Processes and uploads a single data part to an active multipart session.
63
+ * @description
64
+ * This method normalizes various input formats (Buffer, string, or Uint8Array) into a binary Buffer, dispatches the upload to MinIO via `uploadPart`, and updates the internal session state with the returned ETag.
65
+ * @param {string} uploadSessionId - The internal UUID generated during `initiateUpload`.
66
+ * @param {number} chunkNumber - The 1-based index of the part.
67
+ * @param {Buffer | string | Uint8Array} chunkData - The raw binary data or encoded string.
68
+ * @returns {Promise<UploadChunk>} Returns metadata about the uploaded part, including:
69
+ * - `etag`: The S3 identifier for this specific part (required for completion).
70
+ * - `uploadedPart`: The total count of parts successfully tracked in this session so far.
71
+ */
72
+ static uploadChunk(uploadSessionId: string, chunkNumber: number, chunkData: Buffer | string | Uint8Array): Promise<UploadChunk>;
73
+ /**
74
+ * Finalizes the multipart upload by assembling all uploaded parts into a single object.
75
+ * @description
76
+ * This method performs the final handshake with MinIO/S3. It:
77
+ * 1. Retrieves and sorts the accumulated ETags by part number (mandatory for S3 compliance).
78
+ * 2. Signals the storage server to merge the data parts.
79
+ * 3. Extracts file metadata (size, mime-type) from the session.
80
+ * 4. Purges the internal `uploadSessions` cache to prevent memory leaks.
81
+ * @param {string} uploadSessionId - The internal tracking ID of the session to be closed.
82
+ * @returns {Promise<Object>} A curated result object containing:
83
+ * - `etag`: The final entity tag for the completed object.
84
+ * - `location`: The public/internal URL of the uploaded file.
85
+ * - `fileSize`: Parsed size from the 'X-Amz-meta-filesize' metadata.
86
+ * @throws {Error} If parts are missing, the session is not found, or S3 assembly fails.
87
+ */
88
+ static completeUpload(uploadSessionId: string): Promise<CompleteUploadResult>;
89
+ /**
90
+ * Streams an object from storage directly to an HTTP response.
91
+ * @description
92
+ * Fetches object metadata (stat) to populate necessary download headers before
93
+ * piping the data stream to the client. This avoids loading the entire file
94
+ * into memory, making it efficient for large file transfers.
95
+ * * @param {Response} res - The Express/Node.js response object.
96
+ * @param {UploadTypes} options - Configuration object containing:
97
+ * - `bucket`: Target bucket name.
98
+ * - `objectKey`: The internal storage path (e.g., 'stream/123').
99
+ * - `objectName`: The "friendly" filename for the user's browser.
100
+ * @returns {Promise<void>} Resolves once the stream has been successfully piped.
101
+ * @throws {Error} If the object does not exist or metadata cannot be retrieved.
102
+ */
103
+ static streamDownload(res: any, { bucket, objectKey, objectName }: UploadTypes): Promise<void>;
104
+ /**
105
+ * Executes a low-level Multipart Upload Part request.
106
+ * * @description
107
+ * This is a custom wrapper around the S3/MinIO PUT Object Part API. It manually constructs the query parameters and headers required for a part upload. Crucially, it handles inconsistent server responses by attempting to extract the ETag from both the response XML body and the HTTP headers.
108
+ * @param {Object} partConfig - Configuration for the specific part.
109
+ * @param {string} partConfig.bucketName - Target bucket.
110
+ * @param {string} partConfig.objectName - The internal path/key of the object.
111
+ * @param {string} partConfig.uploadID - The ID generated by `initiateNewMultipartUpload`.
112
+ * @param {number} partConfig.partNumber - 1-based index of the part.
113
+ * @param {unknown} partConfig.headers - HTTP headers (e.g., Content-Length, Content-MD5).
114
+ * @param {any} [payload] - The binary data buffer for this part.
115
+ * @returns {Promise<UploadPart>}
116
+ * Returns a sanitized ETag required for the final `completeUpload` call.
117
+ * @throws {Error} If no ETag is found in the response or the network request fails.
118
+ */
119
+ static uploadPart: (partConfig: {
120
+ bucketName: string;
121
+ objectName: string;
122
+ uploadID: string;
123
+ partNumber: number;
124
+ headers: unknown;
125
+ }, payload?: any) => Promise<UploadPart>;
32
126
  }
33
127
  //# sourceMappingURL=Uploader.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ccci/micro-server",
3
- "version": "1.1.3",
3
+ "version": "1.1.5",
4
4
  "module": "index.ts",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -23,7 +23,10 @@
23
23
  "@types/nodemailer": "^6.4.15",
24
24
  "prettier": "^3.0.3",
25
25
  "rimraf": "^5.0.5",
26
- "typescript": "^5.2.2"
26
+ "typescript": "^5.2.2",
27
+ "vite": "^5.0.10",
28
+ "vite-tsconfig-paths": "^6.1.0",
29
+ "vitest": "^1.1.0"
27
30
  },
28
31
  "peerDependencies": {
29
32
  "typescript": "^5.0.0"
@@ -45,6 +48,7 @@
45
48
  "express": "^4.19.2",
46
49
  "express-fileupload": "^1.5.0",
47
50
  "express-list-endpoints": "^7.1.1",
51
+ "fast-xml-parser": "^5.3.4",
48
52
  "firebase": "^10.13.1",
49
53
  "firebase-admin": "^12.4.0",
50
54
  "handlebars": "^4.7.8",
@@ -55,6 +59,7 @@
55
59
  "nodemailer": "^6.9.13",
56
60
  "pg": "^8.11.5",
57
61
  "pg-hstore": "^2.3.4",
62
+ "redis": "^5.11.0",
58
63
  "sequelize": "^6.37.3",
59
64
  "socket.io": "^4.7.5",
60
65
  "winston": "^3.13.0"