@rebasepro/server-core 0.0.1-canary.4d4fb3e → 0.0.1-canary.ca2cb6e
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/dist/common/src/collections/CollectionRegistry.d.ts +8 -0
- package/dist/common/src/util/entities.d.ts +22 -0
- package/dist/common/src/util/relations.d.ts +14 -4
- package/dist/common/src/util/resolutions.d.ts +1 -1
- package/dist/index-DXVBFp5V.js +37 -0
- package/dist/index-DXVBFp5V.js.map +1 -0
- package/dist/index.es.js +49931 -55
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +28475 -34569
- package/dist/index.umd.js.map +1 -1
- package/dist/server-core/src/api/openapi-generator.d.ts +15 -1
- package/dist/server-core/src/api/types.d.ts +1 -1
- package/dist/server-core/src/auth/admin-routes.d.ts +10 -1
- 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 +5 -11
- package/dist/server-core/src/auth/index.d.ts +13 -2
- package/dist/server-core/src/auth/interfaces.d.ts +45 -6
- package/dist/server-core/src/auth/jwt.d.ts +1 -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 +36 -11
- package/dist/server-core/src/auth/rate-limiter.d.ts +2 -2
- package/dist/server-core/src/auth/routes.d.ts +13 -3
- 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/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/email/index.d.ts +2 -2
- package/dist/server-core/src/email/templates.d.ts +9 -0
- package/dist/server-core/src/email/types.d.ts +20 -23
- package/dist/server-core/src/history/history-routes.d.ts +2 -2
- package/dist/server-core/src/index.d.ts +5 -0
- package/dist/server-core/src/init.d.ts +112 -2
- package/dist/server-core/src/singleton.d.ts +35 -0
- package/dist/server-core/src/storage/LocalStorageController.d.ts +9 -9
- package/dist/server-core/src/storage/S3StorageController.d.ts +8 -8
- package/dist/server-core/src/storage/index.d.ts +18 -11
- package/dist/server-core/src/storage/routes.d.ts +3 -3
- package/dist/server-core/src/storage/types.d.ts +28 -16
- 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/request-logger.d.ts +19 -0
- package/dist/types/src/controllers/auth.d.ts +2 -0
- package/dist/types/src/controllers/client.d.ts +119 -7
- package/dist/types/src/controllers/collection_registry.d.ts +4 -3
- package/dist/types/src/controllers/customization_controller.d.ts +7 -1
- package/dist/types/src/controllers/data.d.ts +34 -7
- package/dist/types/src/controllers/data_driver.d.ts +20 -28
- package/dist/types/src/controllers/database_admin.d.ts +2 -2
- package/dist/types/src/controllers/email.d.ts +34 -0
- package/dist/types/src/controllers/index.d.ts +1 -0
- package/dist/types/src/controllers/local_config_persistence.d.ts +4 -4
- package/dist/types/src/controllers/navigation.d.ts +5 -5
- package/dist/types/src/controllers/registry.d.ts +6 -3
- package/dist/types/src/controllers/side_entity_controller.d.ts +7 -6
- package/dist/types/src/controllers/storage.d.ts +24 -26
- package/dist/types/src/rebase_context.d.ts +8 -4
- package/dist/types/src/types/backend.d.ts +4 -1
- package/dist/types/src/types/builders.d.ts +5 -4
- package/dist/types/src/types/chips.d.ts +1 -1
- package/dist/types/src/types/collections.d.ts +169 -125
- package/dist/types/src/types/cron.d.ts +102 -0
- package/dist/types/src/types/data_source.d.ts +1 -1
- package/dist/types/src/types/entity_actions.d.ts +8 -8
- package/dist/types/src/types/entity_callbacks.d.ts +15 -15
- package/dist/types/src/types/entity_link_builder.d.ts +1 -1
- package/dist/types/src/types/entity_overrides.d.ts +2 -1
- package/dist/types/src/types/entity_views.d.ts +8 -8
- package/dist/types/src/types/export_import.d.ts +3 -3
- package/dist/types/src/types/index.d.ts +1 -0
- package/dist/types/src/types/plugins.d.ts +72 -18
- package/dist/types/src/types/properties.d.ts +118 -33
- package/dist/types/src/types/relations.d.ts +1 -1
- package/dist/types/src/types/slots.d.ts +30 -6
- package/dist/types/src/types/translations.d.ts +44 -0
- package/dist/types/src/types/user_management_delegate.d.ts +1 -0
- package/package.json +84 -84
- package/scratch.ts +6 -5
- package/src/api/ast-schema-editor.ts +11 -11
- package/src/api/collections_for_test/callbacks_test_collection.ts +5 -2
- package/src/api/errors.ts +38 -14
- package/src/api/graphql/graphql-schema-generator.ts +5 -3
- package/src/api/openapi-generator.ts +613 -58
- package/src/api/rest/api-generator.ts +17 -11
- package/src/api/rest/query-parser.ts +1 -1
- package/src/api/schema-editor-routes.ts +2 -0
- package/src/api/server.ts +8 -5
- package/src/api/types.ts +1 -1
- package/src/auth/admin-routes.ts +58 -17
- 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 +34 -46
- package/src/auth/index.ts +15 -2
- package/src/auth/interfaces.ts +54 -7
- package/src/auth/jwt.ts +22 -5
- package/src/auth/linkedin-oauth.ts +81 -0
- package/src/auth/microsoft-oauth.ts +88 -0
- package/src/auth/middleware.ts +194 -45
- package/src/auth/password.ts +3 -1
- package/src/auth/rate-limiter.ts +4 -4
- package/src/auth/routes.ts +197 -139
- 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/collections/loader.ts +6 -6
- 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 +2 -2
- package/src/email/index.ts +3 -2
- package/src/email/smtp-email-service.ts +5 -2
- package/src/email/templates.ts +93 -6
- package/src/email/types.ts +17 -24
- package/src/functions/function-loader.ts +34 -6
- package/src/functions/function-routes.ts +2 -2
- package/src/history/history-routes.ts +4 -3
- package/src/index.ts +10 -0
- package/src/init.ts +475 -57
- package/src/services/driver-registry.ts +1 -1
- package/src/singleton.test.ts +28 -0
- package/src/singleton.ts +70 -0
- package/src/storage/LocalStorageController.ts +50 -53
- package/src/storage/S3StorageController.ts +49 -46
- package/src/storage/index.ts +27 -16
- package/src/storage/routes.ts +76 -59
- package/src/storage/storage-registry.ts +5 -5
- package/src/storage/types.ts +44 -32
- package/src/types/index.ts +4 -4
- package/src/utils/dev-port.ts +176 -0
- package/src/utils/logger.ts +143 -0
- package/src/utils/logging.ts +5 -2
- package/src/utils/request-logger.ts +66 -0
- package/test/admin-routes.test.ts +114 -65
- package/test/api-generator.test.ts +73 -30
- package/test/ast-schema-editor.test.ts +5 -3
- package/test/auth-middleware-hono.test.ts +268 -33
- package/test/auth-routes.test.ts +325 -146
- package/test/driver-registry.test.ts +4 -2
- package/test/error-propagation.test.ts +226 -0
- package/test/errors.test.ts +12 -7
- package/test/jwt-security.test.ts +18 -9
- package/test/jwt.test.ts +26 -13
- package/test/middleware.test.ts +13 -8
- package/test/query-parser.test.ts +13 -8
- package/test/rate-limiter.test.ts +7 -7
- package/test/safe-compare.test.ts +66 -0
- package/test/singleton.test.ts +59 -0
- package/test/storage-local.test.ts +36 -43
- package/test/storage-registry.test.ts +38 -36
- package/test/storage-routes.test.ts +9 -5
- package/test/storage-s3.test.ts +51 -48
- package/test.ts +6 -0
- package/tsconfig.json +1 -1
- package/vite.config.ts +7 -5
- package/dist/index-BeMqpmfQ.js +0 -239
- package/dist/index-BeMqpmfQ.js.map +0 -1
- package/dist/index-bl4J3lNb.js +0 -55823
- package/dist/index-bl4J3lNb.js.map +0 -1
- package/vite.config.ts.timestamp-1775065397568-8a853255edf6e.mjs +0 -46
|
@@ -117,7 +117,7 @@ export class DefaultDriverRegistry implements DriverRegistry {
|
|
|
117
117
|
const delegate = this.delegates.get(DEFAULT_DRIVER_ID);
|
|
118
118
|
if (!delegate) {
|
|
119
119
|
throw new Error(
|
|
120
|
-
|
|
120
|
+
"[DriverRegistry] No default driver registered. " +
|
|
121
121
|
`Register one with id "${DEFAULT_DRIVER_ID}" or pass a single DataDriver.`
|
|
122
122
|
);
|
|
123
123
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { rebase, _initRebase } from "./singleton";
|
|
2
|
+
import type { RebaseClient } from "@rebasepro/types";
|
|
3
|
+
|
|
4
|
+
describe("rebase singleton", () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
// Reset the singleton before each test
|
|
7
|
+
// By calling _initRebase with null (cast to bypass type checking for the reset)
|
|
8
|
+
_initRebase(null as unknown as RebaseClient);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("should throw an error if accessed before initialization", () => {
|
|
12
|
+
expect(() => rebase.data).toThrow(
|
|
13
|
+
"rebase.data: server not initialized yet. The singleton is available after Rebase starts — don't call it at import time."
|
|
14
|
+
);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("should return the correctly initialized instance properties", () => {
|
|
18
|
+
const mockClient = {
|
|
19
|
+
data: { test: "mockData" },
|
|
20
|
+
auth: { test: "mockAuth" }
|
|
21
|
+
} as unknown as RebaseClient;
|
|
22
|
+
|
|
23
|
+
_initRebase(mockClient);
|
|
24
|
+
|
|
25
|
+
expect(rebase.data).toEqual({ test: "mockData" });
|
|
26
|
+
expect(rebase.auth).toEqual({ test: "mockAuth" });
|
|
27
|
+
});
|
|
28
|
+
});
|
package/src/singleton.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { RebaseClient } from "@rebasepro/types";
|
|
2
|
+
|
|
3
|
+
let _instance: RebaseClient | null = null;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @internal Called once during server initialization to set the backing instance.
|
|
7
|
+
* This is invoked by `initializeRebaseBackend()` — never call it manually.
|
|
8
|
+
*/
|
|
9
|
+
export function _initRebase(client: RebaseClient): void {
|
|
10
|
+
_instance = client;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @internal Allows overriding the underlying instance for unit testing.
|
|
15
|
+
* Throws an error if used in a non-test environment to prevent production abuse.
|
|
16
|
+
*/
|
|
17
|
+
export function _setRebaseMock(mockInstance: Partial<RebaseClient>): void {
|
|
18
|
+
if (process.env.NODE_ENV !== "test") {
|
|
19
|
+
throw new Error("_setRebaseMock can only be called in a test environment (NODE_ENV=test).");
|
|
20
|
+
}
|
|
21
|
+
_instance = { ...(_instance || {} as RebaseClient),
|
|
22
|
+
...mockInstance } as RebaseClient;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @internal Resets the singleton instance, useful for afterEach() in test suites.
|
|
27
|
+
*/
|
|
28
|
+
export function _resetRebaseMock(): void {
|
|
29
|
+
if (process.env.NODE_ENV !== "test") {
|
|
30
|
+
throw new Error("_resetRebaseMock can only be called in a test environment.");
|
|
31
|
+
}
|
|
32
|
+
_instance = null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The server-side Rebase singleton.
|
|
37
|
+
*
|
|
38
|
+
* Initialized automatically during server startup. Provides access to all
|
|
39
|
+
* app-scoped services: **data**, **auth**, **storage**, and **email**.
|
|
40
|
+
*
|
|
41
|
+
* `rebase.data` runs with **admin privileges** (no RLS). For user-scoped
|
|
42
|
+
* queries inside request handlers, continue using the handler's context
|
|
43
|
+
* or the RLS-scoped driver.
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```typescript
|
|
47
|
+
* import { rebase } from "@rebasepro/server-core";
|
|
48
|
+
*
|
|
49
|
+
* // In a Hono handler, cron job, hook, or service file:
|
|
50
|
+
* await rebase.email?.send({ to: "admin@co.com", subject: "Alert", html: "<p>Hi</p>" });
|
|
51
|
+
* const jobs = await rebase.data.jobs.find({ limit: 10 });
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
export const rebase: RebaseClient = new Proxy({} as RebaseClient, {
|
|
55
|
+
get(_, prop) {
|
|
56
|
+
if (!_instance) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`rebase.${String(prop)}: server not initialized yet. ` +
|
|
59
|
+
"The singleton is available after Rebase starts — don't call it at import time."
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
return _instance[prop as keyof RebaseClient];
|
|
63
|
+
},
|
|
64
|
+
set(_, prop) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
`Cannot set rebase.${String(prop)} directly. ` +
|
|
67
|
+
"The singleton is read-only. Use _initRebase() during server startup."
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
* Local filesystem storage controller
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import * as fs from
|
|
6
|
-
import * as path from
|
|
7
|
-
import { promisify } from
|
|
5
|
+
import * as fs from "fs";
|
|
6
|
+
import * as path from "path";
|
|
7
|
+
import { promisify } from "util";
|
|
8
8
|
import {
|
|
9
9
|
StorageController,
|
|
10
10
|
LocalStorageConfig,
|
|
11
11
|
DEFAULT_MAX_FILE_SIZE
|
|
12
|
-
} from
|
|
12
|
+
} from "./types";
|
|
13
13
|
import {
|
|
14
14
|
UploadFileProps,
|
|
15
15
|
UploadFileResult,
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
DownloadMetadata,
|
|
18
18
|
StorageListResult,
|
|
19
19
|
StorageReference
|
|
20
|
-
} from
|
|
20
|
+
} from "@rebasepro/types";
|
|
21
21
|
|
|
22
22
|
const mkdir = promisify(fs.mkdir);
|
|
23
23
|
const writeFile = promisify(fs.writeFile);
|
|
@@ -33,10 +33,10 @@ const access = promisify(fs.access);
|
|
|
33
33
|
*/
|
|
34
34
|
function normalizeStoragePath(s: string): string {
|
|
35
35
|
let result = s;
|
|
36
|
-
while (result.startsWith(
|
|
36
|
+
while (result.startsWith("/")) {
|
|
37
37
|
result = result.slice(1);
|
|
38
38
|
}
|
|
39
|
-
while (result.endsWith(
|
|
39
|
+
while (result.endsWith("/")) {
|
|
40
40
|
result = result.slice(0, -1);
|
|
41
41
|
}
|
|
42
42
|
return result;
|
|
@@ -55,8 +55,8 @@ export class LocalStorageController implements StorageController {
|
|
|
55
55
|
this.basePath = path.resolve(config.basePath);
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
getType():
|
|
59
|
-
return
|
|
58
|
+
getType(): "local" {
|
|
59
|
+
return "local";
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
/**
|
|
@@ -66,7 +66,7 @@ export class LocalStorageController implements StorageController {
|
|
|
66
66
|
try {
|
|
67
67
|
await mkdir(dirPath, { recursive: true });
|
|
68
68
|
} catch (error: unknown) {
|
|
69
|
-
if (error instanceof Error && (error as NodeJS.ErrnoException).code !==
|
|
69
|
+
if (error instanceof Error && (error as NodeJS.ErrnoException).code !== "EEXIST") {
|
|
70
70
|
throw error;
|
|
71
71
|
}
|
|
72
72
|
}
|
|
@@ -96,26 +96,22 @@ export class LocalStorageController implements StorageController {
|
|
|
96
96
|
|
|
97
97
|
if (this.config.allowedMimeTypes && this.config.allowedMimeTypes.length > 0) {
|
|
98
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(
|
|
99
|
+
throw new Error(`File type ${file.type} is not allowed. Allowed types: ${this.config.allowedMimeTypes.join(", ")}`);
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
async
|
|
104
|
+
async putObject({
|
|
105
105
|
file,
|
|
106
|
-
|
|
107
|
-
path: storagePath,
|
|
106
|
+
key,
|
|
108
107
|
metadata,
|
|
109
108
|
bucket
|
|
110
109
|
}: UploadFileProps): Promise<UploadFileResult> {
|
|
111
110
|
this.validateFile(file);
|
|
112
111
|
|
|
113
112
|
// Always use a bucket (default to 'default')
|
|
114
|
-
const usedBucket = bucket ??
|
|
115
|
-
const
|
|
116
|
-
// Normalize storage path to remove leading/trailing slashes
|
|
117
|
-
const normalizedPath = storagePath ? normalizeStoragePath(storagePath) : '';
|
|
118
|
-
const fullStoragePath = normalizedPath ? `${normalizedPath}/${usedFileName}` : usedFileName;
|
|
113
|
+
const usedBucket = bucket ?? "default";
|
|
114
|
+
const fullStoragePath = key;
|
|
119
115
|
const fullPath = this.getFullPath(fullStoragePath, usedBucket);
|
|
120
116
|
|
|
121
117
|
// Ensure parent directory exists
|
|
@@ -136,20 +132,20 @@ export class LocalStorageController implements StorageController {
|
|
|
136
132
|
}, null, 2));
|
|
137
133
|
|
|
138
134
|
return {
|
|
139
|
-
|
|
135
|
+
key: fullStoragePath,
|
|
140
136
|
bucket: usedBucket,
|
|
141
137
|
storageUrl: `local://${usedBucket}/${fullStoragePath}`
|
|
142
138
|
};
|
|
143
139
|
}
|
|
144
140
|
|
|
145
|
-
async
|
|
141
|
+
async getSignedUrl(key: string, bucket?: string): Promise<DownloadConfig> {
|
|
146
142
|
// Handle local:// URLs
|
|
147
|
-
let resolvedPath =
|
|
143
|
+
let resolvedPath = key;
|
|
148
144
|
let resolvedBucket = bucket;
|
|
149
145
|
|
|
150
|
-
if (
|
|
151
|
-
const withoutProtocol =
|
|
152
|
-
const firstSlash = withoutProtocol.indexOf(
|
|
146
|
+
if (key.startsWith("local://")) {
|
|
147
|
+
const withoutProtocol = key.substring("local://".length);
|
|
148
|
+
const firstSlash = withoutProtocol.indexOf("/");
|
|
153
149
|
if (firstSlash > 0) {
|
|
154
150
|
resolvedBucket = withoutProtocol.substring(0, firstSlash);
|
|
155
151
|
resolvedPath = withoutProtocol.substring(firstSlash + 1);
|
|
@@ -173,16 +169,16 @@ export class LocalStorageController implements StorageController {
|
|
|
173
169
|
let metadata: DownloadMetadata | undefined;
|
|
174
170
|
const metadataPath = `${fullPath}.metadata.json`;
|
|
175
171
|
try {
|
|
176
|
-
const metadataContent = await readFile(metadataPath,
|
|
172
|
+
const metadataContent = await readFile(metadataPath, "utf-8");
|
|
177
173
|
const savedMetadata = JSON.parse(metadataContent);
|
|
178
174
|
const fileStat = await stat(fullPath);
|
|
179
175
|
|
|
180
176
|
metadata = {
|
|
181
|
-
bucket: resolvedBucket ??
|
|
177
|
+
bucket: resolvedBucket ?? "default",
|
|
182
178
|
fullPath: resolvedPath,
|
|
183
179
|
name: path.basename(resolvedPath),
|
|
184
180
|
size: fileStat.size,
|
|
185
|
-
contentType: savedMetadata.contentType ||
|
|
181
|
+
contentType: savedMetadata.contentType || "application/octet-stream",
|
|
186
182
|
customMetadata: savedMetadata
|
|
187
183
|
};
|
|
188
184
|
} catch {
|
|
@@ -190,11 +186,11 @@ export class LocalStorageController implements StorageController {
|
|
|
190
186
|
try {
|
|
191
187
|
const fileStat = await stat(fullPath);
|
|
192
188
|
metadata = {
|
|
193
|
-
bucket: resolvedBucket ??
|
|
189
|
+
bucket: resolvedBucket ?? "default",
|
|
194
190
|
fullPath: resolvedPath,
|
|
195
191
|
name: path.basename(resolvedPath),
|
|
196
192
|
size: fileStat.size,
|
|
197
|
-
contentType:
|
|
193
|
+
contentType: "application/octet-stream",
|
|
198
194
|
customMetadata: {}
|
|
199
195
|
};
|
|
200
196
|
} catch {
|
|
@@ -203,7 +199,7 @@ export class LocalStorageController implements StorageController {
|
|
|
203
199
|
}
|
|
204
200
|
|
|
205
201
|
// Return a relative URL that will be served by the storage routes
|
|
206
|
-
const bucketPath = resolvedBucket ? `${resolvedBucket}/` :
|
|
202
|
+
const bucketPath = resolvedBucket ? `${resolvedBucket}/` : "";
|
|
207
203
|
const url = `/api/storage/file/${bucketPath}${resolvedPath}`;
|
|
208
204
|
|
|
209
205
|
return {
|
|
@@ -212,14 +208,14 @@ export class LocalStorageController implements StorageController {
|
|
|
212
208
|
};
|
|
213
209
|
}
|
|
214
210
|
|
|
215
|
-
async
|
|
211
|
+
async getObject(key: string, bucket?: string): Promise<File | null> {
|
|
216
212
|
// Handle local:// URLs
|
|
217
|
-
let resolvedPath =
|
|
213
|
+
let resolvedPath = key;
|
|
218
214
|
let resolvedBucket = bucket;
|
|
219
215
|
|
|
220
|
-
if (
|
|
221
|
-
const withoutProtocol =
|
|
222
|
-
const firstSlash = withoutProtocol.indexOf(
|
|
216
|
+
if (key.startsWith("local://")) {
|
|
217
|
+
const withoutProtocol = key.substring("local://".length);
|
|
218
|
+
const firstSlash = withoutProtocol.indexOf("/");
|
|
223
219
|
if (firstSlash > 0) {
|
|
224
220
|
resolvedBucket = withoutProtocol.substring(0, firstSlash);
|
|
225
221
|
resolvedPath = withoutProtocol.substring(firstSlash + 1);
|
|
@@ -235,10 +231,10 @@ export class LocalStorageController implements StorageController {
|
|
|
235
231
|
const buffer = await readFile(fullPath);
|
|
236
232
|
|
|
237
233
|
// Try to get content type from metadata
|
|
238
|
-
let contentType =
|
|
234
|
+
let contentType = "application/octet-stream";
|
|
239
235
|
try {
|
|
240
236
|
const metadataPath = `${fullPath}.metadata.json`;
|
|
241
|
-
const metadataContent = await readFile(metadataPath,
|
|
237
|
+
const metadataContent = await readFile(metadataPath, "utf-8");
|
|
242
238
|
const metadata = JSON.parse(metadataContent);
|
|
243
239
|
contentType = metadata.contentType || contentType;
|
|
244
240
|
} catch {
|
|
@@ -252,14 +248,14 @@ export class LocalStorageController implements StorageController {
|
|
|
252
248
|
}
|
|
253
249
|
}
|
|
254
250
|
|
|
255
|
-
async
|
|
251
|
+
async deleteObject(key: string, bucket?: string): Promise<void> {
|
|
256
252
|
// Handle local:// URLs
|
|
257
|
-
let resolvedPath =
|
|
253
|
+
let resolvedPath = key;
|
|
258
254
|
let resolvedBucket = bucket;
|
|
259
255
|
|
|
260
|
-
if (
|
|
261
|
-
const withoutProtocol =
|
|
262
|
-
const firstSlash = withoutProtocol.indexOf(
|
|
256
|
+
if (key.startsWith("local://")) {
|
|
257
|
+
const withoutProtocol = key.substring("local://".length);
|
|
258
|
+
const firstSlash = withoutProtocol.indexOf("/");
|
|
263
259
|
if (firstSlash > 0) {
|
|
264
260
|
resolvedBucket = withoutProtocol.substring(0, firstSlash);
|
|
265
261
|
resolvedPath = withoutProtocol.substring(firstSlash + 1);
|
|
@@ -279,20 +275,20 @@ export class LocalStorageController implements StorageController {
|
|
|
279
275
|
// Metadata file might not exist
|
|
280
276
|
}
|
|
281
277
|
} catch (error: unknown) {
|
|
282
|
-
if (error instanceof Error && (error as NodeJS.ErrnoException).code !==
|
|
278
|
+
if (error instanceof Error && (error as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
283
279
|
throw error;
|
|
284
280
|
}
|
|
285
281
|
// File doesn't exist, nothing to delete
|
|
286
282
|
}
|
|
287
283
|
}
|
|
288
284
|
|
|
289
|
-
async
|
|
285
|
+
async listObjects(prefix: string, options?: {
|
|
290
286
|
bucket?: string;
|
|
291
287
|
maxResults?: number;
|
|
292
288
|
pageToken?: string;
|
|
293
289
|
}): Promise<StorageListResult> {
|
|
294
290
|
// Normalize path to handle leading/trailing slashes
|
|
295
|
-
const normalizedPath = normalizeStoragePath(
|
|
291
|
+
const normalizedPath = normalizeStoragePath(prefix);
|
|
296
292
|
const fullPath = this.getFullPath(normalizedPath, options?.bucket);
|
|
297
293
|
const items: StorageReference[] = [];
|
|
298
294
|
const prefixes: StorageReference[] = [];
|
|
@@ -309,12 +305,12 @@ export class LocalStorageController implements StorageController {
|
|
|
309
305
|
const entry = entries[i];
|
|
310
306
|
|
|
311
307
|
// Skip metadata files
|
|
312
|
-
if (entry.name.endsWith(
|
|
308
|
+
if (entry.name.endsWith(".metadata.json")) {
|
|
313
309
|
continue;
|
|
314
310
|
}
|
|
315
311
|
|
|
316
|
-
const entryPath =
|
|
317
|
-
const bucket = options?.bucket ??
|
|
312
|
+
const entryPath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
313
|
+
const bucket = options?.bucket ?? "default";
|
|
318
314
|
|
|
319
315
|
const ref: StorageReference = {
|
|
320
316
|
bucket,
|
|
@@ -344,8 +340,9 @@ export class LocalStorageController implements StorageController {
|
|
|
344
340
|
};
|
|
345
341
|
} catch (error: unknown) {
|
|
346
342
|
const code = (error as NodeJS.ErrnoException)?.code;
|
|
347
|
-
if (code ===
|
|
348
|
-
return { items: [],
|
|
343
|
+
if (code === "ENOENT" || code === "ENOTDIR") {
|
|
344
|
+
return { items: [],
|
|
345
|
+
prefixes: [] };
|
|
349
346
|
}
|
|
350
347
|
throw error;
|
|
351
348
|
}
|
|
@@ -355,8 +352,8 @@ export class LocalStorageController implements StorageController {
|
|
|
355
352
|
* Get the absolute filesystem path for serving files
|
|
356
353
|
* Used by the storage routes to serve files directly
|
|
357
354
|
*/
|
|
358
|
-
getAbsolutePath(
|
|
359
|
-
return this.getFullPath(
|
|
355
|
+
getAbsolutePath(key: string, bucket?: string): string {
|
|
356
|
+
return this.getFullPath(key, bucket);
|
|
360
357
|
}
|
|
361
358
|
|
|
362
359
|
/**
|
|
@@ -11,13 +11,13 @@ import {
|
|
|
11
11
|
HeadObjectCommand,
|
|
12
12
|
_Object,
|
|
13
13
|
CommonPrefix
|
|
14
|
-
} from
|
|
15
|
-
import { getSignedUrl } from
|
|
14
|
+
} from "@aws-sdk/client-s3";
|
|
15
|
+
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
|
16
16
|
import {
|
|
17
17
|
StorageController,
|
|
18
18
|
S3StorageConfig,
|
|
19
19
|
DEFAULT_MAX_FILE_SIZE
|
|
20
|
-
} from
|
|
20
|
+
} from "./types";
|
|
21
21
|
import {
|
|
22
22
|
UploadFileProps,
|
|
23
23
|
UploadFileResult,
|
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
DownloadMetadata,
|
|
26
26
|
StorageListResult,
|
|
27
27
|
StorageReference
|
|
28
|
-
} from
|
|
28
|
+
} from "@rebasepro/types";
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* S3-compatible storage implementation
|
|
@@ -38,7 +38,7 @@ export class S3StorageController implements StorageController {
|
|
|
38
38
|
constructor(config: S3StorageConfig) {
|
|
39
39
|
this.config = config;
|
|
40
40
|
this.client = new S3Client({
|
|
41
|
-
region: config.region ||
|
|
41
|
+
region: config.region || "us-east-1",
|
|
42
42
|
endpoint: config.endpoint,
|
|
43
43
|
forcePathStyle: config.forcePathStyle ?? !!config.endpoint, // Auto-enable for custom endpoints (MinIO)
|
|
44
44
|
credentials: {
|
|
@@ -48,8 +48,8 @@ export class S3StorageController implements StorageController {
|
|
|
48
48
|
});
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
getType():
|
|
52
|
-
return
|
|
51
|
+
getType(): "s3" {
|
|
52
|
+
return "s3";
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
/**
|
|
@@ -63,7 +63,7 @@ export class S3StorageController implements StorageController {
|
|
|
63
63
|
|
|
64
64
|
if (this.config.allowedMimeTypes && this.config.allowedMimeTypes.length > 0) {
|
|
65
65
|
if (!this.config.allowedMimeTypes.includes(file.type)) {
|
|
66
|
-
throw new Error(`File type ${file.type} is not allowed. Allowed types: ${this.config.allowedMimeTypes.join(
|
|
66
|
+
throw new Error(`File type ${file.type} is not allowed. Allowed types: ${this.config.allowedMimeTypes.join(", ")}`);
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
}
|
|
@@ -75,17 +75,14 @@ export class S3StorageController implements StorageController {
|
|
|
75
75
|
return bucket ?? this.config.bucket;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
async
|
|
78
|
+
async putObject({
|
|
79
79
|
file,
|
|
80
|
-
|
|
81
|
-
path: storagePath,
|
|
80
|
+
key,
|
|
82
81
|
metadata,
|
|
83
82
|
bucket
|
|
84
83
|
}: UploadFileProps): Promise<UploadFileResult> {
|
|
85
84
|
this.validateFile(file);
|
|
86
85
|
|
|
87
|
-
const usedFileName = fileName ?? file.name;
|
|
88
|
-
const key = storagePath ? `${storagePath}/${usedFileName}` : usedFileName;
|
|
89
86
|
const usedBucket = this.getBucket(bucket);
|
|
90
87
|
|
|
91
88
|
// Convert File to Buffer
|
|
@@ -103,7 +100,7 @@ export class S3StorageController implements StorageController {
|
|
|
103
100
|
await this.client.send(command);
|
|
104
101
|
|
|
105
102
|
return {
|
|
106
|
-
|
|
103
|
+
key,
|
|
107
104
|
bucket: usedBucket,
|
|
108
105
|
storageUrl: `s3://${usedBucket}/${key}`
|
|
109
106
|
};
|
|
@@ -115,7 +112,7 @@ export class S3StorageController implements StorageController {
|
|
|
115
112
|
private flattenMetadata(metadata: Record<string, unknown>): Record<string, string> {
|
|
116
113
|
const flattened: Record<string, string> = {};
|
|
117
114
|
for (const [key, value] of Object.entries(metadata)) {
|
|
118
|
-
if (typeof value ===
|
|
115
|
+
if (typeof value === "string") {
|
|
119
116
|
flattened[key] = value;
|
|
120
117
|
} else if (value !== undefined && value !== null) {
|
|
121
118
|
flattened[key] = JSON.stringify(value);
|
|
@@ -124,14 +121,16 @@ export class S3StorageController implements StorageController {
|
|
|
124
121
|
return flattened;
|
|
125
122
|
}
|
|
126
123
|
|
|
127
|
-
async
|
|
128
|
-
// Handle s3:// URLs
|
|
129
|
-
let resolvedPath =
|
|
124
|
+
async getSignedUrl(key: string, bucket?: string): Promise<DownloadConfig> {
|
|
125
|
+
// Handle s3:// and gs:// URLs for backward compatibility
|
|
126
|
+
let resolvedPath = key;
|
|
130
127
|
let resolvedBucket = this.getBucket(bucket);
|
|
131
128
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const
|
|
129
|
+
const match = key.match(/^(s3|gs):\/\//);
|
|
130
|
+
if (match) {
|
|
131
|
+
const protocolLength = match[0].length;
|
|
132
|
+
const withoutProtocol = key.substring(protocolLength);
|
|
133
|
+
const firstSlash = withoutProtocol.indexOf("/");
|
|
135
134
|
if (firstSlash > 0) {
|
|
136
135
|
resolvedBucket = withoutProtocol.substring(0, firstSlash);
|
|
137
136
|
resolvedPath = withoutProtocol.substring(firstSlash + 1);
|
|
@@ -159,9 +158,9 @@ export class S3StorageController implements StorageController {
|
|
|
159
158
|
const metadata: DownloadMetadata = {
|
|
160
159
|
bucket: resolvedBucket,
|
|
161
160
|
fullPath: resolvedPath,
|
|
162
|
-
name: resolvedPath.split(
|
|
161
|
+
name: resolvedPath.split("/").pop() || resolvedPath,
|
|
163
162
|
size: headResult.ContentLength || 0,
|
|
164
|
-
contentType: headResult.ContentType ||
|
|
163
|
+
contentType: headResult.ContentType || "application/octet-stream",
|
|
165
164
|
customMetadata: headResult.Metadata || {}
|
|
166
165
|
};
|
|
167
166
|
|
|
@@ -171,7 +170,7 @@ export class S3StorageController implements StorageController {
|
|
|
171
170
|
};
|
|
172
171
|
} catch (error: unknown) {
|
|
173
172
|
const s3Error = error as { name?: string; $metadata?: { httpStatusCode?: number } };
|
|
174
|
-
if (s3Error.name ===
|
|
173
|
+
if (s3Error.name === "NotFound" || s3Error.$metadata?.httpStatusCode === 404) {
|
|
175
174
|
return {
|
|
176
175
|
url: null,
|
|
177
176
|
fileNotFound: true
|
|
@@ -181,14 +180,16 @@ export class S3StorageController implements StorageController {
|
|
|
181
180
|
}
|
|
182
181
|
}
|
|
183
182
|
|
|
184
|
-
async
|
|
185
|
-
// Handle s3:// URLs
|
|
186
|
-
let resolvedPath =
|
|
183
|
+
async getObject(key: string, bucket?: string): Promise<File | null> {
|
|
184
|
+
// Handle s3:// and gs:// URLs
|
|
185
|
+
let resolvedPath = key;
|
|
187
186
|
let resolvedBucket = this.getBucket(bucket);
|
|
188
187
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
const
|
|
188
|
+
const match = key.match(/^(s3|gs):\/\//);
|
|
189
|
+
if (match) {
|
|
190
|
+
const protocolLength = match[0].length;
|
|
191
|
+
const withoutProtocol = key.substring(protocolLength);
|
|
192
|
+
const firstSlash = withoutProtocol.indexOf("/");
|
|
192
193
|
if (firstSlash > 0) {
|
|
193
194
|
resolvedBucket = withoutProtocol.substring(0, firstSlash);
|
|
194
195
|
resolvedPath = withoutProtocol.substring(firstSlash + 1);
|
|
@@ -215,28 +216,30 @@ export class S3StorageController implements StorageController {
|
|
|
215
216
|
}
|
|
216
217
|
const buffer = Buffer.concat(chunks);
|
|
217
218
|
|
|
218
|
-
const contentType = response.ContentType ||
|
|
219
|
-
const fileName = resolvedPath.split(
|
|
219
|
+
const contentType = response.ContentType || "application/octet-stream";
|
|
220
|
+
const fileName = resolvedPath.split("/").pop() || resolvedPath;
|
|
220
221
|
|
|
221
222
|
const blob = new Blob([buffer], { type: contentType });
|
|
222
223
|
return new File([blob], fileName, { type: contentType });
|
|
223
224
|
} catch (error: unknown) {
|
|
224
225
|
const s3Error = error as { name?: string; $metadata?: { httpStatusCode?: number } };
|
|
225
|
-
if (s3Error.name ===
|
|
226
|
+
if (s3Error.name === "NoSuchKey" || s3Error.$metadata?.httpStatusCode === 404) {
|
|
226
227
|
return null;
|
|
227
228
|
}
|
|
228
229
|
throw error;
|
|
229
230
|
}
|
|
230
231
|
}
|
|
231
232
|
|
|
232
|
-
async
|
|
233
|
-
// Handle s3:// URLs
|
|
234
|
-
let resolvedPath =
|
|
233
|
+
async deleteObject(key: string, bucket?: string): Promise<void> {
|
|
234
|
+
// Handle s3:// and gs:// URLs
|
|
235
|
+
let resolvedPath = key;
|
|
235
236
|
let resolvedBucket = this.getBucket(bucket);
|
|
236
237
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
const
|
|
238
|
+
const match = key.match(/^(s3|gs):\/\//);
|
|
239
|
+
if (match) {
|
|
240
|
+
const protocolLength = match[0].length;
|
|
241
|
+
const withoutProtocol = key.substring(protocolLength);
|
|
242
|
+
const firstSlash = withoutProtocol.indexOf("/");
|
|
240
243
|
if (firstSlash > 0) {
|
|
241
244
|
resolvedBucket = withoutProtocol.substring(0, firstSlash);
|
|
242
245
|
resolvedPath = withoutProtocol.substring(firstSlash + 1);
|
|
@@ -251,7 +254,7 @@ export class S3StorageController implements StorageController {
|
|
|
251
254
|
await this.client.send(command);
|
|
252
255
|
}
|
|
253
256
|
|
|
254
|
-
async
|
|
257
|
+
async listObjects(prefix: string, options?: {
|
|
255
258
|
bucket?: string;
|
|
256
259
|
maxResults?: number;
|
|
257
260
|
pageToken?: string;
|
|
@@ -260,18 +263,18 @@ export class S3StorageController implements StorageController {
|
|
|
260
263
|
|
|
261
264
|
const command = new ListObjectsV2Command({
|
|
262
265
|
Bucket: resolvedBucket,
|
|
263
|
-
Prefix:
|
|
266
|
+
Prefix: prefix || undefined,
|
|
264
267
|
MaxKeys: options?.maxResults ?? 1000,
|
|
265
268
|
ContinuationToken: options?.pageToken,
|
|
266
|
-
Delimiter:
|
|
269
|
+
Delimiter: "/" // This gives us folder-like behavior
|
|
267
270
|
});
|
|
268
271
|
|
|
269
272
|
const response = await this.client.send(command);
|
|
270
273
|
|
|
271
274
|
const items: StorageReference[] = (response.Contents || []).map(obj => ({
|
|
272
275
|
bucket: resolvedBucket,
|
|
273
|
-
fullPath: obj.Key ||
|
|
274
|
-
name: (obj.Key ||
|
|
276
|
+
fullPath: obj.Key || "",
|
|
277
|
+
name: (obj.Key || "").split("/").pop() || "",
|
|
275
278
|
parent: null as never,
|
|
276
279
|
root: null as never,
|
|
277
280
|
toString: () => `s3://${resolvedBucket}/${obj.Key}`
|
|
@@ -279,8 +282,8 @@ export class S3StorageController implements StorageController {
|
|
|
279
282
|
|
|
280
283
|
const prefixes: StorageReference[] = (response.CommonPrefixes || []).map(prefix => ({
|
|
281
284
|
bucket: resolvedBucket,
|
|
282
|
-
fullPath: prefix.Prefix ||
|
|
283
|
-
name: (prefix.Prefix ||
|
|
285
|
+
fullPath: prefix.Prefix || "",
|
|
286
|
+
name: (prefix.Prefix || "").replace(/\/$/, "").split("/").pop() || "",
|
|
284
287
|
parent: null as never,
|
|
285
288
|
root: null as never,
|
|
286
289
|
toString: () => `s3://${resolvedBucket}/${prefix.Prefix}`
|