@rebasepro/server-core 0.0.1-canary.09e5ec5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (300) 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 +56 -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 +58 -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 +22 -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-DXVBFp5V.js +37 -0
  78. package/dist/index-DXVBFp5V.js.map +1 -0
  79. package/dist/index.es.js +49934 -0
  80. package/dist/index.es.js.map +1 -0
  81. package/dist/index.umd.js +49968 -0
  82. package/dist/index.umd.js.map +1 -0
  83. package/dist/server-core/src/api/ast-schema-editor.d.ts +21 -0
  84. package/dist/server-core/src/api/collections_for_test/callbacks_test_collection.d.ts +2 -0
  85. package/dist/server-core/src/api/errors.d.ts +35 -0
  86. package/dist/server-core/src/api/graphql/graphql-schema-generator.d.ts +35 -0
  87. package/dist/server-core/src/api/graphql/index.d.ts +1 -0
  88. package/dist/server-core/src/api/index.d.ts +9 -0
  89. package/dist/server-core/src/api/openapi-generator.d.ts +16 -0
  90. package/dist/server-core/src/api/rest/api-generator.d.ts +64 -0
  91. package/dist/server-core/src/api/rest/index.d.ts +1 -0
  92. package/dist/server-core/src/api/rest/query-parser.d.ts +9 -0
  93. package/dist/server-core/src/api/schema-editor-routes.d.ts +3 -0
  94. package/dist/server-core/src/api/server.d.ts +40 -0
  95. package/dist/server-core/src/api/types.d.ts +90 -0
  96. package/dist/server-core/src/auth/admin-routes.d.ts +16 -0
  97. package/dist/server-core/src/auth/apple-oauth.d.ts +30 -0
  98. package/dist/server-core/src/auth/bitbucket-oauth.d.ts +11 -0
  99. package/dist/server-core/src/auth/discord-oauth.d.ts +14 -0
  100. package/dist/server-core/src/auth/facebook-oauth.d.ts +14 -0
  101. package/dist/server-core/src/auth/github-oauth.d.ts +15 -0
  102. package/dist/server-core/src/auth/gitlab-oauth.d.ts +13 -0
  103. package/dist/server-core/src/auth/google-oauth.d.ts +14 -0
  104. package/dist/server-core/src/auth/index.d.ts +23 -0
  105. package/dist/server-core/src/auth/interfaces.d.ts +309 -0
  106. package/dist/server-core/src/auth/jwt.d.ts +43 -0
  107. package/dist/server-core/src/auth/linkedin-oauth.d.ts +18 -0
  108. package/dist/server-core/src/auth/microsoft-oauth.d.ts +16 -0
  109. package/dist/server-core/src/auth/middleware.d.ts +81 -0
  110. package/dist/server-core/src/auth/password.d.ts +22 -0
  111. package/dist/server-core/src/auth/rate-limiter.d.ts +31 -0
  112. package/dist/server-core/src/auth/routes.d.ts +27 -0
  113. package/dist/server-core/src/auth/slack-oauth.d.ts +12 -0
  114. package/dist/server-core/src/auth/spotify-oauth.d.ts +12 -0
  115. package/dist/server-core/src/auth/twitter-oauth.d.ts +18 -0
  116. package/dist/server-core/src/bootstrappers/index.d.ts +0 -0
  117. package/dist/server-core/src/collections/BackendCollectionRegistry.d.ts +13 -0
  118. package/dist/server-core/src/collections/loader.d.ts +5 -0
  119. package/dist/server-core/src/cron/cron-loader.d.ts +17 -0
  120. package/dist/server-core/src/cron/cron-routes.d.ts +14 -0
  121. package/dist/server-core/src/cron/cron-scheduler.d.ts +61 -0
  122. package/dist/server-core/src/cron/cron-store.d.ts +32 -0
  123. package/dist/server-core/src/cron/index.d.ts +6 -0
  124. package/dist/server-core/src/db/interfaces.d.ts +18 -0
  125. package/dist/server-core/src/email/index.d.ts +6 -0
  126. package/dist/server-core/src/email/smtp-email-service.d.ts +25 -0
  127. package/dist/server-core/src/email/templates.d.ts +42 -0
  128. package/dist/server-core/src/email/types.d.ts +107 -0
  129. package/dist/server-core/src/functions/function-loader.d.ts +17 -0
  130. package/dist/server-core/src/functions/function-routes.d.ts +10 -0
  131. package/dist/server-core/src/functions/index.d.ts +3 -0
  132. package/dist/server-core/src/history/history-routes.d.ts +23 -0
  133. package/dist/server-core/src/history/index.d.ts +1 -0
  134. package/dist/server-core/src/index.d.ts +29 -0
  135. package/dist/server-core/src/init.d.ts +159 -0
  136. package/dist/server-core/src/serve-spa.d.ts +30 -0
  137. package/dist/server-core/src/services/driver-registry.d.ts +78 -0
  138. package/dist/server-core/src/singleton.d.ts +35 -0
  139. package/dist/server-core/src/storage/LocalStorageController.d.ts +46 -0
  140. package/dist/server-core/src/storage/S3StorageController.d.ts +36 -0
  141. package/dist/server-core/src/storage/index.d.ts +25 -0
  142. package/dist/server-core/src/storage/routes.d.ts +38 -0
  143. package/dist/server-core/src/storage/storage-registry.d.ts +78 -0
  144. package/dist/server-core/src/storage/types.d.ts +103 -0
  145. package/dist/server-core/src/types/index.d.ts +11 -0
  146. package/dist/server-core/src/utils/dev-port.d.ts +35 -0
  147. package/dist/server-core/src/utils/logger.d.ts +31 -0
  148. package/dist/server-core/src/utils/logging.d.ts +9 -0
  149. package/dist/server-core/src/utils/request-logger.d.ts +19 -0
  150. package/dist/server-core/src/utils/sql.d.ts +27 -0
  151. package/dist/types/src/controllers/analytics_controller.d.ts +7 -0
  152. package/dist/types/src/controllers/auth.d.ts +119 -0
  153. package/dist/types/src/controllers/client.d.ts +170 -0
  154. package/dist/types/src/controllers/collection_registry.d.ts +45 -0
  155. package/dist/types/src/controllers/customization_controller.d.ts +60 -0
  156. package/dist/types/src/controllers/data.d.ts +168 -0
  157. package/dist/types/src/controllers/data_driver.d.ts +160 -0
  158. package/dist/types/src/controllers/database_admin.d.ts +11 -0
  159. package/dist/types/src/controllers/dialogs_controller.d.ts +36 -0
  160. package/dist/types/src/controllers/effective_role.d.ts +4 -0
  161. package/dist/types/src/controllers/email.d.ts +34 -0
  162. package/dist/types/src/controllers/index.d.ts +18 -0
  163. package/dist/types/src/controllers/local_config_persistence.d.ts +20 -0
  164. package/dist/types/src/controllers/navigation.d.ts +213 -0
  165. package/dist/types/src/controllers/registry.d.ts +54 -0
  166. package/dist/types/src/controllers/side_dialogs_controller.d.ts +67 -0
  167. package/dist/types/src/controllers/side_entity_controller.d.ts +90 -0
  168. package/dist/types/src/controllers/snackbar.d.ts +24 -0
  169. package/dist/types/src/controllers/storage.d.ts +171 -0
  170. package/dist/types/src/index.d.ts +4 -0
  171. package/dist/types/src/rebase_context.d.ts +105 -0
  172. package/dist/types/src/types/backend.d.ts +536 -0
  173. package/dist/types/src/types/builders.d.ts +15 -0
  174. package/dist/types/src/types/chips.d.ts +5 -0
  175. package/dist/types/src/types/collections.d.ts +856 -0
  176. package/dist/types/src/types/cron.d.ts +102 -0
  177. package/dist/types/src/types/data_source.d.ts +64 -0
  178. package/dist/types/src/types/entities.d.ts +145 -0
  179. package/dist/types/src/types/entity_actions.d.ts +98 -0
  180. package/dist/types/src/types/entity_callbacks.d.ts +173 -0
  181. package/dist/types/src/types/entity_link_builder.d.ts +7 -0
  182. package/dist/types/src/types/entity_overrides.d.ts +10 -0
  183. package/dist/types/src/types/entity_views.d.ts +61 -0
  184. package/dist/types/src/types/export_import.d.ts +21 -0
  185. package/dist/types/src/types/index.d.ts +23 -0
  186. package/dist/types/src/types/locales.d.ts +4 -0
  187. package/dist/types/src/types/modify_collections.d.ts +5 -0
  188. package/dist/types/src/types/plugins.d.ts +279 -0
  189. package/dist/types/src/types/properties.d.ts +1176 -0
  190. package/dist/types/src/types/property_config.d.ts +70 -0
  191. package/dist/types/src/types/relations.d.ts +336 -0
  192. package/dist/types/src/types/slots.d.ts +252 -0
  193. package/dist/types/src/types/translations.d.ts +870 -0
  194. package/dist/types/src/types/user_management_delegate.d.ts +121 -0
  195. package/dist/types/src/types/websockets.d.ts +78 -0
  196. package/dist/types/src/users/index.d.ts +2 -0
  197. package/dist/types/src/users/roles.d.ts +22 -0
  198. package/dist/types/src/users/user.d.ts +46 -0
  199. package/history_diff.log +385 -0
  200. package/jest.config.cjs +16 -0
  201. package/package.json +86 -0
  202. package/scratch.ts +9 -0
  203. package/src/api/ast-schema-editor.ts +289 -0
  204. package/src/api/collections_for_test/callbacks_test_collection.ts +60 -0
  205. package/src/api/errors.ts +179 -0
  206. package/src/api/graphql/graphql-schema-generator.ts +336 -0
  207. package/src/api/graphql/index.ts +2 -0
  208. package/src/api/index.ts +11 -0
  209. package/src/api/openapi-generator.ts +715 -0
  210. package/src/api/rest/api-generator.ts +472 -0
  211. package/src/api/rest/index.ts +2 -0
  212. package/src/api/rest/query-parser.ts +155 -0
  213. package/src/api/schema-editor-routes.ts +41 -0
  214. package/src/api/server.ts +248 -0
  215. package/src/api/types.ts +90 -0
  216. package/src/auth/admin-routes.ts +529 -0
  217. package/src/auth/apple-oauth.ts +130 -0
  218. package/src/auth/bitbucket-oauth.ts +82 -0
  219. package/src/auth/discord-oauth.ts +83 -0
  220. package/src/auth/facebook-oauth.ts +72 -0
  221. package/src/auth/github-oauth.ts +110 -0
  222. package/src/auth/gitlab-oauth.ts +70 -0
  223. package/src/auth/google-oauth.ts +48 -0
  224. package/src/auth/index.ts +34 -0
  225. package/src/auth/interfaces.ts +363 -0
  226. package/src/auth/jwt.ts +181 -0
  227. package/src/auth/linkedin-oauth.ts +81 -0
  228. package/src/auth/microsoft-oauth.ts +88 -0
  229. package/src/auth/middleware.ts +384 -0
  230. package/src/auth/password.ts +77 -0
  231. package/src/auth/rate-limiter.ts +129 -0
  232. package/src/auth/routes.ts +788 -0
  233. package/src/auth/slack-oauth.ts +71 -0
  234. package/src/auth/spotify-oauth.ts +67 -0
  235. package/src/auth/twitter-oauth.ts +120 -0
  236. package/src/bootstrappers/index.ts +1 -0
  237. package/src/collections/BackendCollectionRegistry.ts +20 -0
  238. package/src/collections/loader.ts +49 -0
  239. package/src/cron/cron-loader.ts +89 -0
  240. package/src/cron/cron-routes.test.ts +265 -0
  241. package/src/cron/cron-routes.ts +85 -0
  242. package/src/cron/cron-scheduler.test.ts +421 -0
  243. package/src/cron/cron-scheduler.ts +413 -0
  244. package/src/cron/cron-store.ts +163 -0
  245. package/src/cron/index.ts +6 -0
  246. package/src/db/interfaces.ts +60 -0
  247. package/src/email/index.ts +18 -0
  248. package/src/email/smtp-email-service.ts +91 -0
  249. package/src/email/templates.ts +388 -0
  250. package/src/email/types.ts +105 -0
  251. package/src/functions/function-loader.ts +119 -0
  252. package/src/functions/function-routes.ts +31 -0
  253. package/src/functions/index.ts +3 -0
  254. package/src/history/history-routes.ts +129 -0
  255. package/src/history/index.ts +2 -0
  256. package/src/index.ts +66 -0
  257. package/src/init.ts +727 -0
  258. package/src/serve-spa.ts +81 -0
  259. package/src/services/driver-registry.ts +182 -0
  260. package/src/singleton.test.ts +28 -0
  261. package/src/singleton.ts +70 -0
  262. package/src/storage/LocalStorageController.ts +365 -0
  263. package/src/storage/S3StorageController.ts +298 -0
  264. package/src/storage/index.ts +43 -0
  265. package/src/storage/routes.ts +264 -0
  266. package/src/storage/storage-registry.ts +187 -0
  267. package/src/storage/types.ts +134 -0
  268. package/src/types/index.ts +27 -0
  269. package/src/utils/dev-port.ts +176 -0
  270. package/src/utils/logger.ts +143 -0
  271. package/src/utils/logging.ts +38 -0
  272. package/src/utils/request-logger.ts +66 -0
  273. package/src/utils/sql.ts +38 -0
  274. package/test/admin-routes.test.ts +640 -0
  275. package/test/api-generator.test.ts +501 -0
  276. package/test/ast-schema-editor.test.ts +63 -0
  277. package/test/auth-middleware-hono.test.ts +556 -0
  278. package/test/auth-routes.test.ts +1047 -0
  279. package/test/driver-registry.test.ts +282 -0
  280. package/test/error-propagation.test.ts +226 -0
  281. package/test/errors-hono.test.ts +133 -0
  282. package/test/errors.test.ts +155 -0
  283. package/test/jwt-security.test.ts +182 -0
  284. package/test/jwt.test.ts +324 -0
  285. package/test/middleware.test.ts +300 -0
  286. package/test/password.test.ts +165 -0
  287. package/test/query-parser.test.ts +263 -0
  288. package/test/rate-limiter.test.ts +102 -0
  289. package/test/safe-compare.test.ts +66 -0
  290. package/test/singleton.test.ts +59 -0
  291. package/test/storage-local.test.ts +271 -0
  292. package/test/storage-registry.test.ts +282 -0
  293. package/test/storage-routes.test.ts +222 -0
  294. package/test/storage-s3.test.ts +304 -0
  295. package/test-ast.ts +28 -0
  296. package/test.ts +6 -0
  297. package/test_output.txt +1133 -0
  298. package/tsconfig.json +49 -0
  299. package/tsconfig.prod.json +20 -0
  300. package/vite.config.ts +80 -0
@@ -0,0 +1,472 @@
1
+ import { Hono } from "hono";
2
+ import { DataDriver, Entity, EntityCollection, FetchCollectionProps } from "@rebasepro/types";
3
+ import { QueryOptions, HonoEnv } from "../types";
4
+ import { ApiError } from "../errors";
5
+ import { parseQueryOptions } from "./query-parser";
6
+
7
+
8
+ /**
9
+ * Lightweight REST API generator that leverages existing Rebase DataDriver.
10
+ * Supports `include` query parameter for eager-loading relations via Drizzle.
11
+ */
12
+ export class RestApiGenerator {
13
+ private collections: EntityCollection[];
14
+ private router: Hono<HonoEnv>;
15
+ private driver: DataDriver;
16
+
17
+ constructor(collections: EntityCollection[], driver: DataDriver) {
18
+ this.collections = collections;
19
+ this.driver = driver;
20
+ this.router = new Hono<HonoEnv>();
21
+ }
22
+
23
+ /**
24
+ * Generate REST routes using existing DataDriver
25
+ */
26
+ generateRoutes(): Hono<HonoEnv> {
27
+ this.collections.forEach(collection => {
28
+ this.createCollectionRoutes(collection);
29
+ });
30
+
31
+ // Catch-all routes for subcollection paths like
32
+ // /authors/111094/posts and /authors/111094/posts/43
33
+ // The DataDriver already knows how to resolve nested relation paths.
34
+ this.createSubcollectionRoutes();
35
+
36
+ return this.router;
37
+ }
38
+
39
+ /**
40
+ * Get the EntityFetchService from a driver if it exposes one (for include support)
41
+ */
42
+ private getFetchService(driver: DataDriver): Record<string, (...args: unknown[]) => unknown> | null {
43
+ if ("entityService" in driver && typeof driver.entityService === "object" && driver.entityService) {
44
+ const es = driver.entityService as Record<string, unknown>;
45
+ if (typeof es.getFetchService === "function") {
46
+ return es.getFetchService();
47
+ }
48
+ }
49
+ return null;
50
+ }
51
+
52
+ /**
53
+ * Create REST routes for a collection using existing Rebase patterns
54
+ */
55
+ private createCollectionRoutes(collection: EntityCollection): void {
56
+ const basePath = `/${collection.slug}`;
57
+ const resolvedCollection = collection;
58
+
59
+ // GET /collection - List entities
60
+ this.router.get(basePath, async (c) => {
61
+ const queryDict = c.req.query();
62
+ const queryOptions = parseQueryOptions(queryDict);
63
+ const searchString = queryDict.searchString as string | undefined;
64
+
65
+ const driver = c.get("driver") || this.driver;
66
+ const fetchService = this.getFetchService(driver);
67
+
68
+ // Use include-aware path when available
69
+ if (fetchService) {
70
+ const collectionPath = collection.slug;
71
+ const entities = await fetchService.fetchCollectionForRest(
72
+ collectionPath,
73
+ {
74
+ filter: queryOptions.where as FetchCollectionProps["filter"],
75
+ limit: queryOptions.limit,
76
+ offset: queryOptions.offset,
77
+ orderBy: queryOptions.orderBy?.[0]?.field,
78
+ order: queryOptions.orderBy?.[0]?.direction === "desc" ? "desc" : "asc",
79
+ searchString
80
+ },
81
+ queryOptions.include
82
+ );
83
+
84
+ const total = await this.countRawEntities(driver, resolvedCollection, queryOptions, searchString);
85
+
86
+ return c.json({
87
+ data: entities,
88
+ meta: {
89
+ total,
90
+ limit: queryOptions.limit,
91
+ offset: queryOptions.offset,
92
+ hasMore: (queryOptions.offset || 0) + (entities as unknown[]).length < total
93
+ }
94
+ });
95
+ }
96
+
97
+ // Fallback path
98
+ const entities = await this.fetchRawCollection(driver, resolvedCollection, queryOptions, searchString);
99
+
100
+ const total = await this.countRawEntities(driver, resolvedCollection, queryOptions, searchString);
101
+
102
+ return c.json({
103
+ data: entities,
104
+ meta: {
105
+ total,
106
+ limit: queryOptions.limit,
107
+ offset: queryOptions.offset,
108
+ hasMore: (queryOptions.offset || 0) + (entities as unknown[]).length < total
109
+ }
110
+ });
111
+ });
112
+
113
+ // GET /collection/:id - Get single entity
114
+ this.router.get(`${basePath}/:id`, async (c) => {
115
+ const id = c.req.param("id");
116
+ const queryDict = c.req.query();
117
+ const queryOptions = parseQueryOptions(queryDict);
118
+ const driver = c.get("driver") || this.driver;
119
+ const fetchService = this.getFetchService(driver);
120
+
121
+ // Use include-aware path when available
122
+ if (fetchService) {
123
+ const collectionPath = collection.slug;
124
+ const entity = await fetchService.fetchEntityForRest(
125
+ collectionPath,
126
+ String(id),
127
+ queryOptions.include
128
+ );
129
+
130
+ if (!entity) {
131
+ throw ApiError.notFound("Entity not found");
132
+ }
133
+
134
+ return c.json(entity);
135
+ }
136
+
137
+ // Fallback
138
+ const entity = await this.fetchRawEntity(driver, resolvedCollection, String(id));
139
+
140
+ if (!entity) {
141
+ throw ApiError.notFound("Entity not found");
142
+ }
143
+
144
+ return c.json(entity);
145
+ });
146
+
147
+ // POST /collection - Create entity
148
+ this.router.post(basePath, async (c) => {
149
+ try {
150
+ const driver = c.get("driver") || this.driver;
151
+ const path = collection.slug;
152
+
153
+ const body = await c.req.json().catch(() => ({}));
154
+
155
+ const entity = await driver.saveEntity({
156
+ path,
157
+ values: body,
158
+ collection: resolvedCollection,
159
+ status: "new"
160
+ });
161
+
162
+ return c.json(this.formatResponse(entity), 201);
163
+ } catch (error) {
164
+ const err = error as Error & { code?: string };
165
+ err.code = err.code || "BAD_REQUEST";
166
+ throw err;
167
+ }
168
+ });
169
+
170
+ // PUT /collection/:id - Update entity
171
+ this.router.put(`${basePath}/:id`, async (c) => {
172
+ try {
173
+ const id = c.req.param("id");
174
+ const driver = c.get("driver") || this.driver;
175
+
176
+ const existingEntity = await driver.fetchEntity({
177
+ path: collection.slug,
178
+ entityId: String(id),
179
+ collection: resolvedCollection
180
+ });
181
+
182
+ if (!existingEntity) {
183
+ throw ApiError.notFound("Entity not found");
184
+ }
185
+
186
+ const body = await c.req.json().catch(() => ({}));
187
+
188
+ const entity = await driver.saveEntity({
189
+ path: collection.slug,
190
+ entityId: String(id),
191
+ values: body,
192
+ collection: resolvedCollection,
193
+ status: "existing"
194
+ });
195
+
196
+ return c.json(this.formatResponse(entity));
197
+ } catch (error) {
198
+ const err = error as Error & { code?: string };
199
+ err.code = err.code || "BAD_REQUEST";
200
+ throw err;
201
+ }
202
+ });
203
+
204
+ // DELETE /collection/:id - Delete entity
205
+ this.router.delete(`${basePath}/:id`, async (c) => {
206
+ const id = c.req.param("id");
207
+ const driver = c.get("driver") || this.driver;
208
+
209
+ const existingEntity = await driver.fetchEntity({
210
+ path: collection.slug,
211
+ entityId: String(id),
212
+ collection: resolvedCollection
213
+ });
214
+
215
+ if (!existingEntity) {
216
+ throw ApiError.notFound("Entity not found");
217
+ }
218
+
219
+ await driver.deleteEntity({
220
+ entity: existingEntity,
221
+ collection: resolvedCollection
222
+ });
223
+
224
+ return new Response(null, { status: 204 });
225
+ });
226
+ }
227
+
228
+ /**
229
+ * Catch-all routes for subcollection paths.
230
+ *
231
+ * Matches URL patterns like:
232
+ * GET /authors/111094/posts → list child collection
233
+ * GET /authors/111094/posts/43 → get child entity
234
+ * POST /authors/111094/posts → create child entity
235
+ * PUT /authors/111094/posts/43 → update child entity
236
+ * DELETE /authors/111094/posts/43 → delete child entity
237
+ *
238
+ * The `:rest{.+}` regex param captures the full remainder of the URL
239
+ * path (Hono v4 `*` wildcard does not populate `c.req.param("*")`).
240
+ * We split it into segments and reconstruct the `collectionPath`
241
+ * (e.g. "authors/111094/posts") and optional `entityId` (e.g. "43").
242
+ *
243
+ * The DataDriver.saveEntity / fetchCollection / etc. already know how to
244
+ * resolve multi-segment relation paths, so we just forward to them.
245
+ */
246
+ private createSubcollectionRoutes(): void {
247
+ // Reserved path segments that should NOT be treated as relation names.
248
+ // These are handled by dedicated route handlers (e.g., history routes)
249
+ // mounted on the same data router.
250
+ const RESERVED_SEGMENTS = new Set(["history"]);
251
+
252
+ // Helper: parse a path like "authors/111094/posts/43" into
253
+ // { collectionPath: "authors/111094/posts", entityId: "43" }
254
+ // or "authors/111094/posts" into
255
+ // { collectionPath: "authors/111094/posts", entityId: undefined }
256
+ const parseSubPath = (rawPath: string): { collectionPath: string; entityId?: string } | null => {
257
+ const segments = rawPath.split("/").filter(s => s && s !== "undefined");
258
+ // Need at least 3 segments for a subcollection path (parent/id/child)
259
+ if (segments.length < 3) return null;
260
+
261
+ // If any segment is a reserved path (e.g. "history"), this is not a
262
+ // subcollection route — let it fall through to other handlers.
263
+ if (segments.some(s => RESERVED_SEGMENTS.has(s))) return null;
264
+
265
+ // Odd segment count → collection path (parent/id/child or parent/id/child/id2/grandchild)
266
+ // Even segment count → entity path (parent/id/child/entityId)
267
+ if (segments.length % 2 === 1) {
268
+ return { collectionPath: segments.join("/") };
269
+ } else {
270
+ const entityId = segments.pop()!;
271
+ return { collectionPath: segments.join("/"),
272
+ entityId };
273
+ }
274
+ };
275
+
276
+ // GET /<subcollection-path> — list or get single entity
277
+ // Use :rest{.+} instead of * because Hono v4's wildcard doesn't
278
+ // capture into c.req.param("*") — it always returns undefined.
279
+ this.router.get("/:parent/:parentId/:rest{.+}", async (c, next) => {
280
+ const rest = c.req.param("rest");
281
+ if (!rest || rest === "undefined") return next();
282
+ const rawPath = `${c.req.param("parent")}/${c.req.param("parentId")}/${rest}`;
283
+ const parsed = parseSubPath(rawPath);
284
+ if (!parsed) return next();
285
+
286
+ const driver = c.get("driver") || this.driver;
287
+
288
+ if (parsed.entityId) {
289
+ // GET /parent/:parentId/child/:id — single entity
290
+ const entity = await driver.fetchEntity({
291
+ path: parsed.collectionPath,
292
+ entityId: parsed.entityId
293
+ });
294
+ if (!entity) throw ApiError.notFound("Entity not found");
295
+ return c.json(this.flattenEntity(entity));
296
+ } else {
297
+ // GET /parent/:parentId/child — list entities
298
+ const queryDict = c.req.query();
299
+ const queryOptions = parseQueryOptions(queryDict);
300
+ const entities = await driver.fetchCollection({
301
+ path: parsed.collectionPath,
302
+ filter: queryOptions.where as FetchCollectionProps["filter"],
303
+ limit: queryOptions.limit,
304
+ orderBy: queryOptions.orderBy?.[0]?.field,
305
+ order: queryOptions.orderBy?.[0]?.direction === "desc" ? "desc" : "asc",
306
+ searchString: queryDict.searchString as string | undefined
307
+ });
308
+ return c.json({
309
+ data: entities.map(e => this.flattenEntity(e)),
310
+ meta: {
311
+ total: entities.length,
312
+ limit: queryOptions.limit,
313
+ offset: queryOptions.offset,
314
+ hasMore: false
315
+ }
316
+ });
317
+ }
318
+ });
319
+
320
+ // POST /<subcollection-path> — create entity
321
+ this.router.post("/:parent/:parentId/:rest{.+}", async (c, next) => {
322
+ const rest = c.req.param("rest");
323
+ if (!rest || rest === "undefined") return next();
324
+ const rawPath = `${c.req.param("parent")}/${c.req.param("parentId")}/${rest}`;
325
+ const parsed = parseSubPath(rawPath);
326
+ if (!parsed || parsed.entityId) return next();
327
+
328
+ const driver = c.get("driver") || this.driver;
329
+ const body = await c.req.json().catch(() => ({}));
330
+
331
+ const entity = await driver.saveEntity({
332
+ path: parsed.collectionPath,
333
+ values: body,
334
+ status: "new"
335
+ });
336
+
337
+ return c.json(this.formatResponse(entity), 201);
338
+ });
339
+
340
+ // PUT /<subcollection-path>/:id — update entity
341
+ this.router.put("/:parent/:parentId/:rest{.+}", async (c, next) => {
342
+ const rest = c.req.param("rest");
343
+ if (!rest || rest === "undefined") return next();
344
+ const rawPath = `${c.req.param("parent")}/${c.req.param("parentId")}/${rest}`;
345
+ const parsed = parseSubPath(rawPath);
346
+ if (!parsed || !parsed.entityId) return next();
347
+
348
+ const driver = c.get("driver") || this.driver;
349
+ const body = await c.req.json().catch(() => ({}));
350
+
351
+ const entity = await driver.saveEntity({
352
+ path: parsed.collectionPath,
353
+ entityId: parsed.entityId,
354
+ values: body,
355
+ status: "existing"
356
+ });
357
+
358
+ return c.json(this.formatResponse(entity));
359
+ });
360
+
361
+ // DELETE /<subcollection-path>/:id — delete entity
362
+ this.router.delete("/:parent/:parentId/:rest{.+}", async (c, next) => {
363
+ const rest = c.req.param("rest");
364
+ if (!rest || rest === "undefined") return next();
365
+ const rawPath = `${c.req.param("parent")}/${c.req.param("parentId")}/${rest}`;
366
+ const parsed = parseSubPath(rawPath);
367
+ if (!parsed || !parsed.entityId) return next();
368
+
369
+ const driver = c.get("driver") || this.driver;
370
+
371
+ const existingEntity = await driver.fetchEntity({
372
+ path: parsed.collectionPath,
373
+ entityId: parsed.entityId
374
+ });
375
+
376
+ if (!existingEntity) throw ApiError.notFound("Entity not found");
377
+
378
+ await driver.deleteEntity({ entity: existingEntity });
379
+
380
+ return new Response(null, { status: 204 });
381
+ });
382
+ }
383
+
384
+ /**
385
+ * Format successful API response - flattened for traditional REST API
386
+ */
387
+ private formatResponse<T>(data: T, meta?: Record<string, unknown>): unknown {
388
+ if (Array.isArray(data)) {
389
+ const flattenedData = data.map(entity => this.flattenEntity(entity));
390
+ if (meta) {
391
+ return {
392
+ data: flattenedData,
393
+ meta
394
+ };
395
+ }
396
+ return flattenedData;
397
+ }
398
+
399
+ if (data && typeof data === "object" && "values" in data) {
400
+ return this.flattenEntity(data as unknown as Entity<Record<string, unknown>>);
401
+ }
402
+
403
+ if (meta) {
404
+ return {
405
+ data,
406
+ meta
407
+ };
408
+ }
409
+ return data;
410
+ }
411
+
412
+ /**
413
+ * Flatten Rebase entity structure to traditional REST format
414
+ */
415
+ private flattenEntity(entity: Entity<Record<string, unknown>>): Record<string, unknown> {
416
+ if (!entity || typeof entity !== "object") {
417
+ return entity;
418
+ }
419
+
420
+ if ("values" in entity && typeof entity.values === "object") {
421
+ return {
422
+ id: entity.id,
423
+ ...entity.values
424
+ };
425
+ }
426
+
427
+ return entity as unknown as Record<string, unknown>;
428
+ }
429
+
430
+ /**
431
+ * Fetch raw collection data without Entity wrapper (fallback for non-Postgres)
432
+ */
433
+ private async fetchRawCollection(driver: DataDriver, collection: EntityCollection, queryOptions: QueryOptions, searchString?: string) {
434
+ const entities = await driver.fetchCollection({
435
+ path: collection.slug,
436
+ collection,
437
+ filter: queryOptions.where as FetchCollectionProps["filter"],
438
+ limit: queryOptions.limit,
439
+ orderBy: queryOptions.orderBy?.[0]?.field,
440
+ order: queryOptions.orderBy?.[0]?.direction === "desc" ? "desc" : "asc",
441
+ startAfter: queryOptions.offset ? String(queryOptions.offset) : undefined,
442
+ searchString
443
+ });
444
+
445
+ return entities.map(entity => this.flattenEntity(entity));
446
+ }
447
+
448
+ /**
449
+ * Count raw entities for a collection
450
+ */
451
+ private async countRawEntities(driver: DataDriver, collection: EntityCollection, queryOptions: QueryOptions, searchString?: string): Promise<number> {
452
+ return driver.countEntities ? await driver.countEntities({
453
+ path: collection.slug,
454
+ collection,
455
+ filter: queryOptions.where as FetchCollectionProps["filter"],
456
+ searchString
457
+ }) : 0;
458
+ }
459
+
460
+ /**
461
+ * Fetch single entity raw data without Entity wrapper (fallback)
462
+ */
463
+ private async fetchRawEntity(driver: DataDriver, collection: EntityCollection, entityId: string) {
464
+ const entity = await driver.fetchEntity({
465
+ path: collection.slug,
466
+ entityId,
467
+ collection
468
+ });
469
+
470
+ return entity ? this.flattenEntity(entity) : null;
471
+ }
472
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./api-generator";
2
+
@@ -0,0 +1,155 @@
1
+ import { QueryOptions } from "../types";
2
+
3
+ /**
4
+ * Map PostgREST-style operators to Rebase WhereFilterOp
5
+ */
6
+ export function mapOperator(op: string): string | null {
7
+ switch (op) {
8
+ case "eq": return "==";
9
+ case "neq": return "!=";
10
+ case "gt": return ">";
11
+ case "gte": return ">=";
12
+ case "lt": return "<";
13
+ case "lte": return "<=";
14
+ case "in": return "in";
15
+ case "nin": return "not-in";
16
+ case "cs": return "array-contains";
17
+ case "csa": return "array-contains-any";
18
+ default: return null;
19
+ }
20
+ }
21
+
22
+ /**
23
+ * Parse query parameters into QueryOptions
24
+ */
25
+ export function parseQueryOptions(query: Record<string, unknown>): QueryOptions {
26
+ const options: QueryOptions = {};
27
+
28
+ // Pagination
29
+ if (query.limit) options.limit = parseInt(String(query.limit));
30
+ if (query.offset) options.offset = parseInt(String(query.offset));
31
+ if (query.page) {
32
+ const page = parseInt(String(query.page));
33
+ const limit = options.limit || 20;
34
+ options.offset = (page - 1) * limit;
35
+ }
36
+
37
+ // Filtering
38
+ options.where = {};
39
+
40
+ // Legacy JSON where clause
41
+ if (query.where) {
42
+ try {
43
+ const parsedWhere = typeof query.where === "string"
44
+ ? JSON.parse(query.where)
45
+ : query.where;
46
+ if (typeof parsedWhere !== "object" || parsedWhere === null || Array.isArray(parsedWhere)) {
47
+ throw new Error("Filter must be a JSON object");
48
+ }
49
+ Object.assign(options.where, parsedWhere);
50
+ } catch (e) {
51
+ const message = e instanceof Error ? e.message : "malformed JSON";
52
+ const err = new Error(`Invalid 'where' filter: ${message}`) as Error & { code?: string; statusCode?: number };
53
+ err.code = "BAD_REQUEST";
54
+ err.statusCode = 400;
55
+ throw err;
56
+ }
57
+ }
58
+
59
+ // PostgREST style filtering
60
+ const reservedQueryKeys = ["limit", "offset", "page", "orderBy", "where", "include", "fields", "searchString"];
61
+ for (const [key, rawValue] of Object.entries(query)) {
62
+ if (reservedQueryKeys.includes(key)) continue;
63
+
64
+ const value = Array.isArray(rawValue) ? rawValue[rawValue.length - 1] : rawValue;
65
+
66
+ if (typeof value === "string") {
67
+ const parts = value.split(".");
68
+ if (parts.length >= 2) {
69
+ const op = parts[0];
70
+ const val = parts.slice(1).join(".");
71
+ const rebaseOp = mapOperator(op);
72
+
73
+ if (rebaseOp) {
74
+ let parsedVal: string | number | boolean | null | (string | number | boolean | null)[] = val;
75
+ // Attempt to parse primitive types or arrays
76
+ if (val === "true") parsedVal = true;
77
+ else if (val === "false") parsedVal = false;
78
+ else if (val === "null") parsedVal = null;
79
+ else if (!isNaN(Number(val)) && val.trim() !== "") parsedVal = Number(val);
80
+ else if (val.startsWith("(")) {
81
+ // Array for 'in' or 'not-in' ops (e.g. (1,2,3) or (a,b,c))
82
+ const arrayContent = val.endsWith(")") ? val.slice(1, -1) : val.slice(1);
83
+ parsedVal = arrayContent.split(",").map(v => {
84
+ const trimmed = v.trim();
85
+ if (!isNaN(Number(trimmed)) && trimmed !== "") return Number(trimmed);
86
+ if (trimmed === "true") return true;
87
+ if (trimmed === "false") return false;
88
+ if (trimmed === "null") return null;
89
+ return trimmed;
90
+ });
91
+ }
92
+
93
+ options.where[key] = [rebaseOp, parsedVal];
94
+ } else {
95
+ // Fallback: assume implicit eq if the dot wasn't an operator (e.g. email or float)
96
+ let parsedVal: string | number | boolean | null = value;
97
+ if (!isNaN(Number(value)) && value.trim() !== "") parsedVal = Number(value);
98
+ options.where[key] = ["==", parsedVal];
99
+ }
100
+ } else {
101
+ // Implicit eq
102
+ let parsedVal: string | number | boolean | null = value;
103
+ if (value === "true") parsedVal = true;
104
+ else if (value === "false") parsedVal = false;
105
+ else if (value === "null") parsedVal = null;
106
+ else if (!isNaN(Number(value)) && value.trim() !== "") parsedVal = Number(value);
107
+
108
+ options.where[key] = ["==", parsedVal];
109
+ }
110
+ }
111
+ }
112
+
113
+ if (Object.keys(options.where).length === 0) {
114
+ delete options.where;
115
+ }
116
+
117
+ // Sorting
118
+ if (query.orderBy) {
119
+ try {
120
+ options.orderBy = typeof query.orderBy === "string"
121
+ ? JSON.parse(query.orderBy)
122
+ : query.orderBy;
123
+ } catch {
124
+ // Try simple format: "field:direction"
125
+ if (typeof query.orderBy === "string") {
126
+ const [field, direction] = query.orderBy.split(":");
127
+ const dir = (direction === "desc" ? "desc" : "asc") as "asc" | "desc";
128
+ options.orderBy = [
129
+ {
130
+ field,
131
+ direction: dir
132
+ }
133
+ ];
134
+ }
135
+ }
136
+ }
137
+
138
+ // Relation includes
139
+ if (query.include) {
140
+ const includeStr = String(query.include).trim();
141
+ if (includeStr === "*") {
142
+ options.include = ["*"];
143
+ } else {
144
+ options.include = includeStr.split(",").map(s => s.trim()).filter(Boolean);
145
+ }
146
+ }
147
+
148
+ // Field selection
149
+ if (query.fields) {
150
+ const fieldsStr = String(query.fields).trim();
151
+ options.fields = fieldsStr.split(",").map(s => s.trim()).filter(Boolean);
152
+ }
153
+
154
+ return options;
155
+ }
@@ -0,0 +1,41 @@
1
+ import { Hono } from "hono";
2
+ import { AstSchemaEditor } from "./ast-schema-editor";
3
+ import { errorHandler } from "./errors";
4
+ import { HonoEnv } from "./types";
5
+
6
+ export function createSchemaEditorRoutes(collectionsDir: string): Hono<HonoEnv> {
7
+ const router = new Hono<HonoEnv>();
8
+ router.onError(errorHandler);
9
+ const editor = new AstSchemaEditor(collectionsDir);
10
+
11
+ router.post("/property/save", async (c) => {
12
+ const body = await c.req.json();
13
+ const { collectionId, propertyKey, propertyConfig } = body;
14
+ await editor.saveProperty(collectionId, propertyKey, propertyConfig);
15
+ return c.json({ success: true });
16
+ });
17
+
18
+ router.post("/property/delete", async (c) => {
19
+ const body = await c.req.json();
20
+ const { collectionId, propertyKey } = body;
21
+ await editor.deleteProperty(collectionId, propertyKey);
22
+ return c.json({ success: true });
23
+ });
24
+
25
+ router.post("/collection/save", async (c) => {
26
+ const body = await c.req.json();
27
+ const { collectionId, collectionData } = body;
28
+ await editor.saveCollection(collectionId, collectionData);
29
+ return c.json({ success: true });
30
+ });
31
+
32
+ router.post("/collection/delete", async (c) => {
33
+ const body = await c.req.json();
34
+ const { collectionId } = body;
35
+ await editor.deleteCollection(collectionId);
36
+ return c.json({ success: true });
37
+ });
38
+
39
+ return router;
40
+ }
41
+