@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.
Files changed (254) hide show
  1. package/LICENSE +6 -0
  2. package/README.md +40 -0
  3. package/build-errors.txt +52 -0
  4. package/coverage/clover.xml +3739 -0
  5. package/coverage/coverage-final.json +31 -0
  6. package/coverage/lcov-report/base.css +224 -0
  7. package/coverage/lcov-report/block-navigation.js +87 -0
  8. package/coverage/lcov-report/favicon.png +0 -0
  9. package/coverage/lcov-report/index.html +266 -0
  10. package/coverage/lcov-report/prettify.css +1 -0
  11. package/coverage/lcov-report/prettify.js +2 -0
  12. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  13. package/coverage/lcov-report/sorter.js +210 -0
  14. package/coverage/lcov-report/src/api/ast-schema-editor.ts.html +952 -0
  15. package/coverage/lcov-report/src/api/errors.ts.html +472 -0
  16. package/coverage/lcov-report/src/api/graphql/graphql-schema-generator.ts.html +1069 -0
  17. package/coverage/lcov-report/src/api/graphql/index.html +116 -0
  18. package/coverage/lcov-report/src/api/index.html +176 -0
  19. package/coverage/lcov-report/src/api/openapi-generator.ts.html +565 -0
  20. package/coverage/lcov-report/src/api/rest/api-generator.ts.html +994 -0
  21. package/coverage/lcov-report/src/api/rest/index.html +131 -0
  22. package/coverage/lcov-report/src/api/rest/query-parser.ts.html +550 -0
  23. package/coverage/lcov-report/src/api/schema-editor-routes.ts.html +202 -0
  24. package/coverage/lcov-report/src/api/server.ts.html +823 -0
  25. package/coverage/lcov-report/src/auth/admin-routes.ts.html +973 -0
  26. package/coverage/lcov-report/src/auth/index.html +176 -0
  27. package/coverage/lcov-report/src/auth/jwt.ts.html +574 -0
  28. package/coverage/lcov-report/src/auth/middleware.ts.html +745 -0
  29. package/coverage/lcov-report/src/auth/password.ts.html +310 -0
  30. package/coverage/lcov-report/src/auth/services.ts.html +2074 -0
  31. package/coverage/lcov-report/src/collections/index.html +116 -0
  32. package/coverage/lcov-report/src/collections/loader.ts.html +232 -0
  33. package/coverage/lcov-report/src/db/auth-schema.ts.html +523 -0
  34. package/coverage/lcov-report/src/db/data-transformer.ts.html +1753 -0
  35. package/coverage/lcov-report/src/db/entityService.ts.html +700 -0
  36. package/coverage/lcov-report/src/db/index.html +146 -0
  37. package/coverage/lcov-report/src/db/services/EntityFetchService.ts.html +4048 -0
  38. package/coverage/lcov-report/src/db/services/EntityPersistService.ts.html +883 -0
  39. package/coverage/lcov-report/src/db/services/RelationService.ts.html +3121 -0
  40. package/coverage/lcov-report/src/db/services/entity-helpers.ts.html +442 -0
  41. package/coverage/lcov-report/src/db/services/index.html +176 -0
  42. package/coverage/lcov-report/src/db/services/index.ts.html +124 -0
  43. package/coverage/lcov-report/src/generate-drizzle-schema-logic.ts.html +1960 -0
  44. package/coverage/lcov-report/src/index.html +116 -0
  45. package/coverage/lcov-report/src/services/driver-registry.ts.html +631 -0
  46. package/coverage/lcov-report/src/services/index.html +131 -0
  47. package/coverage/lcov-report/src/services/postgresDataDriver.ts.html +3025 -0
  48. package/coverage/lcov-report/src/storage/LocalStorageController.ts.html +1189 -0
  49. package/coverage/lcov-report/src/storage/S3StorageController.ts.html +970 -0
  50. package/coverage/lcov-report/src/storage/index.html +161 -0
  51. package/coverage/lcov-report/src/storage/storage-registry.ts.html +646 -0
  52. package/coverage/lcov-report/src/storage/types.ts.html +451 -0
  53. package/coverage/lcov-report/src/utils/drizzle-conditions.ts.html +3082 -0
  54. package/coverage/lcov-report/src/utils/index.html +116 -0
  55. package/coverage/lcov.info +7179 -0
  56. package/dist/common/src/collections/CollectionRegistry.d.ts +48 -0
  57. package/dist/common/src/collections/index.d.ts +1 -0
  58. package/dist/common/src/data/buildRebaseData.d.ts +14 -0
  59. package/dist/common/src/index.d.ts +3 -0
  60. package/dist/common/src/util/builders.d.ts +57 -0
  61. package/dist/common/src/util/callbacks.d.ts +6 -0
  62. package/dist/common/src/util/collections.d.ts +11 -0
  63. package/dist/common/src/util/common.d.ts +2 -0
  64. package/dist/common/src/util/conditions.d.ts +26 -0
  65. package/dist/common/src/util/entities.d.ts +36 -0
  66. package/dist/common/src/util/enums.d.ts +3 -0
  67. package/dist/common/src/util/index.d.ts +16 -0
  68. package/dist/common/src/util/navigation_from_path.d.ts +34 -0
  69. package/dist/common/src/util/navigation_utils.d.ts +20 -0
  70. package/dist/common/src/util/parent_references_from_path.d.ts +6 -0
  71. package/dist/common/src/util/paths.d.ts +14 -0
  72. package/dist/common/src/util/permissions.d.ts +5 -0
  73. package/dist/common/src/util/references.d.ts +2 -0
  74. package/dist/common/src/util/relations.d.ts +12 -0
  75. package/dist/common/src/util/resolutions.d.ts +72 -0
  76. package/dist/common/src/util/storage.d.ts +24 -0
  77. package/dist/index-BeMqpmfQ.js +239 -0
  78. package/dist/index-BeMqpmfQ.js.map +1 -0
  79. package/dist/index-bl4J3lNb.js +55823 -0
  80. package/dist/index-bl4J3lNb.js.map +1 -0
  81. package/dist/index.es.js +58 -0
  82. package/dist/index.es.js.map +1 -0
  83. package/dist/index.umd.js +56062 -0
  84. package/dist/index.umd.js.map +1 -0
  85. package/dist/server-core/src/api/ast-schema-editor.d.ts +21 -0
  86. package/dist/server-core/src/api/collections_for_test/callbacks_test_collection.d.ts +2 -0
  87. package/dist/server-core/src/api/errors.d.ts +35 -0
  88. package/dist/server-core/src/api/graphql/graphql-schema-generator.d.ts +35 -0
  89. package/dist/server-core/src/api/graphql/index.d.ts +1 -0
  90. package/dist/server-core/src/api/index.d.ts +9 -0
  91. package/dist/server-core/src/api/openapi-generator.d.ts +2 -0
  92. package/dist/server-core/src/api/rest/api-generator.d.ts +64 -0
  93. package/dist/server-core/src/api/rest/index.d.ts +1 -0
  94. package/dist/server-core/src/api/rest/query-parser.d.ts +9 -0
  95. package/dist/server-core/src/api/schema-editor-routes.d.ts +3 -0
  96. package/dist/server-core/src/api/server.d.ts +40 -0
  97. package/dist/server-core/src/api/types.d.ts +90 -0
  98. package/dist/server-core/src/auth/admin-routes.d.ts +7 -0
  99. package/dist/server-core/src/auth/google-oauth.d.ts +20 -0
  100. package/dist/server-core/src/auth/index.d.ts +12 -0
  101. package/dist/server-core/src/auth/interfaces.d.ts +270 -0
  102. package/dist/server-core/src/auth/jwt.d.ts +42 -0
  103. package/dist/server-core/src/auth/middleware.d.ts +56 -0
  104. package/dist/server-core/src/auth/password.d.ts +22 -0
  105. package/dist/server-core/src/auth/rate-limiter.d.ts +31 -0
  106. package/dist/server-core/src/auth/routes.d.ts +17 -0
  107. package/dist/server-core/src/bootstrappers/index.d.ts +0 -0
  108. package/dist/server-core/src/collections/BackendCollectionRegistry.d.ts +13 -0
  109. package/dist/server-core/src/collections/loader.d.ts +5 -0
  110. package/dist/server-core/src/db/interfaces.d.ts +18 -0
  111. package/dist/server-core/src/email/index.d.ts +6 -0
  112. package/dist/server-core/src/email/smtp-email-service.d.ts +25 -0
  113. package/dist/server-core/src/email/templates.d.ts +33 -0
  114. package/dist/server-core/src/email/types.d.ts +110 -0
  115. package/dist/server-core/src/functions/function-loader.d.ts +17 -0
  116. package/dist/server-core/src/functions/function-routes.d.ts +10 -0
  117. package/dist/server-core/src/functions/index.d.ts +3 -0
  118. package/dist/server-core/src/history/history-routes.d.ts +23 -0
  119. package/dist/server-core/src/history/index.d.ts +1 -0
  120. package/dist/server-core/src/index.d.ts +24 -0
  121. package/dist/server-core/src/init.d.ts +49 -0
  122. package/dist/server-core/src/serve-spa.d.ts +30 -0
  123. package/dist/server-core/src/services/driver-registry.d.ts +78 -0
  124. package/dist/server-core/src/storage/LocalStorageController.d.ts +46 -0
  125. package/dist/server-core/src/storage/S3StorageController.d.ts +36 -0
  126. package/dist/server-core/src/storage/index.d.ts +18 -0
  127. package/dist/server-core/src/storage/routes.d.ts +38 -0
  128. package/dist/server-core/src/storage/storage-registry.d.ts +78 -0
  129. package/dist/server-core/src/storage/types.d.ts +91 -0
  130. package/dist/server-core/src/types/index.d.ts +11 -0
  131. package/dist/server-core/src/utils/logging.d.ts +9 -0
  132. package/dist/server-core/src/utils/sql.d.ts +27 -0
  133. package/dist/types/src/controllers/analytics_controller.d.ts +7 -0
  134. package/dist/types/src/controllers/auth.d.ts +117 -0
  135. package/dist/types/src/controllers/client.d.ts +58 -0
  136. package/dist/types/src/controllers/collection_registry.d.ts +44 -0
  137. package/dist/types/src/controllers/customization_controller.d.ts +54 -0
  138. package/dist/types/src/controllers/data.d.ts +141 -0
  139. package/dist/types/src/controllers/data_driver.d.ts +168 -0
  140. package/dist/types/src/controllers/database_admin.d.ts +11 -0
  141. package/dist/types/src/controllers/dialogs_controller.d.ts +36 -0
  142. package/dist/types/src/controllers/effective_role.d.ts +4 -0
  143. package/dist/types/src/controllers/index.d.ts +17 -0
  144. package/dist/types/src/controllers/local_config_persistence.d.ts +20 -0
  145. package/dist/types/src/controllers/navigation.d.ts +213 -0
  146. package/dist/types/src/controllers/registry.d.ts +51 -0
  147. package/dist/types/src/controllers/side_dialogs_controller.d.ts +67 -0
  148. package/dist/types/src/controllers/side_entity_controller.d.ts +89 -0
  149. package/dist/types/src/controllers/snackbar.d.ts +24 -0
  150. package/dist/types/src/controllers/storage.d.ts +173 -0
  151. package/dist/types/src/index.d.ts +4 -0
  152. package/dist/types/src/rebase_context.d.ts +101 -0
  153. package/dist/types/src/types/backend.d.ts +533 -0
  154. package/dist/types/src/types/builders.d.ts +14 -0
  155. package/dist/types/src/types/chips.d.ts +5 -0
  156. package/dist/types/src/types/collections.d.ts +812 -0
  157. package/dist/types/src/types/data_source.d.ts +64 -0
  158. package/dist/types/src/types/entities.d.ts +145 -0
  159. package/dist/types/src/types/entity_actions.d.ts +98 -0
  160. package/dist/types/src/types/entity_callbacks.d.ts +173 -0
  161. package/dist/types/src/types/entity_link_builder.d.ts +7 -0
  162. package/dist/types/src/types/entity_overrides.d.ts +9 -0
  163. package/dist/types/src/types/entity_views.d.ts +61 -0
  164. package/dist/types/src/types/export_import.d.ts +21 -0
  165. package/dist/types/src/types/index.d.ts +22 -0
  166. package/dist/types/src/types/locales.d.ts +4 -0
  167. package/dist/types/src/types/modify_collections.d.ts +5 -0
  168. package/dist/types/src/types/plugins.d.ts +225 -0
  169. package/dist/types/src/types/properties.d.ts +1091 -0
  170. package/dist/types/src/types/property_config.d.ts +70 -0
  171. package/dist/types/src/types/relations.d.ts +336 -0
  172. package/dist/types/src/types/slots.d.ts +228 -0
  173. package/dist/types/src/types/translations.d.ts +826 -0
  174. package/dist/types/src/types/user_management_delegate.d.ts +120 -0
  175. package/dist/types/src/types/websockets.d.ts +78 -0
  176. package/dist/types/src/users/index.d.ts +2 -0
  177. package/dist/types/src/users/roles.d.ts +22 -0
  178. package/dist/types/src/users/user.d.ts +46 -0
  179. package/history_diff.log +385 -0
  180. package/jest.config.cjs +16 -0
  181. package/package.json +86 -0
  182. package/scratch.ts +8 -0
  183. package/src/api/ast-schema-editor.ts +289 -0
  184. package/src/api/collections_for_test/callbacks_test_collection.ts +57 -0
  185. package/src/api/errors.ts +155 -0
  186. package/src/api/graphql/graphql-schema-generator.ts +334 -0
  187. package/src/api/graphql/index.ts +2 -0
  188. package/src/api/index.ts +11 -0
  189. package/src/api/openapi-generator.ts +160 -0
  190. package/src/api/rest/api-generator.ts +466 -0
  191. package/src/api/rest/index.ts +2 -0
  192. package/src/api/rest/query-parser.ts +155 -0
  193. package/src/api/schema-editor-routes.ts +39 -0
  194. package/src/api/server.ts +245 -0
  195. package/src/api/types.ts +90 -0
  196. package/src/auth/admin-routes.ts +488 -0
  197. package/src/auth/google-oauth.ts +60 -0
  198. package/src/auth/index.ts +21 -0
  199. package/src/auth/interfaces.ts +316 -0
  200. package/src/auth/jwt.ts +164 -0
  201. package/src/auth/middleware.ts +235 -0
  202. package/src/auth/password.ts +75 -0
  203. package/src/auth/rate-limiter.ts +129 -0
  204. package/src/auth/routes.ts +730 -0
  205. package/src/bootstrappers/index.ts +1 -0
  206. package/src/collections/BackendCollectionRegistry.ts +20 -0
  207. package/src/collections/loader.ts +49 -0
  208. package/src/db/interfaces.ts +60 -0
  209. package/src/email/index.ts +17 -0
  210. package/src/email/smtp-email-service.ts +88 -0
  211. package/src/email/templates.ts +301 -0
  212. package/src/email/types.ts +112 -0
  213. package/src/functions/function-loader.ts +91 -0
  214. package/src/functions/function-routes.ts +31 -0
  215. package/src/functions/index.ts +3 -0
  216. package/src/history/history-routes.ts +128 -0
  217. package/src/history/index.ts +2 -0
  218. package/src/index.ts +56 -0
  219. package/src/init.ts +309 -0
  220. package/src/serve-spa.ts +81 -0
  221. package/src/services/driver-registry.ts +182 -0
  222. package/src/storage/LocalStorageController.ts +368 -0
  223. package/src/storage/S3StorageController.ts +295 -0
  224. package/src/storage/index.ts +32 -0
  225. package/src/storage/routes.ts +247 -0
  226. package/src/storage/storage-registry.ts +187 -0
  227. package/src/storage/types.ts +122 -0
  228. package/src/types/index.ts +27 -0
  229. package/src/utils/logging.ts +35 -0
  230. package/src/utils/sql.ts +38 -0
  231. package/test/admin-routes.test.ts +591 -0
  232. package/test/api-generator.test.ts +458 -0
  233. package/test/ast-schema-editor.test.ts +61 -0
  234. package/test/auth-middleware-hono.test.ts +321 -0
  235. package/test/auth-routes.test.ts +868 -0
  236. package/test/driver-registry.test.ts +280 -0
  237. package/test/errors-hono.test.ts +133 -0
  238. package/test/errors.test.ts +150 -0
  239. package/test/jwt-security.test.ts +173 -0
  240. package/test/jwt.test.ts +311 -0
  241. package/test/middleware.test.ts +295 -0
  242. package/test/password.test.ts +165 -0
  243. package/test/query-parser.test.ts +258 -0
  244. package/test/rate-limiter.test.ts +102 -0
  245. package/test/storage-local.test.ts +278 -0
  246. package/test/storage-registry.test.ts +280 -0
  247. package/test/storage-routes.test.ts +218 -0
  248. package/test/storage-s3.test.ts +301 -0
  249. package/test-ast.ts +28 -0
  250. package/test_output.txt +1133 -0
  251. package/tsconfig.json +49 -0
  252. package/tsconfig.prod.json +20 -0
  253. package/vite.config.ts +78 -0
  254. 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
+ }