@rebasepro/server-core 0.0.1-canary.09e5ec5
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.
- package/LICENSE +6 -0
- package/README.md +40 -0
- package/build-errors.txt +52 -0
- package/coverage/clover.xml +3739 -0
- package/coverage/coverage-final.json +31 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +266 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +210 -0
- package/coverage/lcov-report/src/api/ast-schema-editor.ts.html +952 -0
- package/coverage/lcov-report/src/api/errors.ts.html +472 -0
- package/coverage/lcov-report/src/api/graphql/graphql-schema-generator.ts.html +1069 -0
- package/coverage/lcov-report/src/api/graphql/index.html +116 -0
- package/coverage/lcov-report/src/api/index.html +176 -0
- package/coverage/lcov-report/src/api/openapi-generator.ts.html +565 -0
- package/coverage/lcov-report/src/api/rest/api-generator.ts.html +994 -0
- package/coverage/lcov-report/src/api/rest/index.html +131 -0
- package/coverage/lcov-report/src/api/rest/query-parser.ts.html +550 -0
- package/coverage/lcov-report/src/api/schema-editor-routes.ts.html +202 -0
- package/coverage/lcov-report/src/api/server.ts.html +823 -0
- package/coverage/lcov-report/src/auth/admin-routes.ts.html +973 -0
- package/coverage/lcov-report/src/auth/index.html +176 -0
- package/coverage/lcov-report/src/auth/jwt.ts.html +574 -0
- package/coverage/lcov-report/src/auth/middleware.ts.html +745 -0
- package/coverage/lcov-report/src/auth/password.ts.html +310 -0
- package/coverage/lcov-report/src/auth/services.ts.html +2074 -0
- package/coverage/lcov-report/src/collections/index.html +116 -0
- package/coverage/lcov-report/src/collections/loader.ts.html +232 -0
- package/coverage/lcov-report/src/db/auth-schema.ts.html +523 -0
- package/coverage/lcov-report/src/db/data-transformer.ts.html +1753 -0
- package/coverage/lcov-report/src/db/entityService.ts.html +700 -0
- package/coverage/lcov-report/src/db/index.html +146 -0
- package/coverage/lcov-report/src/db/services/EntityFetchService.ts.html +4048 -0
- package/coverage/lcov-report/src/db/services/EntityPersistService.ts.html +883 -0
- package/coverage/lcov-report/src/db/services/RelationService.ts.html +3121 -0
- package/coverage/lcov-report/src/db/services/entity-helpers.ts.html +442 -0
- package/coverage/lcov-report/src/db/services/index.html +176 -0
- package/coverage/lcov-report/src/db/services/index.ts.html +124 -0
- package/coverage/lcov-report/src/generate-drizzle-schema-logic.ts.html +1960 -0
- package/coverage/lcov-report/src/index.html +116 -0
- package/coverage/lcov-report/src/services/driver-registry.ts.html +631 -0
- package/coverage/lcov-report/src/services/index.html +131 -0
- package/coverage/lcov-report/src/services/postgresDataDriver.ts.html +3025 -0
- package/coverage/lcov-report/src/storage/LocalStorageController.ts.html +1189 -0
- package/coverage/lcov-report/src/storage/S3StorageController.ts.html +970 -0
- package/coverage/lcov-report/src/storage/index.html +161 -0
- package/coverage/lcov-report/src/storage/storage-registry.ts.html +646 -0
- package/coverage/lcov-report/src/storage/types.ts.html +451 -0
- package/coverage/lcov-report/src/utils/drizzle-conditions.ts.html +3082 -0
- package/coverage/lcov-report/src/utils/index.html +116 -0
- package/coverage/lcov.info +7179 -0
- package/dist/common/src/collections/CollectionRegistry.d.ts +56 -0
- package/dist/common/src/collections/index.d.ts +1 -0
- package/dist/common/src/data/buildRebaseData.d.ts +14 -0
- package/dist/common/src/index.d.ts +3 -0
- package/dist/common/src/util/builders.d.ts +57 -0
- package/dist/common/src/util/callbacks.d.ts +6 -0
- package/dist/common/src/util/collections.d.ts +11 -0
- package/dist/common/src/util/common.d.ts +2 -0
- package/dist/common/src/util/conditions.d.ts +26 -0
- package/dist/common/src/util/entities.d.ts +58 -0
- package/dist/common/src/util/enums.d.ts +3 -0
- package/dist/common/src/util/index.d.ts +16 -0
- package/dist/common/src/util/navigation_from_path.d.ts +34 -0
- package/dist/common/src/util/navigation_utils.d.ts +20 -0
- package/dist/common/src/util/parent_references_from_path.d.ts +6 -0
- package/dist/common/src/util/paths.d.ts +14 -0
- package/dist/common/src/util/permissions.d.ts +5 -0
- package/dist/common/src/util/references.d.ts +2 -0
- package/dist/common/src/util/relations.d.ts +22 -0
- package/dist/common/src/util/resolutions.d.ts +72 -0
- package/dist/common/src/util/storage.d.ts +24 -0
- package/dist/index-DXVBFp5V.js +37 -0
- package/dist/index-DXVBFp5V.js.map +1 -0
- package/dist/index.es.js +49934 -0
- package/dist/index.es.js.map +1 -0
- package/dist/index.umd.js +49968 -0
- package/dist/index.umd.js.map +1 -0
- package/dist/server-core/src/api/ast-schema-editor.d.ts +21 -0
- package/dist/server-core/src/api/collections_for_test/callbacks_test_collection.d.ts +2 -0
- package/dist/server-core/src/api/errors.d.ts +35 -0
- package/dist/server-core/src/api/graphql/graphql-schema-generator.d.ts +35 -0
- package/dist/server-core/src/api/graphql/index.d.ts +1 -0
- package/dist/server-core/src/api/index.d.ts +9 -0
- package/dist/server-core/src/api/openapi-generator.d.ts +16 -0
- package/dist/server-core/src/api/rest/api-generator.d.ts +64 -0
- package/dist/server-core/src/api/rest/index.d.ts +1 -0
- package/dist/server-core/src/api/rest/query-parser.d.ts +9 -0
- package/dist/server-core/src/api/schema-editor-routes.d.ts +3 -0
- package/dist/server-core/src/api/server.d.ts +40 -0
- package/dist/server-core/src/api/types.d.ts +90 -0
- package/dist/server-core/src/auth/admin-routes.d.ts +16 -0
- package/dist/server-core/src/auth/apple-oauth.d.ts +30 -0
- package/dist/server-core/src/auth/bitbucket-oauth.d.ts +11 -0
- package/dist/server-core/src/auth/discord-oauth.d.ts +14 -0
- package/dist/server-core/src/auth/facebook-oauth.d.ts +14 -0
- package/dist/server-core/src/auth/github-oauth.d.ts +15 -0
- package/dist/server-core/src/auth/gitlab-oauth.d.ts +13 -0
- package/dist/server-core/src/auth/google-oauth.d.ts +14 -0
- package/dist/server-core/src/auth/index.d.ts +23 -0
- package/dist/server-core/src/auth/interfaces.d.ts +309 -0
- package/dist/server-core/src/auth/jwt.d.ts +43 -0
- package/dist/server-core/src/auth/linkedin-oauth.d.ts +18 -0
- package/dist/server-core/src/auth/microsoft-oauth.d.ts +16 -0
- package/dist/server-core/src/auth/middleware.d.ts +81 -0
- package/dist/server-core/src/auth/password.d.ts +22 -0
- package/dist/server-core/src/auth/rate-limiter.d.ts +31 -0
- package/dist/server-core/src/auth/routes.d.ts +27 -0
- package/dist/server-core/src/auth/slack-oauth.d.ts +12 -0
- package/dist/server-core/src/auth/spotify-oauth.d.ts +12 -0
- package/dist/server-core/src/auth/twitter-oauth.d.ts +18 -0
- package/dist/server-core/src/bootstrappers/index.d.ts +0 -0
- package/dist/server-core/src/collections/BackendCollectionRegistry.d.ts +13 -0
- package/dist/server-core/src/collections/loader.d.ts +5 -0
- package/dist/server-core/src/cron/cron-loader.d.ts +17 -0
- package/dist/server-core/src/cron/cron-routes.d.ts +14 -0
- package/dist/server-core/src/cron/cron-scheduler.d.ts +61 -0
- package/dist/server-core/src/cron/cron-store.d.ts +32 -0
- package/dist/server-core/src/cron/index.d.ts +6 -0
- package/dist/server-core/src/db/interfaces.d.ts +18 -0
- package/dist/server-core/src/email/index.d.ts +6 -0
- package/dist/server-core/src/email/smtp-email-service.d.ts +25 -0
- package/dist/server-core/src/email/templates.d.ts +42 -0
- package/dist/server-core/src/email/types.d.ts +107 -0
- package/dist/server-core/src/functions/function-loader.d.ts +17 -0
- package/dist/server-core/src/functions/function-routes.d.ts +10 -0
- package/dist/server-core/src/functions/index.d.ts +3 -0
- package/dist/server-core/src/history/history-routes.d.ts +23 -0
- package/dist/server-core/src/history/index.d.ts +1 -0
- package/dist/server-core/src/index.d.ts +29 -0
- package/dist/server-core/src/init.d.ts +159 -0
- package/dist/server-core/src/serve-spa.d.ts +30 -0
- package/dist/server-core/src/services/driver-registry.d.ts +78 -0
- package/dist/server-core/src/singleton.d.ts +35 -0
- package/dist/server-core/src/storage/LocalStorageController.d.ts +46 -0
- package/dist/server-core/src/storage/S3StorageController.d.ts +36 -0
- package/dist/server-core/src/storage/index.d.ts +25 -0
- package/dist/server-core/src/storage/routes.d.ts +38 -0
- package/dist/server-core/src/storage/storage-registry.d.ts +78 -0
- package/dist/server-core/src/storage/types.d.ts +103 -0
- package/dist/server-core/src/types/index.d.ts +11 -0
- package/dist/server-core/src/utils/dev-port.d.ts +35 -0
- package/dist/server-core/src/utils/logger.d.ts +31 -0
- package/dist/server-core/src/utils/logging.d.ts +9 -0
- package/dist/server-core/src/utils/request-logger.d.ts +19 -0
- package/dist/server-core/src/utils/sql.d.ts +27 -0
- package/dist/types/src/controllers/analytics_controller.d.ts +7 -0
- package/dist/types/src/controllers/auth.d.ts +119 -0
- package/dist/types/src/controllers/client.d.ts +170 -0
- package/dist/types/src/controllers/collection_registry.d.ts +45 -0
- package/dist/types/src/controllers/customization_controller.d.ts +60 -0
- package/dist/types/src/controllers/data.d.ts +168 -0
- package/dist/types/src/controllers/data_driver.d.ts +160 -0
- package/dist/types/src/controllers/database_admin.d.ts +11 -0
- package/dist/types/src/controllers/dialogs_controller.d.ts +36 -0
- package/dist/types/src/controllers/effective_role.d.ts +4 -0
- package/dist/types/src/controllers/email.d.ts +34 -0
- package/dist/types/src/controllers/index.d.ts +18 -0
- package/dist/types/src/controllers/local_config_persistence.d.ts +20 -0
- package/dist/types/src/controllers/navigation.d.ts +213 -0
- package/dist/types/src/controllers/registry.d.ts +54 -0
- package/dist/types/src/controllers/side_dialogs_controller.d.ts +67 -0
- package/dist/types/src/controllers/side_entity_controller.d.ts +90 -0
- package/dist/types/src/controllers/snackbar.d.ts +24 -0
- package/dist/types/src/controllers/storage.d.ts +171 -0
- package/dist/types/src/index.d.ts +4 -0
- package/dist/types/src/rebase_context.d.ts +105 -0
- package/dist/types/src/types/backend.d.ts +536 -0
- package/dist/types/src/types/builders.d.ts +15 -0
- package/dist/types/src/types/chips.d.ts +5 -0
- package/dist/types/src/types/collections.d.ts +856 -0
- package/dist/types/src/types/cron.d.ts +102 -0
- package/dist/types/src/types/data_source.d.ts +64 -0
- package/dist/types/src/types/entities.d.ts +145 -0
- package/dist/types/src/types/entity_actions.d.ts +98 -0
- package/dist/types/src/types/entity_callbacks.d.ts +173 -0
- package/dist/types/src/types/entity_link_builder.d.ts +7 -0
- package/dist/types/src/types/entity_overrides.d.ts +10 -0
- package/dist/types/src/types/entity_views.d.ts +61 -0
- package/dist/types/src/types/export_import.d.ts +21 -0
- package/dist/types/src/types/index.d.ts +23 -0
- package/dist/types/src/types/locales.d.ts +4 -0
- package/dist/types/src/types/modify_collections.d.ts +5 -0
- package/dist/types/src/types/plugins.d.ts +279 -0
- package/dist/types/src/types/properties.d.ts +1176 -0
- package/dist/types/src/types/property_config.d.ts +70 -0
- package/dist/types/src/types/relations.d.ts +336 -0
- package/dist/types/src/types/slots.d.ts +252 -0
- package/dist/types/src/types/translations.d.ts +870 -0
- package/dist/types/src/types/user_management_delegate.d.ts +121 -0
- package/dist/types/src/types/websockets.d.ts +78 -0
- package/dist/types/src/users/index.d.ts +2 -0
- package/dist/types/src/users/roles.d.ts +22 -0
- package/dist/types/src/users/user.d.ts +46 -0
- package/history_diff.log +385 -0
- package/jest.config.cjs +16 -0
- package/package.json +86 -0
- package/scratch.ts +9 -0
- package/src/api/ast-schema-editor.ts +289 -0
- package/src/api/collections_for_test/callbacks_test_collection.ts +60 -0
- package/src/api/errors.ts +179 -0
- package/src/api/graphql/graphql-schema-generator.ts +336 -0
- package/src/api/graphql/index.ts +2 -0
- package/src/api/index.ts +11 -0
- package/src/api/openapi-generator.ts +715 -0
- package/src/api/rest/api-generator.ts +472 -0
- package/src/api/rest/index.ts +2 -0
- package/src/api/rest/query-parser.ts +155 -0
- package/src/api/schema-editor-routes.ts +41 -0
- package/src/api/server.ts +248 -0
- package/src/api/types.ts +90 -0
- package/src/auth/admin-routes.ts +529 -0
- package/src/auth/apple-oauth.ts +130 -0
- package/src/auth/bitbucket-oauth.ts +82 -0
- package/src/auth/discord-oauth.ts +83 -0
- package/src/auth/facebook-oauth.ts +72 -0
- package/src/auth/github-oauth.ts +110 -0
- package/src/auth/gitlab-oauth.ts +70 -0
- package/src/auth/google-oauth.ts +48 -0
- package/src/auth/index.ts +34 -0
- package/src/auth/interfaces.ts +363 -0
- package/src/auth/jwt.ts +181 -0
- package/src/auth/linkedin-oauth.ts +81 -0
- package/src/auth/microsoft-oauth.ts +88 -0
- package/src/auth/middleware.ts +384 -0
- package/src/auth/password.ts +77 -0
- package/src/auth/rate-limiter.ts +129 -0
- package/src/auth/routes.ts +788 -0
- package/src/auth/slack-oauth.ts +71 -0
- package/src/auth/spotify-oauth.ts +67 -0
- package/src/auth/twitter-oauth.ts +120 -0
- package/src/bootstrappers/index.ts +1 -0
- package/src/collections/BackendCollectionRegistry.ts +20 -0
- package/src/collections/loader.ts +49 -0
- package/src/cron/cron-loader.ts +89 -0
- package/src/cron/cron-routes.test.ts +265 -0
- package/src/cron/cron-routes.ts +85 -0
- package/src/cron/cron-scheduler.test.ts +421 -0
- package/src/cron/cron-scheduler.ts +413 -0
- package/src/cron/cron-store.ts +163 -0
- package/src/cron/index.ts +6 -0
- package/src/db/interfaces.ts +60 -0
- package/src/email/index.ts +18 -0
- package/src/email/smtp-email-service.ts +91 -0
- package/src/email/templates.ts +388 -0
- package/src/email/types.ts +105 -0
- package/src/functions/function-loader.ts +119 -0
- package/src/functions/function-routes.ts +31 -0
- package/src/functions/index.ts +3 -0
- package/src/history/history-routes.ts +129 -0
- package/src/history/index.ts +2 -0
- package/src/index.ts +66 -0
- package/src/init.ts +727 -0
- package/src/serve-spa.ts +81 -0
- package/src/services/driver-registry.ts +182 -0
- package/src/singleton.test.ts +28 -0
- package/src/singleton.ts +70 -0
- package/src/storage/LocalStorageController.ts +365 -0
- package/src/storage/S3StorageController.ts +298 -0
- package/src/storage/index.ts +43 -0
- package/src/storage/routes.ts +264 -0
- package/src/storage/storage-registry.ts +187 -0
- package/src/storage/types.ts +134 -0
- package/src/types/index.ts +27 -0
- package/src/utils/dev-port.ts +176 -0
- package/src/utils/logger.ts +143 -0
- package/src/utils/logging.ts +38 -0
- package/src/utils/request-logger.ts +66 -0
- package/src/utils/sql.ts +38 -0
- package/test/admin-routes.test.ts +640 -0
- package/test/api-generator.test.ts +501 -0
- package/test/ast-schema-editor.test.ts +63 -0
- package/test/auth-middleware-hono.test.ts +556 -0
- package/test/auth-routes.test.ts +1047 -0
- package/test/driver-registry.test.ts +282 -0
- package/test/error-propagation.test.ts +226 -0
- package/test/errors-hono.test.ts +133 -0
- package/test/errors.test.ts +155 -0
- package/test/jwt-security.test.ts +182 -0
- package/test/jwt.test.ts +324 -0
- package/test/middleware.test.ts +300 -0
- package/test/password.test.ts +165 -0
- package/test/query-parser.test.ts +263 -0
- package/test/rate-limiter.test.ts +102 -0
- package/test/safe-compare.test.ts +66 -0
- package/test/singleton.test.ts +59 -0
- package/test/storage-local.test.ts +271 -0
- package/test/storage-registry.test.ts +282 -0
- package/test/storage-routes.test.ts +222 -0
- package/test/storage-s3.test.ts +304 -0
- package/test-ast.ts +28 -0
- package/test.ts +6 -0
- package/test_output.txt +1133 -0
- package/tsconfig.json +49 -0
- package/tsconfig.prod.json +20 -0
- package/vite.config.ts +80 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, jest as vi } from "@jest/globals";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import * as os from "os";
|
|
5
|
+
import { LocalStorageController } from "../src/storage/LocalStorageController";
|
|
6
|
+
|
|
7
|
+
describe("LocalStorageController", () => {
|
|
8
|
+
let controller: LocalStorageController;
|
|
9
|
+
let tempDir: string;
|
|
10
|
+
|
|
11
|
+
beforeEach(async () => {
|
|
12
|
+
// Create a temporary directory for tests
|
|
13
|
+
tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "rebase-storage-test-"));
|
|
14
|
+
controller = new LocalStorageController({
|
|
15
|
+
basePath: tempDir
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(async () => {
|
|
20
|
+
// Clean up temporary directory
|
|
21
|
+
await fs.promises.rm(tempDir, { recursive: true,
|
|
22
|
+
force: true });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe("constructor", () => {
|
|
26
|
+
it("should initialize with basePath", () => {
|
|
27
|
+
expect(controller.getBasePath()).toBe(tempDir);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("should return 'local' as type", () => {
|
|
31
|
+
expect(controller.getType()).toBe("local");
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe("putObject", () => {
|
|
36
|
+
it("should upload a file and return metadata", async () => {
|
|
37
|
+
const content = Buffer.from("Hello, World!");
|
|
38
|
+
const file = new File([content], "test.txt", { type: "text/plain" });
|
|
39
|
+
|
|
40
|
+
const result = await controller.putObject({
|
|
41
|
+
file,
|
|
42
|
+
key: "uploads/test.txt"
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
expect(result.key).toContain("uploads");
|
|
46
|
+
expect(result.key).toContain("test.txt");
|
|
47
|
+
|
|
48
|
+
// Verify file exists on disk (uses default bucket)
|
|
49
|
+
const filePath = path.join(tempDir, "default", "uploads", "test.txt");
|
|
50
|
+
const exists = await fs.promises.access(filePath).then(() => true).catch(() => false);
|
|
51
|
+
expect(exists).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("should create nested directories", async () => {
|
|
55
|
+
const content = Buffer.from("Nested content");
|
|
56
|
+
const file = new File([content], "nested.txt", { type: "text/plain" });
|
|
57
|
+
|
|
58
|
+
await controller.putObject({
|
|
59
|
+
file,
|
|
60
|
+
key: "level1/level2/level3/nested.txt"
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const filePath = path.join(tempDir, "default", "level1", "level2", "level3", "nested.txt");
|
|
64
|
+
const exists = await fs.promises.access(filePath).then(() => true).catch(() => false);
|
|
65
|
+
expect(exists).toBe(true);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("should handle custom bucket", async () => {
|
|
69
|
+
const content = Buffer.from("Bucket content");
|
|
70
|
+
const file = new File([content], "bucket.txt", { type: "text/plain" });
|
|
71
|
+
|
|
72
|
+
await controller.putObject({
|
|
73
|
+
file,
|
|
74
|
+
key: "files/bucket.txt",
|
|
75
|
+
bucket: "custom-bucket"
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const filePath = path.join(tempDir, "custom-bucket", "files", "bucket.txt");
|
|
79
|
+
const exists = await fs.promises.access(filePath).then(() => true).catch(() => false);
|
|
80
|
+
expect(exists).toBe(true);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("should store metadata alongside file", async () => {
|
|
84
|
+
const content = Buffer.from("With metadata");
|
|
85
|
+
const file = new File([content], "meta.txt", { type: "text/plain" });
|
|
86
|
+
|
|
87
|
+
await controller.putObject({
|
|
88
|
+
file,
|
|
89
|
+
key: "uploads/meta.txt",
|
|
90
|
+
metadata: { customField: "customValue" }
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Implementation uses .metadata.json extension
|
|
94
|
+
const metadataPath = path.join(tempDir, "default", "uploads", "meta.txt.metadata.json");
|
|
95
|
+
const exists = await fs.promises.access(metadataPath).then(() => true).catch(() => false);
|
|
96
|
+
expect(exists).toBe(true);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe("getObject", () => {
|
|
101
|
+
it("should retrieve an uploaded file using local:// URL format", async () => {
|
|
102
|
+
const content = Buffer.from("Retrieve me");
|
|
103
|
+
const file = new File([content], "retrieve.txt", { type: "text/plain" });
|
|
104
|
+
|
|
105
|
+
const uploadResult = await controller.putObject({
|
|
106
|
+
file,
|
|
107
|
+
key: "uploads/retrieve.txt"
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Use the storageUrl from upload result (local:// format)
|
|
111
|
+
const retrieved = await controller.getObject(uploadResult.storageUrl!);
|
|
112
|
+
|
|
113
|
+
expect(retrieved).not.toBeNull();
|
|
114
|
+
expect(retrieved?.name).toBe("retrieve.txt");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("should return null for non-existent file", async () => {
|
|
118
|
+
const result = await controller.getObject("local://default/nonexistent/file.txt");
|
|
119
|
+
expect(result).toBeNull();
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe("deleteObject", () => {
|
|
124
|
+
it("should delete an uploaded file using local:// URL format", async () => {
|
|
125
|
+
const content = Buffer.from("Delete me");
|
|
126
|
+
const file = new File([content], "delete.txt", { type: "text/plain" });
|
|
127
|
+
|
|
128
|
+
const uploadResult = await controller.putObject({
|
|
129
|
+
file,
|
|
130
|
+
key: "uploads/delete.txt"
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// Verify file exists
|
|
134
|
+
const filePath = path.join(tempDir, "default", "uploads", "delete.txt");
|
|
135
|
+
let exists = await fs.promises.access(filePath).then(() => true).catch(() => false);
|
|
136
|
+
expect(exists).toBe(true);
|
|
137
|
+
|
|
138
|
+
// Delete the file using local:// URL format
|
|
139
|
+
await controller.deleteObject(uploadResult.storageUrl!);
|
|
140
|
+
|
|
141
|
+
// Verify file no longer exists
|
|
142
|
+
exists = await fs.promises.access(filePath).then(() => true).catch(() => false);
|
|
143
|
+
expect(exists).toBe(false);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("should not throw when deleting non-existent file", async () => {
|
|
147
|
+
await expect(controller.deleteObject("local://default/nonexistent/file.txt")).resolves.not.toThrow();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("should also delete metadata file", async () => {
|
|
151
|
+
const content = Buffer.from("Delete with metadata");
|
|
152
|
+
const file = new File([content], "withmeta.txt", { type: "text/plain" });
|
|
153
|
+
|
|
154
|
+
const uploadResult = await controller.putObject({
|
|
155
|
+
file,
|
|
156
|
+
key: "uploads/withmeta.txt",
|
|
157
|
+
metadata: { key: "value" }
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
await controller.deleteObject(uploadResult.storageUrl!);
|
|
161
|
+
|
|
162
|
+
const metadataPath = path.join(tempDir, "default", "uploads", "withmeta.txt.metadata.json");
|
|
163
|
+
const exists = await fs.promises.access(metadataPath).then(() => true).catch(() => false);
|
|
164
|
+
expect(exists).toBe(false);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe("list", () => {
|
|
169
|
+
beforeEach(async () => {
|
|
170
|
+
// Upload some test files
|
|
171
|
+
for (let i = 1; i <= 5; i++) {
|
|
172
|
+
const file = new File([`Content ${i}`], `file${i}.txt`, { type: "text/plain" });
|
|
173
|
+
await controller.putObject({
|
|
174
|
+
file,
|
|
175
|
+
key: `listtest/file${i}.txt`
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("should list files in a directory", async () => {
|
|
181
|
+
const result = await controller.listObjects("listtest", { bucket: "default" });
|
|
182
|
+
|
|
183
|
+
// Items should be the actual files (not metadata files)
|
|
184
|
+
expect(result.items.length).toBeGreaterThanOrEqual(5);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("should return empty list for non-existent directory", async () => {
|
|
188
|
+
const result = await controller.listObjects("nonexistent", { bucket: "default" });
|
|
189
|
+
|
|
190
|
+
expect(result.items).toHaveLength(0);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
describe("getSignedUrl", () => {
|
|
195
|
+
it("should return download URL for existing file", async () => {
|
|
196
|
+
const content = Buffer.from("Download me");
|
|
197
|
+
const file = new File([content], "download.txt", { type: "text/plain" });
|
|
198
|
+
|
|
199
|
+
const uploadResult = await controller.putObject({
|
|
200
|
+
file,
|
|
201
|
+
key: "uploads/download.txt"
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// Use the storageUrl from upload result
|
|
205
|
+
const result = await controller.getSignedUrl(uploadResult.storageUrl!);
|
|
206
|
+
|
|
207
|
+
expect(result.url).toBeTruthy();
|
|
208
|
+
expect(result.fileNotFound).toBeFalsy();
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("should return fileNotFound for non-existent file", async () => {
|
|
212
|
+
const result = await controller.getSignedUrl("local://default/nonexistent/file.txt");
|
|
213
|
+
|
|
214
|
+
expect(result.fileNotFound).toBe(true);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
describe("getAbsolutePath", () => {
|
|
219
|
+
it("should return absolute filesystem path without bucket", () => {
|
|
220
|
+
const absPath = controller.getAbsolutePath("uploads/test.txt");
|
|
221
|
+
|
|
222
|
+
// getAbsolutePath uses getFullPath which doesn't include bucket unless specified
|
|
223
|
+
expect(absPath).toBe(path.join(tempDir, "uploads", "test.txt"));
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("should handle custom bucket", () => {
|
|
227
|
+
const absPath = controller.getAbsolutePath("uploads/test.txt", "custom");
|
|
228
|
+
|
|
229
|
+
expect(absPath).toBe(path.join(tempDir, "custom", "uploads", "test.txt"));
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
describe("validateFile", () => {
|
|
234
|
+
it("should accept valid file", () => {
|
|
235
|
+
const file = new File(["content"], "valid.txt", { type: "text/plain" });
|
|
236
|
+
|
|
237
|
+
// Should not throw
|
|
238
|
+
expect(() => {
|
|
239
|
+
(controller as {validateFile: (f: File) => void}).validateFile(file);
|
|
240
|
+
}).not.toThrow();
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("should reject file exceeding max size", () => {
|
|
244
|
+
// Create a controller with small max size
|
|
245
|
+
const smallController = new LocalStorageController({
|
|
246
|
+
basePath: tempDir,
|
|
247
|
+
maxFileSize: 10 // 10 bytes
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const largeContent = Buffer.alloc(100);
|
|
251
|
+
const file = new File([largeContent], "large.txt", { type: "text/plain" });
|
|
252
|
+
|
|
253
|
+
expect(() => {
|
|
254
|
+
(smallController as {validateFile: (f: File) => void}).validateFile(file);
|
|
255
|
+
}).toThrow(/exceeds/i);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it("should reject disallowed file types", () => {
|
|
259
|
+
const controllerWithTypes = new LocalStorageController({
|
|
260
|
+
basePath: tempDir,
|
|
261
|
+
allowedMimeTypes: ["image/png", "image/jpeg"]
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
const file = new File(["content"], "script.js", { type: "application/javascript" });
|
|
265
|
+
|
|
266
|
+
expect(() => {
|
|
267
|
+
(controllerWithTypes as {validateFile: (f: File) => void}).validateFile(file);
|
|
268
|
+
}).toThrow(/not allowed/i);
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
});
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, jest as vi } from "@jest/globals";
|
|
2
|
+
import {
|
|
3
|
+
StorageRegistry,
|
|
4
|
+
DefaultStorageRegistry,
|
|
5
|
+
DEFAULT_STORAGE_ID
|
|
6
|
+
} from "../src/storage/storage-registry";
|
|
7
|
+
import { StorageController } from "../src/storage/types";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Mock StorageController for testing
|
|
11
|
+
*/
|
|
12
|
+
function createMockStorageController(type: "local" | "s3"): StorageController {
|
|
13
|
+
return {
|
|
14
|
+
putObject: vi.fn().mockResolvedValue({ path: "test/file.txt" }),
|
|
15
|
+
getSignedUrl: vi.fn().mockResolvedValue({ url: "http://example.com/file.txt" }),
|
|
16
|
+
getObject: vi.fn().mockResolvedValue(null),
|
|
17
|
+
deleteObject: vi.fn().mockResolvedValue(undefined),
|
|
18
|
+
listObjects: vi.fn().mockResolvedValue({ items: [],
|
|
19
|
+
prefixes: [] }),
|
|
20
|
+
getType: vi.fn().mockReturnValue(type)
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe("StorageRegistry", () => {
|
|
25
|
+
describe("DEFAULT_STORAGE_ID", () => {
|
|
26
|
+
it("should be '(default)'", () => {
|
|
27
|
+
expect(DEFAULT_STORAGE_ID).toBe("(default)");
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe("DefaultStorageRegistry", () => {
|
|
32
|
+
describe("constructor and basic operations", () => {
|
|
33
|
+
it("should create an empty registry", () => {
|
|
34
|
+
const registry = new DefaultStorageRegistry();
|
|
35
|
+
expect(registry.size()).toBe(0);
|
|
36
|
+
expect(registry.list()).toEqual([]);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("should register a storage controller", () => {
|
|
40
|
+
const registry = new DefaultStorageRegistry();
|
|
41
|
+
const mockController = createMockStorageController("local");
|
|
42
|
+
|
|
43
|
+
registry.register("test-storage", mockController);
|
|
44
|
+
|
|
45
|
+
expect(registry.has("test-storage")).toBe(true);
|
|
46
|
+
expect(registry.size()).toBe(1);
|
|
47
|
+
expect(registry.list()).toContain("test-storage");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("should get a registered storage controller", () => {
|
|
51
|
+
const registry = new DefaultStorageRegistry();
|
|
52
|
+
const mockController = createMockStorageController("s3");
|
|
53
|
+
|
|
54
|
+
registry.register("my-storage", mockController);
|
|
55
|
+
|
|
56
|
+
const retrieved = registry.get("my-storage");
|
|
57
|
+
expect(retrieved).toBe(mockController);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("should return undefined for non-existent storage", () => {
|
|
61
|
+
const registry = new DefaultStorageRegistry();
|
|
62
|
+
expect(registry.get("non-existent")).toBeUndefined();
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("default storage handling", () => {
|
|
67
|
+
it("should get default storage with get(undefined)", () => {
|
|
68
|
+
const registry = new DefaultStorageRegistry();
|
|
69
|
+
const mockController = createMockStorageController("local");
|
|
70
|
+
|
|
71
|
+
registry.register(DEFAULT_STORAGE_ID, mockController);
|
|
72
|
+
|
|
73
|
+
expect(registry.get(undefined)).toBe(mockController);
|
|
74
|
+
expect(registry.get(null)).toBe(mockController);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("should get default storage with getDefault()", () => {
|
|
78
|
+
const registry = new DefaultStorageRegistry();
|
|
79
|
+
const mockController = createMockStorageController("local");
|
|
80
|
+
|
|
81
|
+
registry.register(DEFAULT_STORAGE_ID, mockController);
|
|
82
|
+
|
|
83
|
+
expect(registry.getDefault()).toBe(mockController);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("should throw error when no default storage exists", () => {
|
|
87
|
+
const registry = new DefaultStorageRegistry();
|
|
88
|
+
|
|
89
|
+
expect(() => registry.getDefault()).toThrow(
|
|
90
|
+
"[StorageRegistry] No default storage registered."
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe("getOrDefault", () => {
|
|
96
|
+
let registry: DefaultStorageRegistry;
|
|
97
|
+
let defaultController: StorageController;
|
|
98
|
+
let mediaController: StorageController;
|
|
99
|
+
|
|
100
|
+
beforeEach(() => {
|
|
101
|
+
registry = new DefaultStorageRegistry();
|
|
102
|
+
defaultController = createMockStorageController("local");
|
|
103
|
+
mediaController = createMockStorageController("s3");
|
|
104
|
+
|
|
105
|
+
registry.register(DEFAULT_STORAGE_ID, defaultController);
|
|
106
|
+
registry.register("media", mediaController);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("should return specific storage when found", () => {
|
|
110
|
+
expect(registry.getOrDefault("media")).toBe(mediaController);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("should return default when id is undefined", () => {
|
|
114
|
+
expect(registry.getOrDefault(undefined)).toBe(defaultController);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("should return default when id is null", () => {
|
|
118
|
+
expect(registry.getOrDefault(null)).toBe(defaultController);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("should fallback to default when id not found", () => {
|
|
122
|
+
const consoleSpy = vi.spyOn(console, "warn").mockImplementation();
|
|
123
|
+
|
|
124
|
+
expect(registry.getOrDefault("non-existent")).toBe(defaultController);
|
|
125
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
126
|
+
expect.stringContaining('Storage "non-existent" not found')
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
consoleSpy.mockRestore();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("should throw when fallback fails (no default)", () => {
|
|
133
|
+
const emptyRegistry = new DefaultStorageRegistry();
|
|
134
|
+
|
|
135
|
+
expect(() => emptyRegistry.getOrDefault("anything")).toThrow();
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe("overwriting storages", () => {
|
|
140
|
+
it("should overwrite existing storage with same id", () => {
|
|
141
|
+
const registry = new DefaultStorageRegistry();
|
|
142
|
+
const original = createMockStorageController("local");
|
|
143
|
+
const replacement = createMockStorageController("s3");
|
|
144
|
+
|
|
145
|
+
const consoleSpy = vi.spyOn(console, "warn").mockImplementation();
|
|
146
|
+
|
|
147
|
+
registry.register("my-storage", original);
|
|
148
|
+
registry.register("my-storage", replacement);
|
|
149
|
+
|
|
150
|
+
expect(registry.get("my-storage")).toBe(replacement);
|
|
151
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
152
|
+
expect.stringContaining('Overwriting storage with id "my-storage"')
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
consoleSpy.mockRestore();
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe("list and size", () => {
|
|
160
|
+
it("should list all registered storages", () => {
|
|
161
|
+
const registry = new DefaultStorageRegistry();
|
|
162
|
+
|
|
163
|
+
registry.register("storage-1", createMockStorageController("local"));
|
|
164
|
+
registry.register("storage-2", createMockStorageController("s3"));
|
|
165
|
+
registry.register(DEFAULT_STORAGE_ID, createMockStorageController("local"));
|
|
166
|
+
|
|
167
|
+
const list = registry.list();
|
|
168
|
+
expect(list).toHaveLength(3);
|
|
169
|
+
expect(list).toContain("storage-1");
|
|
170
|
+
expect(list).toContain("storage-2");
|
|
171
|
+
expect(list).toContain(DEFAULT_STORAGE_ID);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("should return correct size", () => {
|
|
175
|
+
const registry = new DefaultStorageRegistry();
|
|
176
|
+
|
|
177
|
+
expect(registry.size()).toBe(0);
|
|
178
|
+
|
|
179
|
+
registry.register("storage-1", createMockStorageController("local"));
|
|
180
|
+
expect(registry.size()).toBe(1);
|
|
181
|
+
|
|
182
|
+
registry.register("storage-2", createMockStorageController("s3"));
|
|
183
|
+
expect(registry.size()).toBe(2);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
describe("DefaultStorageRegistry.create() factory", () => {
|
|
189
|
+
describe("with single StorageController", () => {
|
|
190
|
+
it('should register single controller as "(default)"', () => {
|
|
191
|
+
const mockController = createMockStorageController("local");
|
|
192
|
+
|
|
193
|
+
const registry = DefaultStorageRegistry.create(mockController);
|
|
194
|
+
|
|
195
|
+
expect(registry.has(DEFAULT_STORAGE_ID)).toBe(true);
|
|
196
|
+
expect(registry.getDefault()).toBe(mockController);
|
|
197
|
+
expect(registry.size()).toBe(1);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe("with map of StorageControllers", () => {
|
|
202
|
+
it("should register all controllers from map", () => {
|
|
203
|
+
const defaultController = createMockStorageController("local");
|
|
204
|
+
const mediaController = createMockStorageController("s3");
|
|
205
|
+
|
|
206
|
+
const registry = DefaultStorageRegistry.create({
|
|
207
|
+
[DEFAULT_STORAGE_ID]: defaultController,
|
|
208
|
+
"media": mediaController
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
expect(registry.size()).toBe(2);
|
|
212
|
+
expect(registry.getDefault()).toBe(defaultController);
|
|
213
|
+
expect(registry.get("media")).toBe(mediaController);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("should use first entry as default if no explicit default provided", () => {
|
|
217
|
+
const local = createMockStorageController("local");
|
|
218
|
+
const s3 = createMockStorageController("s3");
|
|
219
|
+
|
|
220
|
+
const consoleSpy = vi.spyOn(console, "warn").mockImplementation();
|
|
221
|
+
|
|
222
|
+
const registry = DefaultStorageRegistry.create({
|
|
223
|
+
"primary": local,
|
|
224
|
+
"secondary": s3
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// Should have registered both + created default pointing to first
|
|
228
|
+
expect(registry.size()).toBe(3); // primary, secondary, (default)
|
|
229
|
+
expect(registry.has(DEFAULT_STORAGE_ID)).toBe(true);
|
|
230
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
231
|
+
expect.stringContaining('No "(default)" storage provided')
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
consoleSpy.mockRestore();
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("should handle empty map gracefully", () => {
|
|
238
|
+
const registry = DefaultStorageRegistry.create({});
|
|
239
|
+
|
|
240
|
+
expect(registry.size()).toBe(0);
|
|
241
|
+
expect(() => registry.getDefault()).toThrow();
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
describe("type detection (isStorageController)", () => {
|
|
247
|
+
it("should correctly identify a StorageController", () => {
|
|
248
|
+
const mockController = createMockStorageController("local");
|
|
249
|
+
|
|
250
|
+
// The factory should recognize it as a single controller
|
|
251
|
+
const registry = DefaultStorageRegistry.create(mockController);
|
|
252
|
+
expect(registry.size()).toBe(1);
|
|
253
|
+
expect(registry.has(DEFAULT_STORAGE_ID)).toBe(true);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("should correctly identify a map of StorageControllers", () => {
|
|
257
|
+
const controllers = {
|
|
258
|
+
[DEFAULT_STORAGE_ID]: createMockStorageController("local"),
|
|
259
|
+
"other": createMockStorageController("s3")
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
// The factory should recognize it as a map
|
|
263
|
+
const registry = DefaultStorageRegistry.create(controllers);
|
|
264
|
+
expect(registry.size()).toBe(2);
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
describe("integration with storage types", () => {
|
|
269
|
+
it("should correctly report storage types", () => {
|
|
270
|
+
const localController = createMockStorageController("local");
|
|
271
|
+
const s3Controller = createMockStorageController("s3");
|
|
272
|
+
|
|
273
|
+
const registry = DefaultStorageRegistry.create({
|
|
274
|
+
[DEFAULT_STORAGE_ID]: localController,
|
|
275
|
+
"media": s3Controller
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
expect(registry.getDefault().getType()).toBe("local");
|
|
279
|
+
expect(registry.get("media")?.getType()).toBe("s3");
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
});
|