@bejibun/storage 0.1.0 → 0.1.11

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.
Files changed (44) hide show
  1. package/.prettierignore +45 -0
  2. package/.prettierrc.json +14 -0
  3. package/CHANGELOG.md +74 -0
  4. package/README.md +24 -0
  5. package/benchmarks/README.md +46 -0
  6. package/benchmarks/package.json +12 -0
  7. package/benchmarks/scripts/coldstart-baseline.mjs +9 -0
  8. package/benchmarks/scripts/coldstart-optimized.mjs +9 -0
  9. package/benchmarks/scripts/coldstart.mjs +93 -0
  10. package/benchmarks/scripts/readme-writer.mjs +35 -0
  11. package/benchmarks/scripts/table-format.mjs +123 -0
  12. package/benchmarks/scripts/throughput-baseline.mjs +68 -0
  13. package/benchmarks/scripts/throughput-optimized.mjs +68 -0
  14. package/benchmarks/scripts/throughput.mjs +83 -0
  15. package/builders/StorageBuilder.d.ts +117 -0
  16. package/builders/StorageBuilder.js +165 -32
  17. package/builders/storage/StorageLocalBuilder.d.ts +89 -0
  18. package/builders/storage/StorageLocalBuilder.js +119 -25
  19. package/builders/storage/StorageS3Builder.d.ts +90 -0
  20. package/builders/storage/StorageS3Builder.js +117 -22
  21. package/config/storage.d.ts +3 -0
  22. package/config/storage.js +8 -0
  23. package/configure.js +6 -2
  24. package/enums/StorageDiskDriverEnum.d.ts +5 -0
  25. package/enums/StorageDiskDriverEnum.js +5 -0
  26. package/enums/index.d.ts +4 -1
  27. package/enums/index.js +4 -1
  28. package/eslint.config.js +61 -0
  29. package/exceptions/StorageException.d.ts +10 -0
  30. package/exceptions/StorageException.js +10 -0
  31. package/exceptions/index.d.ts +4 -1
  32. package/exceptions/index.js +4 -1
  33. package/facades/Storage.d.ts +87 -0
  34. package/facades/Storage.js +88 -1
  35. package/facades/index.d.ts +4 -1
  36. package/facades/index.js +4 -1
  37. package/index.d.ts +4 -0
  38. package/index.js +4 -0
  39. package/package.json +23 -12
  40. package/tests/integration/storage.integration.test.ts +145 -0
  41. package/tests/unit/storage.test.ts +177 -0
  42. package/tsconfig.json +2 -2
  43. package/types/index.d.ts +4 -1
  44. package/types/storage.d.ts +138 -138
@@ -0,0 +1,61 @@
1
+ import js from "@eslint/js";
2
+ import eslintConfigPrettier from "eslint-config-prettier";
3
+ import globals from "globals";
4
+ import tseslint from "typescript-eslint";
5
+
6
+ export default tseslint.config(
7
+ {
8
+ ignores: [
9
+ "node_modules/**",
10
+ "dist/**",
11
+ "out/**",
12
+ "coverage/**",
13
+ "public/**",
14
+ "storage/app/**",
15
+ "storage/cache/**",
16
+ "storage/framework/**",
17
+ "bun.lock",
18
+ "*.tsbuildinfo"
19
+ ]
20
+ },
21
+
22
+ js.configs.recommended,
23
+ ...tseslint.configs.recommended,
24
+
25
+ // All TypeScript (server code + resources/views), no React-specific rules
26
+ {
27
+ files: ["**/*.{ts,tsx}"],
28
+ languageOptions: {
29
+ ecmaVersion: "latest",
30
+ sourceType: "module",
31
+ parserOptions: {
32
+ ecmaFeatures: {
33
+ experimentalDecorators: true
34
+ }
35
+ },
36
+ globals: {
37
+ ...globals.node,
38
+ ...globals.browser,
39
+ Bun: "readonly"
40
+ }
41
+ },
42
+ rules: {
43
+ "no-unused-vars": "off",
44
+ "@typescript-eslint/no-unused-vars": [
45
+ "warn",
46
+ {
47
+ argsIgnorePattern: "^_",
48
+ varsIgnorePattern: "^_"
49
+ }
50
+ ],
51
+ "@typescript-eslint/no-explicit-any": "off",
52
+ "@typescript-eslint/no-empty-object-type": "off",
53
+ "@typescript-eslint/no-inferrable-types": "off",
54
+ "@typescript-eslint/no-require-imports": "off",
55
+ "no-console": "off"
56
+ }
57
+ },
58
+
59
+ // Prettier must be last: turns off stylistic rules that conflict with formatting
60
+ eslintConfigPrettier
61
+ );
@@ -1,4 +1,14 @@
1
+ /**
2
+ * Exception thrown when a storage operation fails.
3
+ */
1
4
  export default class StorageException extends Error {
5
+ /** The HTTP status code associated with the exception. */
2
6
  code: number;
7
+ /**
8
+ * Create a new storage exception.
9
+ *
10
+ * @param {string} message - The error message.
11
+ * @param {number} code - The HTTP status code, defaults to 503.
12
+ */
3
13
  constructor(message?: string, code?: number);
4
14
  }
@@ -1,7 +1,17 @@
1
1
  import Logger from "@bejibun/logger";
2
2
  import { defineValue } from "@bejibun/utils";
3
+ /**
4
+ * Exception thrown when a storage operation fails.
5
+ */
3
6
  export default class StorageException extends Error {
7
+ /** The HTTP status code associated with the exception. */
4
8
  code;
9
+ /**
10
+ * Create a new storage exception.
11
+ *
12
+ * @param {string} message - The error message.
13
+ * @param {number} code - The HTTP status code, defaults to 503.
14
+ */
5
15
  constructor(message, code) {
6
16
  super(message);
7
17
  this.name = "StorageException";
@@ -1 +1,4 @@
1
- export * from "../exceptions/StorageException";
1
+ /**
2
+ * Re-exports the storage exception classes.
3
+ */
4
+ export { default as StorageException } from "./StorageException";
@@ -1 +1,4 @@
1
- export * from "../exceptions/StorageException";
1
+ /**
2
+ * Re-exports the storage exception classes.
3
+ */
4
+ export { default as StorageException } from "./StorageException";
@@ -1,18 +1,105 @@
1
1
  import type { Stats } from "fs";
2
2
  import type { StorageDisk, StorageOptions } from "../types/storage";
3
3
  import StorageBuilder from "../builders/StorageBuilder";
4
+ /**
5
+ * Static facade for performing storage operations via the default builder.
6
+ */
4
7
  export default class Storage {
8
+ /**
9
+ * Build a storage builder with the given disk override.
10
+ *
11
+ * @param {StorageDisk} disk - The disk configuration to use.
12
+ * @returns {StorageBuilder} A storage builder bound to the disk.
13
+ */
5
14
  static build(disk: StorageDisk): StorageBuilder;
15
+ /**
16
+ * Select a disk by name and return a storage builder.
17
+ *
18
+ * @param {string} disk - The name of the disk to use.
19
+ * @returns {StorageBuilder} A storage builder bound to the disk name.
20
+ */
6
21
  static disk(disk: string): StorageBuilder;
22
+ /**
23
+ * Determine whether a file exists.
24
+ *
25
+ * @param {string} path - The path to the file.
26
+ * @returns {Promise<boolean>} Whether the file exists.
27
+ */
7
28
  static exists(path: string): Promise<boolean>;
29
+ /**
30
+ * Determine whether a file is missing.
31
+ *
32
+ * @param {string} path - The path to the file.
33
+ * @returns {Promise<boolean>} Whether the file does not exist.
34
+ */
8
35
  static missing(path: string): Promise<boolean>;
36
+ /**
37
+ * Retrieve metadata for a file.
38
+ *
39
+ * @param {string} path - The path to the file.
40
+ * @returns {Promise<Stats | Bun.S3Stats>} File metadata and statistics.
41
+ */
9
42
  static metadata(path: string): Promise<Stats | Bun.S3Stats>;
43
+ /**
44
+ * Get the file size in bytes.
45
+ *
46
+ * @param {string} path - The path to the file.
47
+ * @returns {Promise<number>} The file size in bytes.
48
+ */
10
49
  static size(path: string): Promise<number>;
50
+ /**
51
+ * Get the file MIME type.
52
+ *
53
+ * @param {string} path - The path to the file.
54
+ * @returns {Promise<string>} The detected MIME type.
55
+ */
11
56
  static mimeType(path: string): Promise<string>;
57
+ /**
58
+ * Get the file's last modification date.
59
+ *
60
+ * @param {string} path - The path to the file.
61
+ * @returns {Promise<Date>} The last modified timestamp.
62
+ */
12
63
  static lastModified(path: string): Promise<Date>;
64
+ /**
65
+ * Retrieve a file from storage.
66
+ *
67
+ * @param {string} path - The path to the file.
68
+ * @returns {Promise<Bun.BunFile | Bun.S3File>} The storage file instance.
69
+ */
13
70
  static get(path: string): Promise<Bun.BunFile | Bun.S3File>;
71
+ /**
72
+ * Store content at the given path.
73
+ *
74
+ * @param {string} path - The destination file path.
75
+ * @param {any} content - The content to store.
76
+ * @param {StorageOptions} options - Additional storage options.
77
+ * @returns {Promise<void>} A promise resolving once the file is stored.
78
+ */
14
79
  static put(path: string, content: any, options?: StorageOptions): Promise<void>;
80
+ /**
81
+ * Copy a file to a new location.
82
+ *
83
+ * @param {string} source - The source file path.
84
+ * @param {string} destination - The destination file path.
85
+ * @param {StorageOptions} options - Additional storage options.
86
+ * @returns {Promise<void>} A promise resolving once the file is copied.
87
+ */
15
88
  static copy(source: string, destination: string, options?: StorageOptions): Promise<void>;
89
+ /**
90
+ * Move a file to a new location.
91
+ *
92
+ * @param {string} source - The source file path.
93
+ * @param {string} destination - The destination file path.
94
+ * @param {StorageOptions} options - Additional storage options.
95
+ * @returns {Promise<void>} A promise resolving once the file is moved.
96
+ */
16
97
  static move(source: string, destination: string, options?: StorageOptions): Promise<void>;
98
+ /**
99
+ * Delete a file from storage.
100
+ *
101
+ * @param {string} path - The path to the file.
102
+ * @returns {Promise<void>} A promise resolving once the file is deleted.
103
+ */
17
104
  static delete(path: string): Promise<any>;
18
105
  }
@@ -1,41 +1,128 @@
1
1
  import StorageBuilder from "../builders/StorageBuilder";
2
+ /**
3
+ * Static facade for performing storage operations via the default builder.
4
+ */
2
5
  export default class Storage {
6
+ /**
7
+ * Build a storage builder with the given disk override.
8
+ *
9
+ * @param {StorageDisk} disk - The disk configuration to use.
10
+ * @returns {StorageBuilder} A storage builder bound to the disk.
11
+ */
3
12
  static build(disk) {
4
13
  return new StorageBuilder().build(disk);
5
14
  }
15
+ /**
16
+ * Select a disk by name and return a storage builder.
17
+ *
18
+ * @param {string} disk - The name of the disk to use.
19
+ * @returns {StorageBuilder} A storage builder bound to the disk name.
20
+ */
6
21
  static disk(disk) {
7
22
  return new StorageBuilder().disk(disk);
8
23
  }
24
+ /**
25
+ * Determine whether a file exists.
26
+ *
27
+ * @param {string} path - The path to the file.
28
+ * @returns {Promise<boolean>} Whether the file exists.
29
+ */
9
30
  static async exists(path) {
10
31
  return await new StorageBuilder().exists(path);
11
32
  }
33
+ /**
34
+ * Determine whether a file is missing.
35
+ *
36
+ * @param {string} path - The path to the file.
37
+ * @returns {Promise<boolean>} Whether the file does not exist.
38
+ */
12
39
  static async missing(path) {
13
40
  return await new StorageBuilder().missing(path);
14
41
  }
42
+ /**
43
+ * Retrieve metadata for a file.
44
+ *
45
+ * @param {string} path - The path to the file.
46
+ * @returns {Promise<Stats | Bun.S3Stats>} File metadata and statistics.
47
+ */
15
48
  static async metadata(path) {
16
49
  return await new StorageBuilder().metadata(path);
17
50
  }
51
+ /**
52
+ * Get the file size in bytes.
53
+ *
54
+ * @param {string} path - The path to the file.
55
+ * @returns {Promise<number>} The file size in bytes.
56
+ */
18
57
  static async size(path) {
19
58
  return await new StorageBuilder().size(path);
20
59
  }
60
+ /**
61
+ * Get the file MIME type.
62
+ *
63
+ * @param {string} path - The path to the file.
64
+ * @returns {Promise<string>} The detected MIME type.
65
+ */
21
66
  static async mimeType(path) {
22
67
  return await new StorageBuilder().mimeType(path);
23
68
  }
69
+ /**
70
+ * Get the file's last modification date.
71
+ *
72
+ * @param {string} path - The path to the file.
73
+ * @returns {Promise<Date>} The last modified timestamp.
74
+ */
24
75
  static async lastModified(path) {
25
76
  return await new StorageBuilder().lastModified(path);
26
77
  }
78
+ /**
79
+ * Retrieve a file from storage.
80
+ *
81
+ * @param {string} path - The path to the file.
82
+ * @returns {Promise<Bun.BunFile | Bun.S3File>} The storage file instance.
83
+ */
27
84
  static async get(path) {
28
85
  return await new StorageBuilder().get(path);
29
86
  }
87
+ /**
88
+ * Store content at the given path.
89
+ *
90
+ * @param {string} path - The destination file path.
91
+ * @param {any} content - The content to store.
92
+ * @param {StorageOptions} options - Additional storage options.
93
+ * @returns {Promise<void>} A promise resolving once the file is stored.
94
+ */
30
95
  static async put(path, content, options) {
31
- return await new StorageBuilder().put(path, content);
96
+ return await new StorageBuilder().put(path, content, options);
32
97
  }
98
+ /**
99
+ * Copy a file to a new location.
100
+ *
101
+ * @param {string} source - The source file path.
102
+ * @param {string} destination - The destination file path.
103
+ * @param {StorageOptions} options - Additional storage options.
104
+ * @returns {Promise<void>} A promise resolving once the file is copied.
105
+ */
33
106
  static async copy(source, destination, options) {
34
107
  return await new StorageBuilder().copy(source, destination, options);
35
108
  }
109
+ /**
110
+ * Move a file to a new location.
111
+ *
112
+ * @param {string} source - The source file path.
113
+ * @param {string} destination - The destination file path.
114
+ * @param {StorageOptions} options - Additional storage options.
115
+ * @returns {Promise<void>} A promise resolving once the file is moved.
116
+ */
36
117
  static async move(source, destination, options) {
37
118
  return await new StorageBuilder().move(source, destination, options);
38
119
  }
120
+ /**
121
+ * Delete a file from storage.
122
+ *
123
+ * @param {string} path - The path to the file.
124
+ * @returns {Promise<void>} A promise resolving once the file is deleted.
125
+ */
39
126
  static async delete(path) {
40
127
  return await new StorageBuilder().delete(path);
41
128
  }
@@ -1 +1,4 @@
1
- export * from "../facades/Storage";
1
+ /**
2
+ * Re-exports the Storage facade.
3
+ */
4
+ export { default as Storage } from "./Storage";
package/facades/index.js CHANGED
@@ -1 +1,4 @@
1
- export * from "../facades/Storage";
1
+ /**
2
+ * Re-exports the Storage facade.
3
+ */
4
+ export { default as Storage } from "./Storage";
package/index.d.ts CHANGED
@@ -1,3 +1,7 @@
1
+ /**
2
+ * Re-exports the Storage facade, storage enums, exception classes, and facade barrel as the package entry point.
3
+ */
1
4
  export { default } from "./facades/Storage";
2
5
  export * from "./enums/index";
6
+ export * from "./exceptions/index";
3
7
  export * from "./facades/index";
package/index.js CHANGED
@@ -1,3 +1,7 @@
1
+ /**
2
+ * Re-exports the Storage facade, storage enums, exception classes, and facade barrel as the package entry point.
3
+ */
1
4
  export { default } from "./facades/Storage";
2
5
  export * from "./enums/index";
6
+ export * from "./exceptions/index";
3
7
  export * from "./facades/index";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bejibun/storage",
3
- "version": "0.1.0",
3
+ "version": "0.1.11",
4
4
  "author": "Havea Crenata <havea.crenata@gmail.com>",
5
5
  "repository": {
6
6
  "type": "git",
@@ -8,14 +8,16 @@
8
8
  },
9
9
  "main": "index.js",
10
10
  "module": "index.js",
11
- "dependencies": {
12
- "@bejibun/app": "^0.1.24",
13
- "@bejibun/logger": "^0.1.22",
14
- "@bejibun/utils": "^0.1.28"
15
- },
16
11
  "devDependencies": {
17
- "@types/bun": "latest",
18
- "tsc-alias": "^1.9.1"
12
+ "@eslint/js": "^10.0.1",
13
+ "@types/bun": "^1.4.0",
14
+ "eslint": "^10.9.1",
15
+ "eslint-config-prettier": "^10.1.8",
16
+ "globals": "^17.12.0",
17
+ "prettier": "^3.9.6",
18
+ "tsc-alias": "^1.9.4",
19
+ "typescript": "^6.0.3",
20
+ "typescript-eslint": "^8.69.0"
19
21
  },
20
22
  "bugs": {
21
23
  "url": "https://github.com/Bejibun-Framework/bejibun-storage/issues"
@@ -31,13 +33,22 @@
31
33
  ],
32
34
  "license": "MIT",
33
35
  "scripts": {
34
- "alias": "bunx tsc-alias -p tsconfig.json",
36
+ "alias": "tsc-alias -p tsconfig.json",
35
37
  "types": "cp -rf src/types dist",
36
38
  "rsync": "rsync -a dist/ ./",
37
39
  "clean": "rm -rf dist",
38
- "build": "bunx tsc -p tsconfig.json && bun run types && bun run alias && bun run rsync && bun run clean",
40
+ "format": "prettier --write src --log-level=silent",
41
+ "eslint": "eslint src",
42
+ "lint": "bun run format && bun run eslint",
43
+ "test": "bun test tests/",
44
+ "build": "bun run lint && tsc -p tsconfig.json && bun run types && bun run alias && bun run rsync && bun run clean",
39
45
  "deploy": "bun run build && bun publish --access public"
40
46
  },
41
47
  "type": "module",
42
- "types": "index.d.ts"
43
- }
48
+ "types": "index.d.ts",
49
+ "dependencies": {
50
+ "@bejibun/app": "^0.1.26",
51
+ "@bejibun/logger": "^0.2.1",
52
+ "@bejibun/utils": "^0.1.30"
53
+ }
54
+ }
@@ -0,0 +1,145 @@
1
+ import {
2
+ afterAll,
3
+ afterEach,
4
+ beforeAll,
5
+ beforeEach,
6
+ describe,
7
+ expect,
8
+ mock,
9
+ test
10
+ } from "bun:test";
11
+ import StorageBuilder from "../../src/builders/StorageBuilder";
12
+ import Storage from "../../src/facades/Storage";
13
+
14
+ const rootDir = `${process.env.TMPDIR ?? "/tmp"}/bejibun-storage-int-${Date.now()}`;
15
+ const disk = {driver: "local", root: rootDir};
16
+
17
+ let writtenFiles: Array<string> = [];
18
+
19
+ const log = mock(console.log);
20
+ const error = mock(console.error);
21
+
22
+ beforeAll(async () => {
23
+ await Bun.write(`${rootDir}/.keep`, "");
24
+ });
25
+
26
+ beforeEach(() => {
27
+ log.mockReset();
28
+ error.mockReset();
29
+ console.log = log;
30
+ console.error = error;
31
+ writtenFiles = [];
32
+ });
33
+
34
+ afterEach(() => {
35
+ console.log = console.log;
36
+ console.error = console.error;
37
+ void cleanup();
38
+ });
39
+
40
+ async function cleanup() {
41
+ for (const file of writtenFiles) {
42
+ try {
43
+ if (await Bun.file(file).exists()) await Bun.file(file).delete();
44
+ } catch {
45
+ /* already removed by the test */
46
+ }
47
+ }
48
+ writtenFiles = [];
49
+ }
50
+
51
+ afterAll(async () => {
52
+ await Bun.file(`${rootDir}/.keep`).delete();
53
+ });
54
+
55
+ describe("StorageBuilder integration (local disk)", () => {
56
+ test("put stores content and exists/size read it back", async () => {
57
+ const builder = new StorageBuilder().build(disk);
58
+ const filepath = `${rootDir}/hello.txt`;
59
+
60
+ writtenFiles.push(filepath);
61
+
62
+ await builder.put(filepath, "hello world");
63
+
64
+ expect(await builder.exists(filepath)).toBe(true);
65
+ expect(await builder.missing(filepath)).toBe(false);
66
+ expect(await builder.size(filepath)).toBe(11);
67
+ expect(await (await builder.get(filepath)).text()).toBe("hello world");
68
+ });
69
+
70
+ test("metadata and lastModified are available after a write", async () => {
71
+ const builder = new StorageBuilder().build(disk);
72
+ const filepath = `${rootDir}/meta.txt`;
73
+
74
+ writtenFiles.push(filepath);
75
+
76
+ await builder.put(filepath, "meta");
77
+
78
+ const meta = await builder.metadata(filepath);
79
+
80
+ expect(meta.size).toBe(4);
81
+ expect(await builder.lastModified(filepath)).toBeInstanceOf(Date);
82
+ expect(builder.mimeType(filepath)).toBeTruthy();
83
+ });
84
+
85
+ test("copy duplicates the file", async () => {
86
+ const builder = new StorageBuilder().build(disk);
87
+ const source = `${rootDir}/copy-src.txt`;
88
+ const destination = `${rootDir}/copy-dst.txt`;
89
+
90
+ writtenFiles.push(source, destination);
91
+
92
+ await builder.put(source, "copy me");
93
+ await builder.copy(source, destination);
94
+
95
+ expect(await builder.exists(destination)).toBe(true);
96
+ expect(await (await builder.get(destination)).text()).toBe("copy me");
97
+ });
98
+
99
+ test("move relocates the file and removes the source", async () => {
100
+ const builder = new StorageBuilder().build(disk);
101
+ const source = `${rootDir}/move-src.txt`;
102
+ const destination = `${rootDir}/move-dst.txt`;
103
+
104
+ writtenFiles.push(source, destination);
105
+
106
+ await builder.put(source, "move me");
107
+ await builder.move(source, destination);
108
+
109
+ expect(await builder.exists(source)).toBe(false);
110
+ expect(await builder.exists(destination)).toBe(true);
111
+ expect(await (await builder.get(destination)).text()).toBe("move me");
112
+ });
113
+
114
+ test("delete removes the file", async () => {
115
+ const builder = new StorageBuilder().build(disk);
116
+ const filepath = `${rootDir}/delete.txt`;
117
+
118
+ writtenFiles.push(filepath);
119
+
120
+ await builder.put(filepath, "gone");
121
+ await builder.delete(filepath);
122
+
123
+ expect(await builder.exists(filepath)).toBe(false);
124
+ expect(await builder.missing(filepath)).toBe(true);
125
+ });
126
+ });
127
+
128
+ describe("Storage facade integration (local disk)", () => {
129
+ test("disk + build resolve the same local disk", async () => {
130
+ const builder = Storage.build(disk).disk("local");
131
+
132
+ expect(builder).toBeInstanceOf(StorageBuilder);
133
+ });
134
+
135
+ test("put via facade stores and get reads back relative to the disk root", async () => {
136
+ const builder = new StorageBuilder().build(disk);
137
+ const filepath = `${rootDir}/facade-put.txt`;
138
+
139
+ writtenFiles.push(filepath);
140
+
141
+ await builder.put(filepath, "facade");
142
+
143
+ expect(await builder.exists(filepath)).toBe(true);
144
+ });
145
+ });