@airoom/nextmin-node 1.3.0 → 1.4.1

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.
@@ -5,7 +5,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.setupFileRoutes = setupFileRoutes;
7
7
  const multer_1 = __importDefault(require("multer"));
8
- const filename_1 = require("../../files/filename");
9
8
  function setupFileRoutes(ctx) {
10
9
  if (!ctx.fileStorage)
11
10
  return;
@@ -24,13 +23,8 @@ function setupFileRoutes(ctx) {
24
23
  .json({ error: true, message: 'No files uploaded' });
25
24
  }
26
25
  const results = await Promise.all(files.map(async (f) => {
27
- const folder = shortFolder();
28
- const ext = (f.originalname.match(/\.([A-Za-z0-9]{1,8})$/)?.[1] ??
29
- (0, filename_1.extFromMime)(f.mimetype) ??
30
- 'bin').toLowerCase();
31
- const key = `${folder}/${shortUid()}.${ext}`;
32
26
  const out = await ctx.fileStorage.upload({
33
- key,
27
+ originalFilename: f.originalname,
34
28
  body: f.buffer,
35
29
  contentType: f.mimetype,
36
30
  metadata: { originalName: f.originalname || '' },
@@ -68,13 +62,8 @@ function setupFileRoutes(ctx) {
68
62
  .json({ error: true, message: 'No files uploaded' });
69
63
  }
70
64
  const results = await Promise.all(files.map(async (f) => {
71
- const folder = shortFolder();
72
- const ext = (f.originalname.match(/\.([A-Za-z0-9]{1,8})$/)?.[1] ??
73
- (0, filename_1.extFromMime)(f.mimetype) ??
74
- 'bin').toLowerCase();
75
- const key = `${folder}/${shortUid()}.${ext}`;
76
65
  const out = await ctx.fileStorage.upload({
77
- key,
66
+ originalFilename: f.originalname,
78
67
  body: f.buffer,
79
68
  contentType: f.mimetype,
80
69
  metadata: { originalName: f.originalname || '' },
@@ -126,13 +115,3 @@ function setupFileRoutes(ctx) {
126
115
  }
127
116
  });
128
117
  }
129
- function shortFolder() {
130
- const d = new Date();
131
- const y = d.getFullYear();
132
- const m = String(d.getMonth() + 1).padStart(2, '0');
133
- const day = String(d.getDate()).padStart(2, '0');
134
- return `uploads/${y}/${m}/${day}`;
135
- }
136
- function shortUid() {
137
- return (Date.now().toString(36) + Math.random().toString(36).slice(2, 6)).toLowerCase();
138
- }
@@ -2,6 +2,8 @@ export type FileProvider = 's3' | 'gcs' | 'local' | (string & {});
2
2
  export interface UploadPayload {
3
3
  /** Path-like key inside bucket/provider, e.g. "uploads/userId/2025/08/18/file.png" */
4
4
  key?: string;
5
+ /** Original filename for slugification (e.g., "My Document.pdf") */
6
+ originalFilename?: string;
5
7
  /** Raw file content */
6
8
  body: Buffer | Uint8Array | ArrayBuffer;
7
9
  /** MIME type */
@@ -0,0 +1,16 @@
1
+ import { FileStorageAdapter, UploadResult, UploadPayload, FileProvider } from './FileStorageAdapter';
2
+ export interface LocalFileStorageOptions {
3
+ uploadDir: string;
4
+ publicUrlPrefix: string;
5
+ }
6
+ export declare class LocalFileStorageAdapter implements FileStorageAdapter {
7
+ readonly name: FileProvider;
8
+ private uploadDir;
9
+ private publicUrlPrefix;
10
+ constructor(options: LocalFileStorageOptions);
11
+ upload(input: UploadPayload): Promise<UploadResult>;
12
+ delete(key: string): Promise<{
13
+ deleted: boolean;
14
+ }>;
15
+ getPublicUrl(key: string): string | null;
16
+ }
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.LocalFileStorageAdapter = void 0;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const stream_1 = require("stream");
10
+ const filename_1 = require("./filename");
11
+ const crypto_1 = __importDefault(require("crypto"));
12
+ class LocalFileStorageAdapter {
13
+ constructor(options) {
14
+ this.name = 'local';
15
+ this.uploadDir = options.uploadDir;
16
+ this.publicUrlPrefix = options.publicUrlPrefix.replace(/\/$/, '');
17
+ if (!fs_1.default.existsSync(this.uploadDir)) {
18
+ fs_1.default.mkdirSync(this.uploadDir, { recursive: true });
19
+ }
20
+ }
21
+ async upload(input) {
22
+ const originalName = input.originalFilename || 'unknown-file';
23
+ const randomId = crypto_1.default.randomBytes(4).toString('hex');
24
+ const uniqueName = input.key || (0, filename_1.filenameWithRandomId)(originalName, randomId);
25
+ const filePath = path_1.default.join(this.uploadDir, uniqueName);
26
+ const writeStream = fs_1.default.createWriteStream(filePath);
27
+ // Convert body to stream if it is a Buffer
28
+ const readStream = new stream_1.Readable();
29
+ readStream.push(input.body);
30
+ readStream.push(null);
31
+ await new Promise((resolve, reject) => {
32
+ readStream.pipe(writeStream);
33
+ readStream.on('error', reject);
34
+ writeStream.on('finish', () => resolve());
35
+ writeStream.on('error', reject);
36
+ });
37
+ return {
38
+ provider: 'local',
39
+ key: uniqueName,
40
+ url: `${this.publicUrlPrefix}/${uniqueName}`,
41
+ bucket: this.uploadDir,
42
+ size: input.body.byteLength,
43
+ contentType: input.contentType,
44
+ };
45
+ }
46
+ async delete(key) {
47
+ const filePath = path_1.default.join(this.uploadDir, key);
48
+ if (fs_1.default.existsSync(filePath)) {
49
+ await fs_1.default.promises.unlink(filePath);
50
+ return { deleted: true };
51
+ }
52
+ return { deleted: false };
53
+ }
54
+ getPublicUrl(key) {
55
+ return `${this.publicUrlPrefix}/${key}`;
56
+ }
57
+ }
58
+ exports.LocalFileStorageAdapter = LocalFileStorageAdapter;
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.S3FileStorageAdapter = void 0;
4
4
  const client_s3_1 = require("@aws-sdk/client-s3");
5
+ const filename_1 = require("./filename");
5
6
  // Encode per path segment, keep slashes
6
7
  function encodeKeySegments(key) {
7
8
  return key.split('/').map(encodeURIComponent).join('/');
@@ -25,7 +26,7 @@ class S3FileStorageAdapter {
25
26
  async upload(input) {
26
27
  if (!input.body)
27
28
  throw new Error('S3 upload: "body" is required');
28
- const key = input.key ?? this.randomKey();
29
+ const key = input.key ?? this.randomKey(input.originalFilename);
29
30
  // Build params without ACL by default
30
31
  const params = {
31
32
  Bucket: this.bucket,
@@ -71,14 +72,19 @@ class S3FileStorageAdapter {
71
72
  return `https://${this.bucket}.s3.${this.regionStr}.amazonaws.com/${encKey}`;
72
73
  }
73
74
  // ---- private helpers ----
74
- randomKey() {
75
+ randomKey(originalFilename) {
75
76
  const ts = new Date();
76
77
  const y = ts.getUTCFullYear();
77
78
  const m = String(ts.getUTCMonth() + 1).padStart(2, '0');
78
79
  const d = String(ts.getUTCDate()).padStart(2, '0');
79
- const rand = Math.random().toString(36).slice(2, 10);
80
- // You wanted keys like uploads/YYYY/MM/DD/uid.ext (ext is added by router)
81
- return `uploads/${y}/${m}/${d}/${rand}`;
80
+ const randomId = Math.random().toString(36).slice(2, 10);
81
+ // Generate filename: slugified-name-randomid.ext or just randomid if no original name
82
+ const filename = originalFilename
83
+ ? (0, filename_1.filenameWithRandomId)(originalFilename, randomId)
84
+ : randomId;
85
+ // Path: uploads/YYYY/MM/DD/filename
86
+ const fullPath = `uploads/${y}/${m}/${d}/${filename}`;
87
+ return fullPath;
82
88
  }
83
89
  }
84
90
  exports.S3FileStorageAdapter = S3FileStorageAdapter;
@@ -1,4 +1,14 @@
1
1
  export declare function sanitizeFilename(name: string): string;
2
+ /**
3
+ * Slugify a filename: lowercase, replace spaces/special chars with hyphens
4
+ * Example: "My Photo (2024).jpg" → "my-photo-2024.jpg"
5
+ */
6
+ export declare function slugify(text: string): string;
7
+ /**
8
+ * Generate a filename from original name + random ID
9
+ * Example: "my-document.pdf" + "abc123" → "my-document-abc123.pdf"
10
+ */
11
+ export declare function filenameWithRandomId(originalName: string, randomId: string): string;
2
12
  export declare function withTimestampPrefix(name: string): string;
3
13
  export declare function joinKey(...parts: string[]): string;
4
14
  export declare function ensureExt(name: string, fallbackExt?: string): string;
@@ -1,6 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.sanitizeFilename = sanitizeFilename;
4
+ exports.slugify = slugify;
5
+ exports.filenameWithRandomId = filenameWithRandomId;
4
6
  exports.withTimestampPrefix = withTimestampPrefix;
5
7
  exports.joinKey = joinKey;
6
8
  exports.ensureExt = ensureExt;
@@ -9,6 +11,39 @@ function sanitizeFilename(name) {
9
11
  const base = name.replace(/[/\\?%*:|"<>]/g, '-').replace(/\s+/g, '-');
10
12
  return base.replace(/-+/g, '-').toLowerCase();
11
13
  }
14
+ /**
15
+ * Slugify a filename: lowercase, replace spaces/special chars with hyphens
16
+ * Example: "My Photo (2024).jpg" → "my-photo-2024.jpg"
17
+ */
18
+ function slugify(text) {
19
+ return text
20
+ .toLowerCase()
21
+ .trim()
22
+ // Replace spaces and underscores with hyphens
23
+ .replace(/[\s_]+/g, '-')
24
+ // Remove special characters except hyphens, dots, and alphanumeric
25
+ .replace(/[^\w\-\.]+/g, '')
26
+ // Replace multiple consecutive hyphens with single hyphen
27
+ .replace(/-+/g, '-')
28
+ // Remove leading/trailing hyphens
29
+ .replace(/^-+|-+$/g, '');
30
+ }
31
+ /**
32
+ * Generate a filename from original name + random ID
33
+ * Example: "my-document.pdf" + "abc123" → "my-document-abc123.pdf"
34
+ */
35
+ function filenameWithRandomId(originalName, randomId) {
36
+ // Extract extension
37
+ const lastDot = originalName.lastIndexOf('.');
38
+ const hasExt = lastDot > 0 && lastDot < originalName.length - 1;
39
+ const nameWithoutExt = hasExt ? originalName.slice(0, lastDot) : originalName;
40
+ const ext = hasExt ? originalName.slice(lastDot) : '';
41
+ // Slugify the name part
42
+ const slugifiedName = slugify(nameWithoutExt);
43
+ // Combine: slugified-name-randomid.ext
44
+ const result = `${slugifiedName}-${randomId}${ext}`;
45
+ return result;
46
+ }
12
47
  function withTimestampPrefix(name) {
13
48
  const ts = Date.now();
14
49
  return `${ts}-${name}`;
package/dist/index.d.ts CHANGED
@@ -3,6 +3,7 @@ export { MongoAdapter } from './database/MongoAdapter';
3
3
  export { generateApiKey } from './utils/apiKey';
4
4
  export * from './files/FileStorageAdapter';
5
5
  export * from './files/S3FileStorageAdapter';
6
+ export * from './files/LocalFileStorageAdapter';
6
7
  export * from './files/filename';
7
8
  /**
8
9
  * Compose the public router:
package/dist/index.js CHANGED
@@ -28,6 +28,7 @@ var apiKey_1 = require("./utils/apiKey");
28
28
  Object.defineProperty(exports, "generateApiKey", { enumerable: true, get: function () { return apiKey_1.generateApiKey; } });
29
29
  __exportStar(require("./files/FileStorageAdapter"), exports);
30
30
  __exportStar(require("./files/S3FileStorageAdapter"), exports);
31
+ __exportStar(require("./files/LocalFileStorageAdapter"), exports);
31
32
  __exportStar(require("./files/filename"), exports);
32
33
  function isMultipart(req) {
33
34
  const ct = (req.headers['content-type'] || '').toLowerCase();
@@ -16,6 +16,7 @@ export interface Attributes {
16
16
  unique?: boolean;
17
17
  sensitive?: boolean;
18
18
  writeOnly?: boolean;
19
+ rich?: boolean;
19
20
  }
20
21
  export interface Schema {
21
22
  modelName: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@airoom/nextmin-node",
3
- "version": "1.3.0",
3
+ "version": "1.4.1",
4
4
  "license": "MIT",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -42,4 +42,4 @@
42
42
  "publishConfig": {
43
43
  "access": "public"
44
44
  }
45
- }
45
+ }