@rebasepro/server-core 0.0.1-canary.4d4fb3e
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 +48 -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 +36 -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 +12 -0
- package/dist/common/src/util/resolutions.d.ts +72 -0
- package/dist/common/src/util/storage.d.ts +24 -0
- package/dist/index-BeMqpmfQ.js +239 -0
- package/dist/index-BeMqpmfQ.js.map +1 -0
- package/dist/index-bl4J3lNb.js +55823 -0
- package/dist/index-bl4J3lNb.js.map +1 -0
- package/dist/index.es.js +58 -0
- package/dist/index.es.js.map +1 -0
- package/dist/index.umd.js +56062 -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 +2 -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 +7 -0
- package/dist/server-core/src/auth/google-oauth.d.ts +20 -0
- package/dist/server-core/src/auth/index.d.ts +12 -0
- package/dist/server-core/src/auth/interfaces.d.ts +270 -0
- package/dist/server-core/src/auth/jwt.d.ts +42 -0
- package/dist/server-core/src/auth/middleware.d.ts +56 -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 +17 -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/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 +33 -0
- package/dist/server-core/src/email/types.d.ts +110 -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 +24 -0
- package/dist/server-core/src/init.d.ts +49 -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/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 +18 -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 +91 -0
- package/dist/server-core/src/types/index.d.ts +11 -0
- package/dist/server-core/src/utils/logging.d.ts +9 -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 +117 -0
- package/dist/types/src/controllers/client.d.ts +58 -0
- package/dist/types/src/controllers/collection_registry.d.ts +44 -0
- package/dist/types/src/controllers/customization_controller.d.ts +54 -0
- package/dist/types/src/controllers/data.d.ts +141 -0
- package/dist/types/src/controllers/data_driver.d.ts +168 -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/index.d.ts +17 -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 +51 -0
- package/dist/types/src/controllers/side_dialogs_controller.d.ts +67 -0
- package/dist/types/src/controllers/side_entity_controller.d.ts +89 -0
- package/dist/types/src/controllers/snackbar.d.ts +24 -0
- package/dist/types/src/controllers/storage.d.ts +173 -0
- package/dist/types/src/index.d.ts +4 -0
- package/dist/types/src/rebase_context.d.ts +101 -0
- package/dist/types/src/types/backend.d.ts +533 -0
- package/dist/types/src/types/builders.d.ts +14 -0
- package/dist/types/src/types/chips.d.ts +5 -0
- package/dist/types/src/types/collections.d.ts +812 -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 +9 -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 +22 -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 +225 -0
- package/dist/types/src/types/properties.d.ts +1091 -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 +228 -0
- package/dist/types/src/types/translations.d.ts +826 -0
- package/dist/types/src/types/user_management_delegate.d.ts +120 -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 +8 -0
- package/src/api/ast-schema-editor.ts +289 -0
- package/src/api/collections_for_test/callbacks_test_collection.ts +57 -0
- package/src/api/errors.ts +155 -0
- package/src/api/graphql/graphql-schema-generator.ts +334 -0
- package/src/api/graphql/index.ts +2 -0
- package/src/api/index.ts +11 -0
- package/src/api/openapi-generator.ts +160 -0
- package/src/api/rest/api-generator.ts +466 -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 +39 -0
- package/src/api/server.ts +245 -0
- package/src/api/types.ts +90 -0
- package/src/auth/admin-routes.ts +488 -0
- package/src/auth/google-oauth.ts +60 -0
- package/src/auth/index.ts +21 -0
- package/src/auth/interfaces.ts +316 -0
- package/src/auth/jwt.ts +164 -0
- package/src/auth/middleware.ts +235 -0
- package/src/auth/password.ts +75 -0
- package/src/auth/rate-limiter.ts +129 -0
- package/src/auth/routes.ts +730 -0
- package/src/bootstrappers/index.ts +1 -0
- package/src/collections/BackendCollectionRegistry.ts +20 -0
- package/src/collections/loader.ts +49 -0
- package/src/db/interfaces.ts +60 -0
- package/src/email/index.ts +17 -0
- package/src/email/smtp-email-service.ts +88 -0
- package/src/email/templates.ts +301 -0
- package/src/email/types.ts +112 -0
- package/src/functions/function-loader.ts +91 -0
- package/src/functions/function-routes.ts +31 -0
- package/src/functions/index.ts +3 -0
- package/src/history/history-routes.ts +128 -0
- package/src/history/index.ts +2 -0
- package/src/index.ts +56 -0
- package/src/init.ts +309 -0
- package/src/serve-spa.ts +81 -0
- package/src/services/driver-registry.ts +182 -0
- package/src/storage/LocalStorageController.ts +368 -0
- package/src/storage/S3StorageController.ts +295 -0
- package/src/storage/index.ts +32 -0
- package/src/storage/routes.ts +247 -0
- package/src/storage/storage-registry.ts +187 -0
- package/src/storage/types.ts +122 -0
- package/src/types/index.ts +27 -0
- package/src/utils/logging.ts +35 -0
- package/src/utils/sql.ts +38 -0
- package/test/admin-routes.test.ts +591 -0
- package/test/api-generator.test.ts +458 -0
- package/test/ast-schema-editor.test.ts +61 -0
- package/test/auth-middleware-hono.test.ts +321 -0
- package/test/auth-routes.test.ts +868 -0
- package/test/driver-registry.test.ts +280 -0
- package/test/errors-hono.test.ts +133 -0
- package/test/errors.test.ts +150 -0
- package/test/jwt-security.test.ts +173 -0
- package/test/jwt.test.ts +311 -0
- package/test/middleware.test.ts +295 -0
- package/test/password.test.ts +165 -0
- package/test/query-parser.test.ts +258 -0
- package/test/rate-limiter.test.ts +102 -0
- package/test/storage-local.test.ts +278 -0
- package/test/storage-registry.test.ts +280 -0
- package/test/storage-routes.test.ts +218 -0
- package/test/storage-s3.test.ts +301 -0
- package/test-ast.ts +28 -0
- package/test_output.txt +1133 -0
- package/tsconfig.json +49 -0
- package/tsconfig.prod.json +20 -0
- package/vite.config.ts +78 -0
- package/vite.config.ts.timestamp-1775065397568-8a853255edf6e.mjs +46 -0
package/src/serve-spa.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { serveStatic } from "@hono/node-server/serve-static";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import * as fs from "fs";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Configuration for serving a Single Page Application
|
|
8
|
+
*/
|
|
9
|
+
export interface ServeSPAConfig {
|
|
10
|
+
/**
|
|
11
|
+
* Absolute path to the frontend build directory
|
|
12
|
+
* @example path.join(__dirname, "../../frontend/dist")
|
|
13
|
+
*/
|
|
14
|
+
frontendPath: string;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Base path for API routes (default: "/api")
|
|
18
|
+
* Requests to this path will be passed through to API handlers
|
|
19
|
+
*/
|
|
20
|
+
apiBasePath?: string;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Additional paths to exclude from SPA handling
|
|
24
|
+
* These paths will be passed through to other handlers
|
|
25
|
+
* @example ["/health", "/ws", "/metrics"]
|
|
26
|
+
*/
|
|
27
|
+
excludePaths?: string[];
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Index file to serve for SPA routes (default: "index.html")
|
|
31
|
+
*/
|
|
32
|
+
indexFile?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Serve a Single Page Application from an Hono app.
|
|
37
|
+
*/
|
|
38
|
+
export function serveSPA<E extends import("hono").Env>(app: Hono<E>, config: ServeSPAConfig): void {
|
|
39
|
+
const {
|
|
40
|
+
frontendPath,
|
|
41
|
+
apiBasePath = "/api",
|
|
42
|
+
excludePaths = [],
|
|
43
|
+
indexFile = "index.html"
|
|
44
|
+
} = config;
|
|
45
|
+
|
|
46
|
+
// Validate frontend path exists
|
|
47
|
+
if (!fs.existsSync(frontendPath)) {
|
|
48
|
+
console.warn(`⚠️ Frontend build path does not exist: ${frontendPath}`);
|
|
49
|
+
console.warn(" SPA serving is disabled. Build your frontend first.");
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Serve static files from frontend build
|
|
54
|
+
app.use("/*", serveStatic({
|
|
55
|
+
root: path.relative(process.cwd(), frontendPath)
|
|
56
|
+
}));
|
|
57
|
+
|
|
58
|
+
// Build list of paths to exclude from SPA handling
|
|
59
|
+
const allExcludePaths = [apiBasePath, ...excludePaths];
|
|
60
|
+
|
|
61
|
+
// SPA fallback - serve index.html for all non-excluded routes
|
|
62
|
+
app.get("*", async (c, next) => {
|
|
63
|
+
// Skip excluded paths (API, health checks, etc.)
|
|
64
|
+
if (allExcludePaths.some(p => c.req.path.startsWith(p))) {
|
|
65
|
+
return next();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const indexPath = path.join(frontendPath, indexFile);
|
|
69
|
+
|
|
70
|
+
if (!fs.existsSync(indexPath)) {
|
|
71
|
+
console.warn(`⚠️ Index file not found: ${indexPath}`);
|
|
72
|
+
return next();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const html = fs.readFileSync(indexPath, "utf-8");
|
|
76
|
+
return c.html(html);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
console.log(`✅ SPA serving enabled from: ${frontendPath}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Driver Registry
|
|
3
|
+
*
|
|
4
|
+
* Manages multiple driver delegates for Rebase backend.
|
|
5
|
+
* Allows different databases for different collections.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* - Single DB: Pass a single DataDriver → maps to "(default)"
|
|
9
|
+
* - Multiple DBs: Pass a map of { dbId: DataDriver }
|
|
10
|
+
* - Collections use `databaseId` property to specify which driver to use
|
|
11
|
+
* - Collections without `databaseId` fallback to "(default)"
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { DataDriver } from "@rebasepro/types";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The default driver identifier used when:
|
|
18
|
+
* - A single driver is provided (not a map)
|
|
19
|
+
* - A collection doesn't specify a databaseId
|
|
20
|
+
*/
|
|
21
|
+
export const DEFAULT_DRIVER_ID = "(default)";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Registry for managing multiple driver delegates
|
|
25
|
+
*/
|
|
26
|
+
export interface DriverRegistry {
|
|
27
|
+
/**
|
|
28
|
+
* Register a driver delegate with an ID
|
|
29
|
+
* @param id - Unique identifier for this driver (e.g., "analytics", "users")
|
|
30
|
+
* @param delegate - The DataDriver instance
|
|
31
|
+
*/
|
|
32
|
+
register(id: string, delegate: DataDriver): void;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Get the default driver delegate (id = "(default)")
|
|
36
|
+
* @throws Error if no default driver is registered
|
|
37
|
+
*/
|
|
38
|
+
getDefault(): DataDriver;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get a driver delegate by ID
|
|
42
|
+
* @param id - Driver identifier, or undefined/null for default
|
|
43
|
+
* @returns The DataDriver, or undefined if not found
|
|
44
|
+
*/
|
|
45
|
+
get(id: string | undefined | null): DataDriver | undefined;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get a driver delegate by ID, with fallback to default
|
|
49
|
+
* @param id - Driver identifier, or undefined/null for default
|
|
50
|
+
* @returns The DataDriver (falls back to default if id not found)
|
|
51
|
+
* @throws Error if neither the specified nor default driver exists
|
|
52
|
+
*/
|
|
53
|
+
getOrDefault(id: string | undefined | null): DataDriver;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Check if a driver with the given ID exists
|
|
57
|
+
*/
|
|
58
|
+
has(id: string): boolean;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* List all registered driver IDs
|
|
62
|
+
*/
|
|
63
|
+
list(): string[];
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Get the number of registered drivers
|
|
67
|
+
*/
|
|
68
|
+
size(): number;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Default implementation of DriverRegistry
|
|
73
|
+
*/
|
|
74
|
+
export class DefaultDriverRegistry implements DriverRegistry {
|
|
75
|
+
private delegates = new Map<string, DataDriver>();
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Create a DriverRegistry from either a single delegate or a map
|
|
79
|
+
* @param input - Single DataDriver (maps to "(default)") or Record<string, DataDriver>
|
|
80
|
+
*/
|
|
81
|
+
static create(
|
|
82
|
+
input: DataDriver | Record<string, DataDriver>
|
|
83
|
+
): DefaultDriverRegistry {
|
|
84
|
+
const registry = new DefaultDriverRegistry();
|
|
85
|
+
|
|
86
|
+
if (isDataDriverDelegate(input)) {
|
|
87
|
+
// Single delegate → register as "(default)"
|
|
88
|
+
registry.register(DEFAULT_DRIVER_ID, input);
|
|
89
|
+
} else {
|
|
90
|
+
// Map of delegates → register each
|
|
91
|
+
for (const [id, delegate] of Object.entries(input)) {
|
|
92
|
+
registry.register(id, delegate);
|
|
93
|
+
}
|
|
94
|
+
// Ensure there's a default if not explicitly provided
|
|
95
|
+
if (!registry.has(DEFAULT_DRIVER_ID) && registry.size() > 0) {
|
|
96
|
+
// If no explicit "(default)", use the first one as default
|
|
97
|
+
const firstId = Object.keys(input)[0];
|
|
98
|
+
console.warn(
|
|
99
|
+
`[DriverRegistry] No "${DEFAULT_DRIVER_ID}" driver provided. ` +
|
|
100
|
+
`Using "${firstId}" as the default.`
|
|
101
|
+
);
|
|
102
|
+
registry.register(DEFAULT_DRIVER_ID, input[firstId]);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return registry;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
register(id: string, delegate: DataDriver): void {
|
|
110
|
+
if (this.delegates.has(id)) {
|
|
111
|
+
console.warn(`[DriverRegistry] Overwriting driver with id "${id}"`);
|
|
112
|
+
}
|
|
113
|
+
this.delegates.set(id, delegate);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
getDefault(): DataDriver {
|
|
117
|
+
const delegate = this.delegates.get(DEFAULT_DRIVER_ID);
|
|
118
|
+
if (!delegate) {
|
|
119
|
+
throw new Error(
|
|
120
|
+
`[DriverRegistry] No default driver registered. ` +
|
|
121
|
+
`Register one with id "${DEFAULT_DRIVER_ID}" or pass a single DataDriver.`
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
return delegate;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
get(id: string | undefined | null): DataDriver | undefined {
|
|
128
|
+
if (id === undefined || id === null) {
|
|
129
|
+
return this.delegates.get(DEFAULT_DRIVER_ID);
|
|
130
|
+
}
|
|
131
|
+
return this.delegates.get(id);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
getOrDefault(id: string | undefined | null): DataDriver {
|
|
135
|
+
// If no ID specified, return default
|
|
136
|
+
if (id === undefined || id === null) {
|
|
137
|
+
return this.getDefault();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Try to get by ID
|
|
141
|
+
const delegate = this.delegates.get(id);
|
|
142
|
+
if (delegate) {
|
|
143
|
+
return delegate;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Fallback to default with warning
|
|
147
|
+
console.warn(
|
|
148
|
+
`[DriverRegistry] Driver "${id}" not found, falling back to "${DEFAULT_DRIVER_ID}"`
|
|
149
|
+
);
|
|
150
|
+
return this.getDefault();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
has(id: string): boolean {
|
|
154
|
+
return this.delegates.has(id);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
list(): string[] {
|
|
158
|
+
return Array.from(this.delegates.keys());
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
size(): number {
|
|
162
|
+
return this.delegates.size;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Type guard to check if an object is a DataDriver
|
|
168
|
+
*/
|
|
169
|
+
function isDataDriverDelegate(obj: unknown): obj is DataDriver {
|
|
170
|
+
if (typeof obj !== "object" || obj === null) {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
const delegate = obj as DataDriver;
|
|
174
|
+
// Check for required DataDriver properties
|
|
175
|
+
return (
|
|
176
|
+
typeof delegate.key === "string" &&
|
|
177
|
+
typeof delegate.fetchCollection === "function" &&
|
|
178
|
+
typeof delegate.fetchEntity === "function" &&
|
|
179
|
+
typeof delegate.saveEntity === "function" &&
|
|
180
|
+
typeof delegate.deleteEntity === "function"
|
|
181
|
+
);
|
|
182
|
+
}
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local filesystem storage controller
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as fs from 'fs';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
import { promisify } from 'util';
|
|
8
|
+
import {
|
|
9
|
+
StorageController,
|
|
10
|
+
LocalStorageConfig,
|
|
11
|
+
DEFAULT_MAX_FILE_SIZE
|
|
12
|
+
} from './types';
|
|
13
|
+
import {
|
|
14
|
+
UploadFileProps,
|
|
15
|
+
UploadFileResult,
|
|
16
|
+
DownloadConfig,
|
|
17
|
+
DownloadMetadata,
|
|
18
|
+
StorageListResult,
|
|
19
|
+
StorageReference
|
|
20
|
+
} from '@rebasepro/types';
|
|
21
|
+
|
|
22
|
+
const mkdir = promisify(fs.mkdir);
|
|
23
|
+
const writeFile = promisify(fs.writeFile);
|
|
24
|
+
const readFile = promisify(fs.readFile);
|
|
25
|
+
const unlink = promisify(fs.unlink);
|
|
26
|
+
const readdir = promisify(fs.readdir);
|
|
27
|
+
const stat = promisify(fs.stat);
|
|
28
|
+
const access = promisify(fs.access);
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Remove initial and trailing slashes from a path.
|
|
32
|
+
* Handles paths like "/images/", "images/", "/images" → "images"
|
|
33
|
+
*/
|
|
34
|
+
function normalizeStoragePath(s: string): string {
|
|
35
|
+
let result = s;
|
|
36
|
+
while (result.startsWith('/')) {
|
|
37
|
+
result = result.slice(1);
|
|
38
|
+
}
|
|
39
|
+
while (result.endsWith('/')) {
|
|
40
|
+
result = result.slice(0, -1);
|
|
41
|
+
}
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Local filesystem storage implementation
|
|
47
|
+
* Stores files in a directory structure: {basePath}/{bucket}/{path}
|
|
48
|
+
*/
|
|
49
|
+
export class LocalStorageController implements StorageController {
|
|
50
|
+
private config: LocalStorageConfig;
|
|
51
|
+
private basePath: string;
|
|
52
|
+
|
|
53
|
+
constructor(config: LocalStorageConfig) {
|
|
54
|
+
this.config = config;
|
|
55
|
+
this.basePath = path.resolve(config.basePath);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
getType(): 'local' {
|
|
59
|
+
return 'local';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Ensure directory exists, creating it if necessary
|
|
64
|
+
*/
|
|
65
|
+
private async ensureDir(dirPath: string): Promise<void> {
|
|
66
|
+
try {
|
|
67
|
+
await mkdir(dirPath, { recursive: true });
|
|
68
|
+
} catch (error: unknown) {
|
|
69
|
+
if (error instanceof Error && (error as NodeJS.ErrnoException).code !== 'EEXIST') {
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Get the full filesystem path for a storage path.
|
|
77
|
+
* Includes a path traversal guard to prevent escaping the base directory.
|
|
78
|
+
*/
|
|
79
|
+
private getFullPath(storagePath: string, bucket?: string): string {
|
|
80
|
+
const parts = bucket ? [this.basePath, bucket, storagePath] : [this.basePath, storagePath];
|
|
81
|
+
const resolved = path.resolve(path.join(...parts));
|
|
82
|
+
if (!resolved.startsWith(this.basePath + path.sep) && resolved !== this.basePath) {
|
|
83
|
+
throw new Error("Path traversal detected: resolved storage path is outside the base directory.");
|
|
84
|
+
}
|
|
85
|
+
return resolved;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Validate file before upload
|
|
90
|
+
*/
|
|
91
|
+
private validateFile(file: File): void {
|
|
92
|
+
const maxSize = this.config.maxFileSize ?? DEFAULT_MAX_FILE_SIZE;
|
|
93
|
+
if (file.size > maxSize) {
|
|
94
|
+
throw new Error(`File size ${file.size} exceeds maximum allowed size ${maxSize}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (this.config.allowedMimeTypes && this.config.allowedMimeTypes.length > 0) {
|
|
98
|
+
if (!this.config.allowedMimeTypes.includes(file.type)) {
|
|
99
|
+
throw new Error(`File type ${file.type} is not allowed. Allowed types: ${this.config.allowedMimeTypes.join(', ')}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async uploadFile({
|
|
105
|
+
file,
|
|
106
|
+
fileName,
|
|
107
|
+
path: storagePath,
|
|
108
|
+
metadata,
|
|
109
|
+
bucket
|
|
110
|
+
}: UploadFileProps): Promise<UploadFileResult> {
|
|
111
|
+
this.validateFile(file);
|
|
112
|
+
|
|
113
|
+
// Always use a bucket (default to 'default')
|
|
114
|
+
const usedBucket = bucket ?? 'default';
|
|
115
|
+
const usedFileName = fileName ?? file.name;
|
|
116
|
+
// Normalize storage path to remove leading/trailing slashes
|
|
117
|
+
const normalizedPath = storagePath ? normalizeStoragePath(storagePath) : '';
|
|
118
|
+
const fullStoragePath = normalizedPath ? `${normalizedPath}/${usedFileName}` : usedFileName;
|
|
119
|
+
const fullPath = this.getFullPath(fullStoragePath, usedBucket);
|
|
120
|
+
|
|
121
|
+
// Ensure parent directory exists
|
|
122
|
+
await this.ensureDir(path.dirname(fullPath));
|
|
123
|
+
|
|
124
|
+
// Convert File to Buffer and write
|
|
125
|
+
const arrayBuffer = await file.arrayBuffer();
|
|
126
|
+
const buffer = Buffer.from(arrayBuffer);
|
|
127
|
+
await writeFile(fullPath, buffer);
|
|
128
|
+
|
|
129
|
+
// Always save metadata file with at least contentType (required for preview)
|
|
130
|
+
const metadataPath = `${fullPath}.metadata.json`;
|
|
131
|
+
await writeFile(metadataPath, JSON.stringify({
|
|
132
|
+
...(metadata || {}),
|
|
133
|
+
contentType: file.type,
|
|
134
|
+
size: file.size,
|
|
135
|
+
uploadedAt: new Date().toISOString()
|
|
136
|
+
}, null, 2));
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
path: fullStoragePath,
|
|
140
|
+
bucket: usedBucket,
|
|
141
|
+
storageUrl: `local://${usedBucket}/${fullStoragePath}`
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async getDownloadURL(storagePath: string, bucket?: string): Promise<DownloadConfig> {
|
|
146
|
+
// Handle local:// URLs
|
|
147
|
+
let resolvedPath = storagePath;
|
|
148
|
+
let resolvedBucket = bucket;
|
|
149
|
+
|
|
150
|
+
if (storagePath.startsWith('local://')) {
|
|
151
|
+
const withoutProtocol = storagePath.substring('local://'.length);
|
|
152
|
+
const firstSlash = withoutProtocol.indexOf('/');
|
|
153
|
+
if (firstSlash > 0) {
|
|
154
|
+
resolvedBucket = withoutProtocol.substring(0, firstSlash);
|
|
155
|
+
resolvedPath = withoutProtocol.substring(firstSlash + 1);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Normalize path to handle leading/trailing slashes
|
|
160
|
+
resolvedPath = normalizeStoragePath(resolvedPath);
|
|
161
|
+
const fullPath = this.getFullPath(resolvedPath, resolvedBucket);
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
await access(fullPath, fs.constants.R_OK);
|
|
165
|
+
} catch {
|
|
166
|
+
return {
|
|
167
|
+
url: null,
|
|
168
|
+
fileNotFound: true
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Read metadata if available
|
|
173
|
+
let metadata: DownloadMetadata | undefined;
|
|
174
|
+
const metadataPath = `${fullPath}.metadata.json`;
|
|
175
|
+
try {
|
|
176
|
+
const metadataContent = await readFile(metadataPath, 'utf-8');
|
|
177
|
+
const savedMetadata = JSON.parse(metadataContent);
|
|
178
|
+
const fileStat = await stat(fullPath);
|
|
179
|
+
|
|
180
|
+
metadata = {
|
|
181
|
+
bucket: resolvedBucket ?? 'default',
|
|
182
|
+
fullPath: resolvedPath,
|
|
183
|
+
name: path.basename(resolvedPath),
|
|
184
|
+
size: fileStat.size,
|
|
185
|
+
contentType: savedMetadata.contentType || 'application/octet-stream',
|
|
186
|
+
customMetadata: savedMetadata
|
|
187
|
+
};
|
|
188
|
+
} catch {
|
|
189
|
+
// No metadata file, create basic metadata from stat
|
|
190
|
+
try {
|
|
191
|
+
const fileStat = await stat(fullPath);
|
|
192
|
+
metadata = {
|
|
193
|
+
bucket: resolvedBucket ?? 'default',
|
|
194
|
+
fullPath: resolvedPath,
|
|
195
|
+
name: path.basename(resolvedPath),
|
|
196
|
+
size: fileStat.size,
|
|
197
|
+
contentType: 'application/octet-stream',
|
|
198
|
+
customMetadata: {}
|
|
199
|
+
};
|
|
200
|
+
} catch {
|
|
201
|
+
// Stat failed
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Return a relative URL that will be served by the storage routes
|
|
206
|
+
const bucketPath = resolvedBucket ? `${resolvedBucket}/` : '';
|
|
207
|
+
const url = `/api/storage/file/${bucketPath}${resolvedPath}`;
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
url,
|
|
211
|
+
metadata
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async getFile(storagePath: string, bucket?: string): Promise<File | null> {
|
|
216
|
+
// Handle local:// URLs
|
|
217
|
+
let resolvedPath = storagePath;
|
|
218
|
+
let resolvedBucket = bucket;
|
|
219
|
+
|
|
220
|
+
if (storagePath.startsWith('local://')) {
|
|
221
|
+
const withoutProtocol = storagePath.substring('local://'.length);
|
|
222
|
+
const firstSlash = withoutProtocol.indexOf('/');
|
|
223
|
+
if (firstSlash > 0) {
|
|
224
|
+
resolvedBucket = withoutProtocol.substring(0, firstSlash);
|
|
225
|
+
resolvedPath = withoutProtocol.substring(firstSlash + 1);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Normalize path to handle leading/trailing slashes
|
|
230
|
+
resolvedPath = normalizeStoragePath(resolvedPath);
|
|
231
|
+
const fullPath = this.getFullPath(resolvedPath, resolvedBucket);
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
await access(fullPath, fs.constants.R_OK);
|
|
235
|
+
const buffer = await readFile(fullPath);
|
|
236
|
+
|
|
237
|
+
// Try to get content type from metadata
|
|
238
|
+
let contentType = 'application/octet-stream';
|
|
239
|
+
try {
|
|
240
|
+
const metadataPath = `${fullPath}.metadata.json`;
|
|
241
|
+
const metadataContent = await readFile(metadataPath, 'utf-8');
|
|
242
|
+
const metadata = JSON.parse(metadataContent);
|
|
243
|
+
contentType = metadata.contentType || contentType;
|
|
244
|
+
} catch {
|
|
245
|
+
// No metadata, use default content type
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const blob = new Blob([buffer], { type: contentType });
|
|
249
|
+
return new File([blob], path.basename(resolvedPath), { type: contentType });
|
|
250
|
+
} catch {
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async deleteFile(storagePath: string, bucket?: string): Promise<void> {
|
|
256
|
+
// Handle local:// URLs
|
|
257
|
+
let resolvedPath = storagePath;
|
|
258
|
+
let resolvedBucket = bucket;
|
|
259
|
+
|
|
260
|
+
if (storagePath.startsWith('local://')) {
|
|
261
|
+
const withoutProtocol = storagePath.substring('local://'.length);
|
|
262
|
+
const firstSlash = withoutProtocol.indexOf('/');
|
|
263
|
+
if (firstSlash > 0) {
|
|
264
|
+
resolvedBucket = withoutProtocol.substring(0, firstSlash);
|
|
265
|
+
resolvedPath = withoutProtocol.substring(firstSlash + 1);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Normalize path to handle leading/trailing slashes
|
|
270
|
+
resolvedPath = normalizeStoragePath(resolvedPath);
|
|
271
|
+
const fullPath = this.getFullPath(resolvedPath, resolvedBucket);
|
|
272
|
+
|
|
273
|
+
try {
|
|
274
|
+
await unlink(fullPath);
|
|
275
|
+
// Also delete metadata file if exists
|
|
276
|
+
try {
|
|
277
|
+
await unlink(`${fullPath}.metadata.json`);
|
|
278
|
+
} catch {
|
|
279
|
+
// Metadata file might not exist
|
|
280
|
+
}
|
|
281
|
+
} catch (error: unknown) {
|
|
282
|
+
if (error instanceof Error && (error as NodeJS.ErrnoException).code !== 'ENOENT') {
|
|
283
|
+
throw error;
|
|
284
|
+
}
|
|
285
|
+
// File doesn't exist, nothing to delete
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async list(storagePath: string, options?: {
|
|
290
|
+
bucket?: string;
|
|
291
|
+
maxResults?: number;
|
|
292
|
+
pageToken?: string;
|
|
293
|
+
}): Promise<StorageListResult> {
|
|
294
|
+
// Normalize path to handle leading/trailing slashes
|
|
295
|
+
const normalizedPath = normalizeStoragePath(storagePath);
|
|
296
|
+
const fullPath = this.getFullPath(normalizedPath, options?.bucket);
|
|
297
|
+
const items: StorageReference[] = [];
|
|
298
|
+
const prefixes: StorageReference[] = [];
|
|
299
|
+
|
|
300
|
+
try {
|
|
301
|
+
await access(fullPath, fs.constants.R_OK);
|
|
302
|
+
const entries = await readdir(fullPath, { withFileTypes: true });
|
|
303
|
+
|
|
304
|
+
let count = 0;
|
|
305
|
+
const maxResults = options?.maxResults ?? 1000;
|
|
306
|
+
const startIndex = options?.pageToken ? parseInt(options.pageToken, 10) : 0;
|
|
307
|
+
|
|
308
|
+
for (let i = startIndex; i < entries.length && count < maxResults; i++) {
|
|
309
|
+
const entry = entries[i];
|
|
310
|
+
|
|
311
|
+
// Skip metadata files
|
|
312
|
+
if (entry.name.endsWith('.metadata.json')) {
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const entryPath = storagePath ? `${storagePath}/${entry.name}` : entry.name;
|
|
317
|
+
const bucket = options?.bucket ?? 'default';
|
|
318
|
+
|
|
319
|
+
const ref: StorageReference = {
|
|
320
|
+
bucket,
|
|
321
|
+
fullPath: entryPath,
|
|
322
|
+
name: entry.name,
|
|
323
|
+
parent: null as never, // Simplified - not fully implementing parent chain
|
|
324
|
+
root: null as never,
|
|
325
|
+
toString: () => `local://${bucket}/${entryPath}`
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
if (entry.isDirectory()) {
|
|
329
|
+
prefixes.push(ref);
|
|
330
|
+
} else {
|
|
331
|
+
items.push(ref);
|
|
332
|
+
}
|
|
333
|
+
count++;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const nextPageToken = startIndex + count < entries.length
|
|
337
|
+
? String(startIndex + count)
|
|
338
|
+
: undefined;
|
|
339
|
+
|
|
340
|
+
return {
|
|
341
|
+
items,
|
|
342
|
+
prefixes,
|
|
343
|
+
nextPageToken
|
|
344
|
+
};
|
|
345
|
+
} catch (error: unknown) {
|
|
346
|
+
const code = (error as NodeJS.ErrnoException)?.code;
|
|
347
|
+
if (code === 'ENOENT' || code === 'ENOTDIR') {
|
|
348
|
+
return { items: [], prefixes: [] };
|
|
349
|
+
}
|
|
350
|
+
throw error;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Get the absolute filesystem path for serving files
|
|
356
|
+
* Used by the storage routes to serve files directly
|
|
357
|
+
*/
|
|
358
|
+
getAbsolutePath(storagePath: string, bucket?: string): string {
|
|
359
|
+
return this.getFullPath(storagePath, bucket);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Get the base path for the storage
|
|
364
|
+
*/
|
|
365
|
+
getBasePath(): string {
|
|
366
|
+
return this.basePath;
|
|
367
|
+
}
|
|
368
|
+
}
|