@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,222 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, jest as vi } from "@jest/globals";
|
|
2
|
+
/**
|
|
3
|
+
* Tests for storage routes — specifically the sub-router wildcard extraction
|
|
4
|
+
* that broke when Hono's c.req.param('*') stopped working in mounted sub-routers.
|
|
5
|
+
*
|
|
6
|
+
* These tests use Hono's built-in `app.fetch()` to simulate requests without
|
|
7
|
+
* needing a running HTTP server, which keeps them fast and deterministic.
|
|
8
|
+
*/
|
|
9
|
+
import * as fs from "fs";
|
|
10
|
+
import * as path from "path";
|
|
11
|
+
import * as os from "os";
|
|
12
|
+
import { Hono } from "hono";
|
|
13
|
+
import { HonoEnv } from "../src/api/types";
|
|
14
|
+
import { errorHandler } from "../src/api/errors";
|
|
15
|
+
import { LocalStorageController } from "../src/storage/LocalStorageController";
|
|
16
|
+
import { createStorageRoutes, extractWildcardPath } from "../src/storage/routes";
|
|
17
|
+
|
|
18
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
19
|
+
// Unit tests for extractWildcardPath
|
|
20
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
describe("extractWildcardPath", () => {
|
|
23
|
+
it("should extract path after the route prefix", () => {
|
|
24
|
+
const result = extractWildcardPath({
|
|
25
|
+
req: {
|
|
26
|
+
path: "/api/storage/metadata/default/author_pictures/photo.jpg",
|
|
27
|
+
routePath: "/api/storage/metadata/*"
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
expect(result).toBe("default/author_pictures/photo.jpg");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("should extract simple file path", () => {
|
|
34
|
+
const result = extractWildcardPath({
|
|
35
|
+
req: {
|
|
36
|
+
path: "/api/storage/file/testfile.jpg",
|
|
37
|
+
routePath: "/api/storage/file/*"
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
expect(result).toBe("testfile.jpg");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("should return empty string for trailing-slash-only path", () => {
|
|
44
|
+
const result = extractWildcardPath({
|
|
45
|
+
req: {
|
|
46
|
+
path: "/api/storage/metadata/",
|
|
47
|
+
routePath: "/api/storage/metadata/*"
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
expect(result).toBe("");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("should return empty string when path equals prefix (no trailing slash)", () => {
|
|
54
|
+
const result = extractWildcardPath({
|
|
55
|
+
req: {
|
|
56
|
+
path: "/api/storage/metadata",
|
|
57
|
+
routePath: "/api/storage/metadata/*"
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
expect(result).toBe("");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("should handle deeply nested paths", () => {
|
|
64
|
+
const result = extractWildcardPath({
|
|
65
|
+
req: {
|
|
66
|
+
path: "/api/storage/file/bucket/a/b/c/d/file.png",
|
|
67
|
+
routePath: "/api/storage/file/*"
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
expect(result).toBe("bucket/a/b/c/d/file.png");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("should handle paths with spaces and special chars", () => {
|
|
74
|
+
const result = extractWildcardPath({
|
|
75
|
+
req: {
|
|
76
|
+
path: "/api/storage/file/default/photos/my%20file%20(1).png",
|
|
77
|
+
routePath: "/api/storage/file/*"
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
expect(result).toBe("default/photos/my%20file%20(1).png");
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
85
|
+
// Integration tests: storage routes mounted as Hono sub-router
|
|
86
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
describe("Storage routes (sub-router integration)", () => {
|
|
89
|
+
let app: Hono<HonoEnv>;
|
|
90
|
+
let tempDir: string;
|
|
91
|
+
let controller: LocalStorageController;
|
|
92
|
+
|
|
93
|
+
beforeEach(async () => {
|
|
94
|
+
tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "rebase-routes-test-"));
|
|
95
|
+
controller = new LocalStorageController({ basePath: tempDir });
|
|
96
|
+
|
|
97
|
+
// Upload a test file so we have something to serve
|
|
98
|
+
const content = Buffer.from("Hello test file");
|
|
99
|
+
const file = new File([content], "test.txt", { type: "text/plain" });
|
|
100
|
+
await controller.putObject({ file,
|
|
101
|
+
key: "photos/test.txt" });
|
|
102
|
+
|
|
103
|
+
// Create the Hono app with storage routes mounted as a SUB-ROUTER
|
|
104
|
+
// (this is the exact pattern that caused the bug)
|
|
105
|
+
app = new Hono<HonoEnv>();
|
|
106
|
+
app.onError(errorHandler); // required to convert ApiError throws to proper HTTP responses
|
|
107
|
+
const storageRoutes = createStorageRoutes({
|
|
108
|
+
controller,
|
|
109
|
+
requireAuth: false // skip auth for tests
|
|
110
|
+
});
|
|
111
|
+
app.route("/api/storage", storageRoutes);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
afterEach(async () => {
|
|
115
|
+
await fs.promises.rm(tempDir, { recursive: true,
|
|
116
|
+
force: true });
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe("GET /metadata/*", () => {
|
|
120
|
+
it("should return metadata for a valid file path", async () => {
|
|
121
|
+
const res = await app.fetch(
|
|
122
|
+
new Request("http://localhost/api/storage/metadata/default/photos/test.txt")
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
expect(res.status).toBe(200);
|
|
126
|
+
const body = await res.json() as { success: boolean; data: { contentType: string } };
|
|
127
|
+
expect(body.success).toBe(true);
|
|
128
|
+
expect(body.data).toBeDefined();
|
|
129
|
+
expect(body.data.contentType).toBe("text/plain");
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("should return metadata without explicit bucket prefix", async () => {
|
|
133
|
+
const res = await app.fetch(
|
|
134
|
+
new Request("http://localhost/api/storage/metadata/photos/test.txt")
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
expect(res.status).toBe(200);
|
|
138
|
+
const body = await res.json() as { success: boolean; data: { contentType: string } };
|
|
139
|
+
expect(body.success).toBe(true);
|
|
140
|
+
expect(body.data).toBeDefined();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("should return 404 for empty path", async () => {
|
|
144
|
+
const res = await app.fetch(
|
|
145
|
+
new Request("http://localhost/api/storage/metadata/")
|
|
146
|
+
);
|
|
147
|
+
expect(res.status).toBe(404);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("should return 404 for non-existent file", async () => {
|
|
151
|
+
const res = await app.fetch(
|
|
152
|
+
new Request("http://localhost/api/storage/metadata/default/nope/missing.txt")
|
|
153
|
+
);
|
|
154
|
+
expect(res.status).toBe(404);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe("GET /file/*", () => {
|
|
159
|
+
it("should serve file content for a valid path", async () => {
|
|
160
|
+
const res = await app.fetch(
|
|
161
|
+
new Request("http://localhost/api/storage/file/default/photos/test.txt")
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
expect(res.status).toBe(200);
|
|
165
|
+
const body = await res.text();
|
|
166
|
+
expect(body).toBe("Hello test file");
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("should serve file without explicit bucket prefix", async () => {
|
|
170
|
+
const res = await app.fetch(
|
|
171
|
+
new Request("http://localhost/api/storage/file/photos/test.txt")
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
expect(res.status).toBe(200);
|
|
175
|
+
const body = await res.text();
|
|
176
|
+
expect(body).toBe("Hello test file");
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("should return 404 for empty path", async () => {
|
|
180
|
+
const res = await app.fetch(
|
|
181
|
+
new Request("http://localhost/api/storage/file/")
|
|
182
|
+
);
|
|
183
|
+
expect(res.status).toBe(404);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("should return 404 for non-existent file", async () => {
|
|
187
|
+
const res = await app.fetch(
|
|
188
|
+
new Request("http://localhost/api/storage/file/default/nope/missing.txt")
|
|
189
|
+
);
|
|
190
|
+
expect(res.status).toBe(404);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
describe("DELETE /file/*", () => {
|
|
195
|
+
it("should delete an existing file", async () => {
|
|
196
|
+
// Upload another file to delete
|
|
197
|
+
const file = new File([Buffer.from("delete me")], "deleteme.txt", { type: "text/plain" });
|
|
198
|
+
await controller.putObject({ file,
|
|
199
|
+
key: "photos/deleteme.txt" });
|
|
200
|
+
|
|
201
|
+
const res = await app.fetch(
|
|
202
|
+
new Request("http://localhost/api/storage/file/default/photos/deleteme.txt", { method: "DELETE" })
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
expect(res.status).toBe(200);
|
|
206
|
+
const body = await res.json() as { success: boolean };
|
|
207
|
+
expect(body.success).toBe(true);
|
|
208
|
+
|
|
209
|
+
// Verify the file is actually gone
|
|
210
|
+
const filePath = path.join(tempDir, "default", "photos", "deleteme.txt");
|
|
211
|
+
const exists = await fs.promises.access(filePath).then(() => true).catch(() => false);
|
|
212
|
+
expect(exists).toBe(false);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("should handle empty path gracefully", async () => {
|
|
216
|
+
const res = await app.fetch(
|
|
217
|
+
new Request("http://localhost/api/storage/file/", { method: "DELETE" })
|
|
218
|
+
);
|
|
219
|
+
expect(res.status).toBe(200);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
});
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, jest as vi } from "@jest/globals";
|
|
2
|
+
import { S3StorageController } from "../src/storage/S3StorageController";
|
|
3
|
+
|
|
4
|
+
// Mock the AWS SDK before importing the controller
|
|
5
|
+
vi.mock("@aws-sdk/client-s3", () => ({
|
|
6
|
+
S3Client: vi.fn().mockImplementation(function() {
|
|
7
|
+
return { send: vi.fn() };
|
|
8
|
+
}),
|
|
9
|
+
PutObjectCommand: vi.fn(),
|
|
10
|
+
GetObjectCommand: vi.fn(),
|
|
11
|
+
DeleteObjectCommand: vi.fn(),
|
|
12
|
+
ListObjectsV2Command: vi.fn(),
|
|
13
|
+
HeadObjectCommand: vi.fn()
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
vi.mock("@aws-sdk/s3-request-presigner", () => ({
|
|
17
|
+
getSignedUrl: vi.fn().mockResolvedValue("https://presigned-url.example.com")
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
import { S3Client, PutObjectCommand, GetObjectCommand, DeleteObjectCommand, ListObjectsV2Command } from "@aws-sdk/client-s3";
|
|
21
|
+
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
|
22
|
+
|
|
23
|
+
describe("S3StorageController", () => {
|
|
24
|
+
let controller: S3StorageController;
|
|
25
|
+
let mockSend: vi.Mock;
|
|
26
|
+
|
|
27
|
+
const defaultConfig = {
|
|
28
|
+
bucket: "test-bucket",
|
|
29
|
+
region: "us-east-1",
|
|
30
|
+
accessKeyId: "test-access-key",
|
|
31
|
+
secretAccessKey: "test-secret-key"
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
vi.clearAllMocks();
|
|
36
|
+
mockSend = vi.fn();
|
|
37
|
+
vi.mocked(S3Client).mockImplementation(function() {
|
|
38
|
+
return { send: mockSend };
|
|
39
|
+
} as any);
|
|
40
|
+
controller = new S3StorageController(defaultConfig);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe("constructor", () => {
|
|
44
|
+
it("should initialize S3 client with credentials", () => {
|
|
45
|
+
expect(S3Client).toHaveBeenCalledWith(expect.objectContaining({
|
|
46
|
+
region: "us-east-1",
|
|
47
|
+
credentials: {
|
|
48
|
+
accessKeyId: "test-access-key",
|
|
49
|
+
secretAccessKey: "test-secret-key"
|
|
50
|
+
}
|
|
51
|
+
}));
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("should initialize with endpoint for S3-compatible services", () => {
|
|
55
|
+
vi.clearAllMocks();
|
|
56
|
+
new S3StorageController({
|
|
57
|
+
...defaultConfig,
|
|
58
|
+
endpoint: "https://minio.example.com",
|
|
59
|
+
forcePathStyle: true
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
expect(S3Client).toHaveBeenCalledWith(expect.objectContaining({
|
|
63
|
+
endpoint: "https://minio.example.com",
|
|
64
|
+
forcePathStyle: true
|
|
65
|
+
}));
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("should return 's3' as type", () => {
|
|
69
|
+
expect(controller.getType()).toBe("s3");
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe("putObject", () => {
|
|
74
|
+
it("should upload file using PutObjectCommand", async () => {
|
|
75
|
+
const content = Buffer.from("Test content");
|
|
76
|
+
const file = new File([content], "test.txt", { type: "text/plain" });
|
|
77
|
+
|
|
78
|
+
mockSend.mockResolvedValueOnce({});
|
|
79
|
+
|
|
80
|
+
const result = await controller.putObject({
|
|
81
|
+
file,
|
|
82
|
+
key: "uploads/test.txt"
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
expect(PutObjectCommand).toHaveBeenCalledWith(expect.objectContaining({
|
|
86
|
+
Bucket: "test-bucket",
|
|
87
|
+
Key: "uploads/test.txt",
|
|
88
|
+
ContentType: "text/plain"
|
|
89
|
+
}));
|
|
90
|
+
expect(mockSend).toHaveBeenCalled();
|
|
91
|
+
expect(result.key).toBe("uploads/test.txt");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("should include metadata in upload", async () => {
|
|
95
|
+
const file = new File(["content"], "test.txt", { type: "text/plain" });
|
|
96
|
+
|
|
97
|
+
mockSend.mockResolvedValueOnce({});
|
|
98
|
+
|
|
99
|
+
await controller.putObject({
|
|
100
|
+
file,
|
|
101
|
+
key: "uploads/test.txt",
|
|
102
|
+
metadata: { customKey: "customValue" }
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
expect(PutObjectCommand).toHaveBeenCalledWith(expect.objectContaining({
|
|
106
|
+
Metadata: expect.objectContaining({
|
|
107
|
+
customKey: "customValue" // Keys are passed as-is, S3 handles casing
|
|
108
|
+
})
|
|
109
|
+
}));
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("should use custom bucket when specified", async () => {
|
|
113
|
+
const file = new File(["content"], "test.txt", { type: "text/plain" });
|
|
114
|
+
|
|
115
|
+
mockSend.mockResolvedValueOnce({});
|
|
116
|
+
|
|
117
|
+
await controller.putObject({
|
|
118
|
+
file,
|
|
119
|
+
key: "uploads/test.txt",
|
|
120
|
+
bucket: "custom-bucket"
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
expect(PutObjectCommand).toHaveBeenCalledWith(expect.objectContaining({
|
|
124
|
+
Bucket: "custom-bucket"
|
|
125
|
+
}));
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe("getSignedUrl", () => {
|
|
130
|
+
it("should generate presigned URL", async () => {
|
|
131
|
+
mockSend.mockResolvedValueOnce({
|
|
132
|
+
ContentType: "text/plain",
|
|
133
|
+
ContentLength: 100,
|
|
134
|
+
LastModified: new Date(),
|
|
135
|
+
Metadata: {}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const result = await controller.getSignedUrl("uploads/test.txt");
|
|
139
|
+
|
|
140
|
+
expect(getSignedUrl).toHaveBeenCalled();
|
|
141
|
+
expect(result.url).toBe("https://presigned-url.example.com");
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("should include metadata in download config", async () => {
|
|
145
|
+
const lastModified = new Date("2024-01-15T10:00:00Z");
|
|
146
|
+
mockSend.mockResolvedValueOnce({
|
|
147
|
+
ContentType: "image/png",
|
|
148
|
+
ContentLength: 5000,
|
|
149
|
+
LastModified: lastModified,
|
|
150
|
+
Metadata: { originalName: "photo.png" }
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const result = await controller.getSignedUrl("images/photo.png");
|
|
154
|
+
|
|
155
|
+
expect(result.metadata).toBeDefined();
|
|
156
|
+
expect(result.metadata?.contentType).toBe("image/png");
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe("getObject", () => {
|
|
161
|
+
it("should return null for non-existent file", async () => {
|
|
162
|
+
const error = new Error("NoSuchKey");
|
|
163
|
+
(error as any).name = "NoSuchKey";
|
|
164
|
+
mockSend.mockRejectedValueOnce(error);
|
|
165
|
+
|
|
166
|
+
const result = await controller.getObject("nonexistent.txt");
|
|
167
|
+
|
|
168
|
+
expect(result).toBeNull();
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe("deleteObject", () => {
|
|
173
|
+
it("should delete file from S3", async () => {
|
|
174
|
+
mockSend.mockResolvedValueOnce({});
|
|
175
|
+
|
|
176
|
+
await controller.deleteObject("uploads/test.txt");
|
|
177
|
+
|
|
178
|
+
expect(DeleteObjectCommand).toHaveBeenCalledWith(expect.objectContaining({
|
|
179
|
+
Bucket: "test-bucket",
|
|
180
|
+
Key: "uploads/test.txt"
|
|
181
|
+
}));
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("should not throw for non-existent file", async () => {
|
|
185
|
+
mockSend.mockResolvedValueOnce({});
|
|
186
|
+
|
|
187
|
+
await expect(controller.deleteObject("nonexistent.txt")).resolves.not.toThrow();
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
describe("list", () => {
|
|
192
|
+
it("should list objects in S3 bucket", async () => {
|
|
193
|
+
mockSend.mockResolvedValueOnce({
|
|
194
|
+
Contents: [
|
|
195
|
+
{ Key: "uploads/file1.txt",
|
|
196
|
+
Size: 100,
|
|
197
|
+
LastModified: new Date() },
|
|
198
|
+
{ Key: "uploads/file2.txt",
|
|
199
|
+
Size: 200,
|
|
200
|
+
LastModified: new Date() }
|
|
201
|
+
],
|
|
202
|
+
CommonPrefixes: [],
|
|
203
|
+
IsTruncated: false
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const result = await controller.listObjects("uploads");
|
|
207
|
+
|
|
208
|
+
expect(ListObjectsV2Command).toHaveBeenCalledWith(expect.objectContaining({
|
|
209
|
+
Bucket: "test-bucket",
|
|
210
|
+
Prefix: "uploads" // Implementation doesn't add trailing slash
|
|
211
|
+
}));
|
|
212
|
+
expect(result.items).toHaveLength(2);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("should handle pagination with maxResults", async () => {
|
|
216
|
+
mockSend.mockResolvedValueOnce({
|
|
217
|
+
Contents: [
|
|
218
|
+
{ Key: "uploads/file1.txt",
|
|
219
|
+
Size: 100,
|
|
220
|
+
LastModified: new Date() }
|
|
221
|
+
],
|
|
222
|
+
CommonPrefixes: [],
|
|
223
|
+
IsTruncated: true,
|
|
224
|
+
NextContinuationToken: "token123"
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
const result = await controller.listObjects("uploads", { maxResults: 1 });
|
|
228
|
+
|
|
229
|
+
expect(ListObjectsV2Command).toHaveBeenCalledWith(expect.objectContaining({
|
|
230
|
+
MaxKeys: 1
|
|
231
|
+
}));
|
|
232
|
+
expect(result.nextPageToken).toBe("token123");
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("should include common prefixes as subdirectories", async () => {
|
|
236
|
+
mockSend.mockResolvedValueOnce({
|
|
237
|
+
Contents: [],
|
|
238
|
+
CommonPrefixes: [
|
|
239
|
+
{ Prefix: "uploads/images/" },
|
|
240
|
+
{ Prefix: "uploads/documents/" }
|
|
241
|
+
],
|
|
242
|
+
IsTruncated: false
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
const result = await controller.listObjects("uploads");
|
|
246
|
+
|
|
247
|
+
expect(result.prefixes).toHaveLength(2);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("should use pageToken for continuation", async () => {
|
|
251
|
+
mockSend.mockResolvedValueOnce({
|
|
252
|
+
Contents: [],
|
|
253
|
+
CommonPrefixes: [],
|
|
254
|
+
IsTruncated: false
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
await controller.listObjects("uploads", { pageToken: "continue-token" });
|
|
258
|
+
|
|
259
|
+
expect(ListObjectsV2Command).toHaveBeenCalledWith(expect.objectContaining({
|
|
260
|
+
ContinuationToken: "continue-token"
|
|
261
|
+
}));
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
describe("validateFile", () => {
|
|
266
|
+
it("should accept valid file", () => {
|
|
267
|
+
const file = new File(["content"], "valid.txt", { type: "text/plain" });
|
|
268
|
+
|
|
269
|
+
expect(() => {
|
|
270
|
+
(controller as any).validateFile(file);
|
|
271
|
+
}).not.toThrow();
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("should reject file exceeding max size", () => {
|
|
275
|
+
const smallController = new S3StorageController({
|
|
276
|
+
...defaultConfig,
|
|
277
|
+
maxFileSize: 10 // 10 bytes
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
const largeContent = Buffer.alloc(100);
|
|
281
|
+
const file = new File([largeContent], "large.txt", { type: "text/plain" });
|
|
282
|
+
|
|
283
|
+
expect(() => {
|
|
284
|
+
(smallController as any).validateFile(file);
|
|
285
|
+
}).toThrow(/exceeds/i);
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
describe("flattenMetadata", () => {
|
|
290
|
+
it("should flatten nested metadata to strings", () => {
|
|
291
|
+
const metadata = {
|
|
292
|
+
simple: "value",
|
|
293
|
+
number: 42,
|
|
294
|
+
nested: { key: "value" }
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
const flattened = (controller as any).flattenMetadata(metadata);
|
|
298
|
+
|
|
299
|
+
expect(flattened.simple).toBe("value");
|
|
300
|
+
expect(flattened.number).toBe("42");
|
|
301
|
+
expect(typeof flattened.nested).toBe("string");
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
});
|
package/test-ast.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { AstSchemaEditor } from "./src/api/ast-schema-editor";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
|
|
4
|
+
const collectionsDir = __dirname + "/collections-test";
|
|
5
|
+
fs.mkdirSync(collectionsDir, { recursive: true });
|
|
6
|
+
fs.writeFileSync(collectionsDir + "/users.ts", `
|
|
7
|
+
export default {
|
|
8
|
+
name: "Users",
|
|
9
|
+
slug: "users",
|
|
10
|
+
properties: {},
|
|
11
|
+
securityRules: [
|
|
12
|
+
{ name: "test", operation: "read", mode: "permissive", roles: ["public"] }
|
|
13
|
+
]
|
|
14
|
+
};
|
|
15
|
+
`);
|
|
16
|
+
|
|
17
|
+
async function main() {
|
|
18
|
+
const editor = new AstSchemaEditor(collectionsDir);
|
|
19
|
+
await editor.saveCollection("users", {
|
|
20
|
+
name: "Users",
|
|
21
|
+
slug: "users",
|
|
22
|
+
properties: {},
|
|
23
|
+
securityRules: []
|
|
24
|
+
});
|
|
25
|
+
console.log("AFTER SAVE:", fs.readFileSync(collectionsDir + "/users.ts", "utf8"));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
main().catch(console.error);
|