@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
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S3-compatible storage controller (works with AWS S3 and MinIO)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
S3Client,
|
|
7
|
+
PutObjectCommand,
|
|
8
|
+
GetObjectCommand,
|
|
9
|
+
DeleteObjectCommand,
|
|
10
|
+
ListObjectsV2Command,
|
|
11
|
+
HeadObjectCommand,
|
|
12
|
+
_Object,
|
|
13
|
+
CommonPrefix
|
|
14
|
+
} from '@aws-sdk/client-s3';
|
|
15
|
+
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
|
16
|
+
import {
|
|
17
|
+
StorageController,
|
|
18
|
+
S3StorageConfig,
|
|
19
|
+
DEFAULT_MAX_FILE_SIZE
|
|
20
|
+
} from './types';
|
|
21
|
+
import {
|
|
22
|
+
UploadFileProps,
|
|
23
|
+
UploadFileResult,
|
|
24
|
+
DownloadConfig,
|
|
25
|
+
DownloadMetadata,
|
|
26
|
+
StorageListResult,
|
|
27
|
+
StorageReference
|
|
28
|
+
} from '@rebasepro/types';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* S3-compatible storage implementation
|
|
32
|
+
* Works with AWS S3 and MinIO (with forcePathStyle option)
|
|
33
|
+
*/
|
|
34
|
+
export class S3StorageController implements StorageController {
|
|
35
|
+
private config: S3StorageConfig;
|
|
36
|
+
private client: S3Client;
|
|
37
|
+
|
|
38
|
+
constructor(config: S3StorageConfig) {
|
|
39
|
+
this.config = config;
|
|
40
|
+
this.client = new S3Client({
|
|
41
|
+
region: config.region || 'us-east-1',
|
|
42
|
+
endpoint: config.endpoint,
|
|
43
|
+
forcePathStyle: config.forcePathStyle ?? !!config.endpoint, // Auto-enable for custom endpoints (MinIO)
|
|
44
|
+
credentials: {
|
|
45
|
+
accessKeyId: config.accessKeyId,
|
|
46
|
+
secretAccessKey: config.secretAccessKey
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
getType(): 's3' {
|
|
52
|
+
return 's3';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Validate file before upload
|
|
57
|
+
*/
|
|
58
|
+
private validateFile(file: File): void {
|
|
59
|
+
const maxSize = this.config.maxFileSize ?? DEFAULT_MAX_FILE_SIZE;
|
|
60
|
+
if (file.size > maxSize) {
|
|
61
|
+
throw new Error(`File size ${file.size} exceeds maximum allowed size ${maxSize}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (this.config.allowedMimeTypes && this.config.allowedMimeTypes.length > 0) {
|
|
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(', ')}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Get the bucket name - either from parameter or config
|
|
73
|
+
*/
|
|
74
|
+
private getBucket(bucket?: string): string {
|
|
75
|
+
return bucket ?? this.config.bucket;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async uploadFile({
|
|
79
|
+
file,
|
|
80
|
+
fileName,
|
|
81
|
+
path: storagePath,
|
|
82
|
+
metadata,
|
|
83
|
+
bucket
|
|
84
|
+
}: UploadFileProps): Promise<UploadFileResult> {
|
|
85
|
+
this.validateFile(file);
|
|
86
|
+
|
|
87
|
+
const usedFileName = fileName ?? file.name;
|
|
88
|
+
const key = storagePath ? `${storagePath}/${usedFileName}` : usedFileName;
|
|
89
|
+
const usedBucket = this.getBucket(bucket);
|
|
90
|
+
|
|
91
|
+
// Convert File to Buffer
|
|
92
|
+
const arrayBuffer = await file.arrayBuffer();
|
|
93
|
+
const buffer = Buffer.from(arrayBuffer);
|
|
94
|
+
|
|
95
|
+
const command = new PutObjectCommand({
|
|
96
|
+
Bucket: usedBucket,
|
|
97
|
+
Key: key,
|
|
98
|
+
Body: buffer,
|
|
99
|
+
ContentType: file.type,
|
|
100
|
+
Metadata: metadata ? this.flattenMetadata(metadata) : undefined
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
await this.client.send(command);
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
path: key,
|
|
107
|
+
bucket: usedBucket,
|
|
108
|
+
storageUrl: `s3://${usedBucket}/${key}`
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Flatten nested metadata to string values (S3 requirement)
|
|
114
|
+
*/
|
|
115
|
+
private flattenMetadata(metadata: Record<string, unknown>): Record<string, string> {
|
|
116
|
+
const flattened: Record<string, string> = {};
|
|
117
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
118
|
+
if (typeof value === 'string') {
|
|
119
|
+
flattened[key] = value;
|
|
120
|
+
} else if (value !== undefined && value !== null) {
|
|
121
|
+
flattened[key] = JSON.stringify(value);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return flattened;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async getDownloadURL(storagePath: string, bucket?: string): Promise<DownloadConfig> {
|
|
128
|
+
// Handle s3:// URLs
|
|
129
|
+
let resolvedPath = storagePath;
|
|
130
|
+
let resolvedBucket = this.getBucket(bucket);
|
|
131
|
+
|
|
132
|
+
if (storagePath.startsWith('s3://')) {
|
|
133
|
+
const withoutProtocol = storagePath.substring('s3://'.length);
|
|
134
|
+
const firstSlash = withoutProtocol.indexOf('/');
|
|
135
|
+
if (firstSlash > 0) {
|
|
136
|
+
resolvedBucket = withoutProtocol.substring(0, firstSlash);
|
|
137
|
+
resolvedPath = withoutProtocol.substring(firstSlash + 1);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
// First check if the object exists and get metadata
|
|
143
|
+
const headCommand = new HeadObjectCommand({
|
|
144
|
+
Bucket: resolvedBucket,
|
|
145
|
+
Key: resolvedPath
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const headResult = await this.client.send(headCommand);
|
|
149
|
+
|
|
150
|
+
// Generate a signed URL
|
|
151
|
+
const getCommand = new GetObjectCommand({
|
|
152
|
+
Bucket: resolvedBucket,
|
|
153
|
+
Key: resolvedPath
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const expiresIn = this.config.signedUrlExpiration ?? 3600;
|
|
157
|
+
const url = await getSignedUrl(this.client, getCommand, { expiresIn });
|
|
158
|
+
|
|
159
|
+
const metadata: DownloadMetadata = {
|
|
160
|
+
bucket: resolvedBucket,
|
|
161
|
+
fullPath: resolvedPath,
|
|
162
|
+
name: resolvedPath.split('/').pop() || resolvedPath,
|
|
163
|
+
size: headResult.ContentLength || 0,
|
|
164
|
+
contentType: headResult.ContentType || 'application/octet-stream',
|
|
165
|
+
customMetadata: headResult.Metadata || {}
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
url,
|
|
170
|
+
metadata
|
|
171
|
+
};
|
|
172
|
+
} catch (error: unknown) {
|
|
173
|
+
const s3Error = error as { name?: string; $metadata?: { httpStatusCode?: number } };
|
|
174
|
+
if (s3Error.name === 'NotFound' || s3Error.$metadata?.httpStatusCode === 404) {
|
|
175
|
+
return {
|
|
176
|
+
url: null,
|
|
177
|
+
fileNotFound: true
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
throw error;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async getFile(storagePath: string, bucket?: string): Promise<File | null> {
|
|
185
|
+
// Handle s3:// URLs
|
|
186
|
+
let resolvedPath = storagePath;
|
|
187
|
+
let resolvedBucket = this.getBucket(bucket);
|
|
188
|
+
|
|
189
|
+
if (storagePath.startsWith('s3://')) {
|
|
190
|
+
const withoutProtocol = storagePath.substring('s3://'.length);
|
|
191
|
+
const firstSlash = withoutProtocol.indexOf('/');
|
|
192
|
+
if (firstSlash > 0) {
|
|
193
|
+
resolvedBucket = withoutProtocol.substring(0, firstSlash);
|
|
194
|
+
resolvedPath = withoutProtocol.substring(firstSlash + 1);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
const command = new GetObjectCommand({
|
|
200
|
+
Bucket: resolvedBucket,
|
|
201
|
+
Key: resolvedPath
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const response = await this.client.send(command);
|
|
205
|
+
|
|
206
|
+
if (!response.Body) {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Convert stream to buffer
|
|
211
|
+
const chunks: Uint8Array[] = [];
|
|
212
|
+
// @ts-ignore - Body is a ReadableStream in Node.js
|
|
213
|
+
for await (const chunk of response.Body) {
|
|
214
|
+
chunks.push(chunk);
|
|
215
|
+
}
|
|
216
|
+
const buffer = Buffer.concat(chunks);
|
|
217
|
+
|
|
218
|
+
const contentType = response.ContentType || 'application/octet-stream';
|
|
219
|
+
const fileName = resolvedPath.split('/').pop() || resolvedPath;
|
|
220
|
+
|
|
221
|
+
const blob = new Blob([buffer], { type: contentType });
|
|
222
|
+
return new File([blob], fileName, { type: contentType });
|
|
223
|
+
} catch (error: unknown) {
|
|
224
|
+
const s3Error = error as { name?: string; $metadata?: { httpStatusCode?: number } };
|
|
225
|
+
if (s3Error.name === 'NoSuchKey' || s3Error.$metadata?.httpStatusCode === 404) {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
throw error;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async deleteFile(storagePath: string, bucket?: string): Promise<void> {
|
|
233
|
+
// Handle s3:// URLs
|
|
234
|
+
let resolvedPath = storagePath;
|
|
235
|
+
let resolvedBucket = this.getBucket(bucket);
|
|
236
|
+
|
|
237
|
+
if (storagePath.startsWith('s3://')) {
|
|
238
|
+
const withoutProtocol = storagePath.substring('s3://'.length);
|
|
239
|
+
const firstSlash = withoutProtocol.indexOf('/');
|
|
240
|
+
if (firstSlash > 0) {
|
|
241
|
+
resolvedBucket = withoutProtocol.substring(0, firstSlash);
|
|
242
|
+
resolvedPath = withoutProtocol.substring(firstSlash + 1);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const command = new DeleteObjectCommand({
|
|
247
|
+
Bucket: resolvedBucket,
|
|
248
|
+
Key: resolvedPath
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
await this.client.send(command);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async list(storagePath: string, options?: {
|
|
255
|
+
bucket?: string;
|
|
256
|
+
maxResults?: number;
|
|
257
|
+
pageToken?: string;
|
|
258
|
+
}): Promise<StorageListResult> {
|
|
259
|
+
const resolvedBucket = this.getBucket(options?.bucket);
|
|
260
|
+
|
|
261
|
+
const command = new ListObjectsV2Command({
|
|
262
|
+
Bucket: resolvedBucket,
|
|
263
|
+
Prefix: storagePath || undefined,
|
|
264
|
+
MaxKeys: options?.maxResults ?? 1000,
|
|
265
|
+
ContinuationToken: options?.pageToken,
|
|
266
|
+
Delimiter: '/' // This gives us folder-like behavior
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
const response = await this.client.send(command);
|
|
270
|
+
|
|
271
|
+
const items: StorageReference[] = (response.Contents || []).map(obj => ({
|
|
272
|
+
bucket: resolvedBucket,
|
|
273
|
+
fullPath: obj.Key || '',
|
|
274
|
+
name: (obj.Key || '').split('/').pop() || '',
|
|
275
|
+
parent: null as never,
|
|
276
|
+
root: null as never,
|
|
277
|
+
toString: () => `s3://${resolvedBucket}/${obj.Key}`
|
|
278
|
+
}));
|
|
279
|
+
|
|
280
|
+
const prefixes: StorageReference[] = (response.CommonPrefixes || []).map(prefix => ({
|
|
281
|
+
bucket: resolvedBucket,
|
|
282
|
+
fullPath: prefix.Prefix || '',
|
|
283
|
+
name: (prefix.Prefix || '').replace(/\/$/, '').split('/').pop() || '',
|
|
284
|
+
parent: null as never,
|
|
285
|
+
root: null as never,
|
|
286
|
+
toString: () => `s3://${resolvedBucket}/${prefix.Prefix}`
|
|
287
|
+
}));
|
|
288
|
+
|
|
289
|
+
return {
|
|
290
|
+
items,
|
|
291
|
+
prefixes,
|
|
292
|
+
nextPageToken: response.NextContinuationToken
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storage module for Rebase backend
|
|
3
|
+
*
|
|
4
|
+
* Provides file storage functionality with support for:
|
|
5
|
+
* - Local filesystem storage (default, zero config)
|
|
6
|
+
* - S3-compatible storage (AWS S3, MinIO)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export * from './types';
|
|
10
|
+
export { LocalStorageController } from './LocalStorageController';
|
|
11
|
+
export { S3StorageController } from './S3StorageController';
|
|
12
|
+
export { createStorageRoutes } from './routes';
|
|
13
|
+
export type { StorageRoutesConfig } from './routes';
|
|
14
|
+
export * from './storage-registry';
|
|
15
|
+
|
|
16
|
+
import { BackendStorageConfig, StorageController } from './types';
|
|
17
|
+
import { LocalStorageController } from './LocalStorageController';
|
|
18
|
+
import { S3StorageController } from './S3StorageController';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Factory function to create a storage controller based on configuration
|
|
22
|
+
*/
|
|
23
|
+
export function createStorageController(config: BackendStorageConfig): StorageController {
|
|
24
|
+
switch (config.type) {
|
|
25
|
+
case 'local':
|
|
26
|
+
return new LocalStorageController(config);
|
|
27
|
+
case 's3':
|
|
28
|
+
return new S3StorageController(config);
|
|
29
|
+
default:
|
|
30
|
+
throw new Error(`Unknown storage type: ${(config as Record<string, unknown>).type}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storage REST API routes using Hono
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Hono } from 'hono';
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import { StorageController } from './types';
|
|
8
|
+
import { LocalStorageController } from './LocalStorageController';
|
|
9
|
+
import { requireAuth as jwtRequireAuth, optionalAuth } from '../auth/middleware';
|
|
10
|
+
import { ApiError } from '../api/errors';
|
|
11
|
+
import { HonoEnv } from '../api/types';
|
|
12
|
+
|
|
13
|
+
export interface StorageRoutesConfig {
|
|
14
|
+
controller: StorageController;
|
|
15
|
+
/** Base path for storage routes (default: '/api/storage') */
|
|
16
|
+
basePath?: string;
|
|
17
|
+
/** Require authentication for write operations (default: true) */
|
|
18
|
+
requireAuth?: boolean;
|
|
19
|
+
/** Allow unauthenticated read access to stored files (default: false).
|
|
20
|
+
* When false and requireAuth is true, reads also require authentication. */
|
|
21
|
+
publicRead?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Extract the wildcard portion of a route path from the full request path.
|
|
26
|
+
*
|
|
27
|
+
* Hono's `c.req.param('*')` does not work reliably in sub-routers mounted
|
|
28
|
+
* via `app.route(prefix, subRouter)`. Instead we derive the wildcard value
|
|
29
|
+
* from the fully-resolved `c.req.path` and `c.req.routePath`.
|
|
30
|
+
*
|
|
31
|
+
* For a route `/metadata/*` mounted at `/api/storage`, a request to
|
|
32
|
+
* `/api/storage/metadata/default/file.jpg` yields routePath
|
|
33
|
+
* `/api/storage/metadata/*`. We strip the prefix (everything before `/*`)
|
|
34
|
+
* plus one character for the trailing `/` to obtain `default/file.jpg`.
|
|
35
|
+
*/
|
|
36
|
+
export function extractWildcardPath(c: { req: { path: string; routePath: string } }): string {
|
|
37
|
+
const routePath = c.req.routePath; // e.g. "/api/storage/metadata/*"
|
|
38
|
+
const prefix = routePath.replace('/*', ''); // e.g. "/api/storage/metadata"
|
|
39
|
+
const fullPath = c.req.path; // e.g. "/api/storage/metadata/default/file.jpg"
|
|
40
|
+
const idx = fullPath.indexOf(prefix);
|
|
41
|
+
if (idx < 0) return '';
|
|
42
|
+
// +1 to skip the '/' after the prefix
|
|
43
|
+
return fullPath.substring(idx + prefix.length + 1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Create storage REST API routes
|
|
48
|
+
*/
|
|
49
|
+
export function createStorageRoutes(config: StorageRoutesConfig): Hono<HonoEnv> {
|
|
50
|
+
const router = new Hono<HonoEnv>();
|
|
51
|
+
const { controller, requireAuth = true, publicRead = false } = config;
|
|
52
|
+
|
|
53
|
+
// Use actual JWT auth middleware from auth module
|
|
54
|
+
const writeAuthMiddleware = requireAuth ? jwtRequireAuth : optionalAuth;
|
|
55
|
+
|
|
56
|
+
// For read operations: respect publicRead config.
|
|
57
|
+
const readAuthMiddleware = (publicRead || !requireAuth) ? optionalAuth : jwtRequireAuth;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Parse bucket and path from a combined file path.
|
|
61
|
+
*/
|
|
62
|
+
const parseBucketAndPath = (filePath: string): { bucket: string; resolvedPath: string } => {
|
|
63
|
+
const parts = filePath.split('/');
|
|
64
|
+
|
|
65
|
+
// Only recognize 'default' as an explicit bucket prefix
|
|
66
|
+
if (parts.length > 1 && parts[0].toLowerCase() === 'default') {
|
|
67
|
+
return {
|
|
68
|
+
bucket: 'default',
|
|
69
|
+
resolvedPath: parts.slice(1).join('/')
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// All other paths use 'default' bucket with the full path
|
|
74
|
+
return {
|
|
75
|
+
bucket: 'default',
|
|
76
|
+
resolvedPath: filePath
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* POST /upload - Upload a file
|
|
82
|
+
* Body: multipart/form-data with 'file' field
|
|
83
|
+
* Request body can also contain metadata keys 'metadata_*'
|
|
84
|
+
*/
|
|
85
|
+
router.post('/upload', writeAuthMiddleware, async (c) => {
|
|
86
|
+
const body = await c.req.parseBody();
|
|
87
|
+
const uploadedFile = body['file'];
|
|
88
|
+
|
|
89
|
+
if (!uploadedFile || typeof uploadedFile === 'string') {
|
|
90
|
+
throw ApiError.badRequest('No file provided');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const storagePath = typeof body['path'] === 'string' ? body['path'] : '';
|
|
94
|
+
const bucket = typeof body['bucket'] === 'string' ? body['bucket'] : undefined;
|
|
95
|
+
let fileName = typeof body['fileName'] === 'string' ? body['fileName'] : undefined;
|
|
96
|
+
|
|
97
|
+
if (!fileName) {
|
|
98
|
+
fileName = uploadedFile.name;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Extract custom metadata from request body
|
|
102
|
+
const metadata: Record<string, unknown> = {};
|
|
103
|
+
for (const [key, value] of Object.entries(body)) {
|
|
104
|
+
if (key.startsWith('metadata_')) {
|
|
105
|
+
metadata[key.replace('metadata_', '')] = value;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const result = await controller.uploadFile({
|
|
110
|
+
file: uploadedFile,
|
|
111
|
+
fileName: fileName || 'unnamed',
|
|
112
|
+
path: storagePath,
|
|
113
|
+
metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
|
|
114
|
+
bucket
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
return c.json({
|
|
118
|
+
success: true,
|
|
119
|
+
data: result
|
|
120
|
+
}, 201);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* GET /file/* - Download/serve a file
|
|
125
|
+
* Path: /file/{bucket}/{path} or /file/{path}
|
|
126
|
+
*/
|
|
127
|
+
router.get('/file/*', readAuthMiddleware, async (c) => {
|
|
128
|
+
const rawPath = extractWildcardPath(c);
|
|
129
|
+
if (!rawPath) {
|
|
130
|
+
throw ApiError.notFound('File not found');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const filePath = decodeURIComponent(rawPath);
|
|
134
|
+
|
|
135
|
+
// For local storage, serve the file directly
|
|
136
|
+
if (controller.getType() === 'local') {
|
|
137
|
+
const localController = controller as LocalStorageController;
|
|
138
|
+
const { bucket, resolvedPath } = parseBucketAndPath(filePath);
|
|
139
|
+
|
|
140
|
+
const absolutePath = localController.getAbsolutePath(resolvedPath, bucket);
|
|
141
|
+
|
|
142
|
+
// Check if file exists
|
|
143
|
+
if (!fs.existsSync(absolutePath)) {
|
|
144
|
+
throw ApiError.notFound('File not found');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Get content type from metadata or infer from extension
|
|
148
|
+
let contentType = 'application/octet-stream';
|
|
149
|
+
const metadataPath = `${absolutePath}.metadata.json`;
|
|
150
|
+
if (fs.existsSync(metadataPath)) {
|
|
151
|
+
try {
|
|
152
|
+
const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf-8'));
|
|
153
|
+
contentType = metadata.contentType || contentType;
|
|
154
|
+
} catch {
|
|
155
|
+
// Ignore metadata errors
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
c.header('Content-Type', contentType);
|
|
160
|
+
// In a better scenario, we should pipe the stream instead of reading whole file
|
|
161
|
+
const fileContent = fs.readFileSync(absolutePath);
|
|
162
|
+
return c.body(new Uint8Array(fileContent));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// For S3 storage, redirect to signed URL
|
|
166
|
+
const downloadConfig = await controller.getDownloadURL(filePath);
|
|
167
|
+
if (downloadConfig.fileNotFound || !downloadConfig.url) {
|
|
168
|
+
throw ApiError.notFound('File not found');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return c.redirect(downloadConfig.url);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* GET /metadata/* - Get file metadata
|
|
176
|
+
*/
|
|
177
|
+
router.get('/metadata/*', readAuthMiddleware, async (c) => {
|
|
178
|
+
const rawPath = extractWildcardPath(c);
|
|
179
|
+
if (!rawPath) {
|
|
180
|
+
return c.json({
|
|
181
|
+
success: true,
|
|
182
|
+
data: null,
|
|
183
|
+
fileNotFound: true
|
|
184
|
+
}, 404);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const filePath = decodeURIComponent(rawPath);
|
|
188
|
+
const { bucket, resolvedPath } = parseBucketAndPath(filePath);
|
|
189
|
+
|
|
190
|
+
const downloadConfig = await controller.getDownloadURL(resolvedPath, bucket);
|
|
191
|
+
|
|
192
|
+
if (downloadConfig.fileNotFound) {
|
|
193
|
+
throw ApiError.notFound('File not found');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return c.json({
|
|
197
|
+
success: true,
|
|
198
|
+
data: downloadConfig.metadata
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* DELETE /file/* - Delete a file
|
|
204
|
+
*/
|
|
205
|
+
router.delete('/file/*', writeAuthMiddleware, async (c) => {
|
|
206
|
+
const rawPath = extractWildcardPath(c);
|
|
207
|
+
if (!rawPath) {
|
|
208
|
+
return c.json({ success: true, message: 'No file to delete' });
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const filePath = decodeURIComponent(rawPath);
|
|
212
|
+
const { bucket, resolvedPath } = parseBucketAndPath(filePath);
|
|
213
|
+
|
|
214
|
+
await controller.deleteFile(resolvedPath, bucket);
|
|
215
|
+
|
|
216
|
+
return c.json({
|
|
217
|
+
success: true,
|
|
218
|
+
message: 'File deleted'
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* GET /list - List files in a path
|
|
224
|
+
*/
|
|
225
|
+
router.get('/list', writeAuthMiddleware, async (c) => {
|
|
226
|
+
const storagePath = c.req.query('path') || '';
|
|
227
|
+
const bucket = c.req.query('bucket');
|
|
228
|
+
const maxResults = c.req.query('maxResults');
|
|
229
|
+
const pageToken = c.req.query('pageToken');
|
|
230
|
+
|
|
231
|
+
const result = await controller.list(
|
|
232
|
+
storagePath,
|
|
233
|
+
{
|
|
234
|
+
bucket,
|
|
235
|
+
maxResults: maxResults ? parseInt(maxResults, 10) : undefined,
|
|
236
|
+
pageToken
|
|
237
|
+
}
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
return c.json({
|
|
241
|
+
success: true,
|
|
242
|
+
data: result
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
return router;
|
|
247
|
+
}
|