@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,466 @@
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
+
64
+ const driver = c.get("driver") || this.driver;
65
+ const fetchService = this.getFetchService(driver);
66
+
67
+ // Use include-aware path when available
68
+ if (fetchService) {
69
+ const collectionPath = collection.slug;
70
+ const entities = await fetchService.fetchCollectionForRest(
71
+ collectionPath,
72
+ {
73
+ filter: queryOptions.where as FetchCollectionProps["filter"],
74
+ limit: queryOptions.limit,
75
+ orderBy: queryOptions.orderBy?.[0]?.field,
76
+ order: queryOptions.orderBy?.[0]?.direction === "desc" ? "desc" : "asc",
77
+ searchString: queryDict.searchString as string | undefined,
78
+ },
79
+ queryOptions.include
80
+ );
81
+
82
+ const total = await this.countRawEntities(driver, resolvedCollection, queryOptions);
83
+
84
+ return c.json({
85
+ data: entities,
86
+ meta: {
87
+ total,
88
+ limit: queryOptions.limit,
89
+ offset: queryOptions.offset,
90
+ hasMore: (queryOptions.offset || 0) + (entities as unknown[]).length < total
91
+ }
92
+ });
93
+ }
94
+
95
+ // Fallback for non-Postgres drivers
96
+ const entities = await this.fetchRawCollection(driver, resolvedCollection, queryOptions);
97
+ const total = await this.countRawEntities(driver, resolvedCollection, queryOptions);
98
+
99
+ return c.json({
100
+ data: entities,
101
+ meta: {
102
+ total,
103
+ limit: queryOptions.limit,
104
+ offset: queryOptions.offset,
105
+ hasMore: (queryOptions.offset || 0) + (entities as unknown[]).length < total
106
+ }
107
+ });
108
+ });
109
+
110
+ // GET /collection/:id - Get single entity
111
+ this.router.get(`${basePath}/:id`, async (c) => {
112
+ const id = c.req.param("id");
113
+ const queryDict = c.req.query();
114
+ const queryOptions = parseQueryOptions(queryDict);
115
+ const driver = c.get("driver") || this.driver;
116
+ const fetchService = this.getFetchService(driver);
117
+
118
+ // Use include-aware path when available
119
+ if (fetchService) {
120
+ const collectionPath = collection.slug;
121
+ const entity = await fetchService.fetchEntityForRest(
122
+ collectionPath,
123
+ String(id),
124
+ queryOptions.include
125
+ );
126
+
127
+ if (!entity) {
128
+ throw ApiError.notFound("Entity not found");
129
+ }
130
+
131
+ return c.json(entity);
132
+ }
133
+
134
+ // Fallback
135
+ const entity = await this.fetchRawEntity(driver, resolvedCollection, String(id));
136
+
137
+ if (!entity) {
138
+ throw ApiError.notFound("Entity not found");
139
+ }
140
+
141
+ return c.json(entity);
142
+ });
143
+
144
+ // POST /collection - Create entity
145
+ this.router.post(basePath, async (c) => {
146
+ try {
147
+ const driver = c.get("driver") || this.driver;
148
+ const path = collection.slug;
149
+
150
+ const body = await c.req.json().catch(() => ({}));
151
+
152
+ const entity = await driver.saveEntity({
153
+ path,
154
+ values: body,
155
+ collection: resolvedCollection,
156
+ status: "new"
157
+ });
158
+
159
+ return c.json(this.formatResponse(entity), 201);
160
+ } catch (error) {
161
+ const err = error as Error & { code?: string };
162
+ err.code = err.code || "BAD_REQUEST";
163
+ throw err;
164
+ }
165
+ });
166
+
167
+ // PUT /collection/:id - Update entity
168
+ this.router.put(`${basePath}/:id`, async (c) => {
169
+ try {
170
+ const id = c.req.param("id");
171
+ const driver = c.get("driver") || this.driver;
172
+
173
+ const existingEntity = await driver.fetchEntity({
174
+ path: collection.slug,
175
+ entityId: String(id),
176
+ collection: resolvedCollection
177
+ });
178
+
179
+ if (!existingEntity) {
180
+ throw ApiError.notFound("Entity not found");
181
+ }
182
+
183
+ const body = await c.req.json().catch(() => ({}));
184
+
185
+ const entity = await driver.saveEntity({
186
+ path: collection.slug,
187
+ entityId: String(id),
188
+ values: body,
189
+ collection: resolvedCollection,
190
+ status: "existing"
191
+ });
192
+
193
+ return c.json(this.formatResponse(entity));
194
+ } catch (error) {
195
+ const err = error as Error & { code?: string };
196
+ err.code = err.code || "BAD_REQUEST";
197
+ throw err;
198
+ }
199
+ });
200
+
201
+ // DELETE /collection/:id - Delete entity
202
+ this.router.delete(`${basePath}/:id`, async (c) => {
203
+ const id = c.req.param("id");
204
+ const driver = c.get("driver") || this.driver;
205
+
206
+ const existingEntity = await driver.fetchEntity({
207
+ path: collection.slug,
208
+ entityId: String(id),
209
+ collection: resolvedCollection
210
+ });
211
+
212
+ if (!existingEntity) {
213
+ throw ApiError.notFound("Entity not found");
214
+ }
215
+
216
+ await driver.deleteEntity({
217
+ entity: existingEntity,
218
+ collection: resolvedCollection
219
+ });
220
+
221
+ return new Response(null, { status: 204 });
222
+ });
223
+ }
224
+
225
+ /**
226
+ * Catch-all routes for subcollection paths.
227
+ *
228
+ * Matches URL patterns like:
229
+ * GET /authors/111094/posts → list child collection
230
+ * GET /authors/111094/posts/43 → get child entity
231
+ * POST /authors/111094/posts → create child entity
232
+ * PUT /authors/111094/posts/43 → update child entity
233
+ * DELETE /authors/111094/posts/43 → delete child entity
234
+ *
235
+ * The `:rest{.+}` regex param captures the full remainder of the URL
236
+ * path (Hono v4 `*` wildcard does not populate `c.req.param("*")`).
237
+ * We split it into segments and reconstruct the `collectionPath`
238
+ * (e.g. "authors/111094/posts") and optional `entityId` (e.g. "43").
239
+ *
240
+ * The DataDriver.saveEntity / fetchCollection / etc. already know how to
241
+ * resolve multi-segment relation paths, so we just forward to them.
242
+ */
243
+ private createSubcollectionRoutes(): void {
244
+ // Reserved path segments that should NOT be treated as relation names.
245
+ // These are handled by dedicated route handlers (e.g., history routes)
246
+ // mounted on the same data router.
247
+ const RESERVED_SEGMENTS = new Set(["history"]);
248
+
249
+ // Helper: parse a path like "authors/111094/posts/43" into
250
+ // { collectionPath: "authors/111094/posts", entityId: "43" }
251
+ // or "authors/111094/posts" into
252
+ // { collectionPath: "authors/111094/posts", entityId: undefined }
253
+ const parseSubPath = (rawPath: string): { collectionPath: string; entityId?: string } | null => {
254
+ const segments = rawPath.split("/").filter(s => s && s !== "undefined");
255
+ // Need at least 3 segments for a subcollection path (parent/id/child)
256
+ if (segments.length < 3) return null;
257
+
258
+ // If any segment is a reserved path (e.g. "history"), this is not a
259
+ // subcollection route — let it fall through to other handlers.
260
+ if (segments.some(s => RESERVED_SEGMENTS.has(s))) return null;
261
+
262
+ // Odd segment count → collection path (parent/id/child or parent/id/child/id2/grandchild)
263
+ // Even segment count → entity path (parent/id/child/entityId)
264
+ if (segments.length % 2 === 1) {
265
+ return { collectionPath: segments.join("/") };
266
+ } else {
267
+ const entityId = segments.pop()!;
268
+ return { collectionPath: segments.join("/"), entityId };
269
+ }
270
+ };
271
+
272
+ // GET /<subcollection-path> — list or get single entity
273
+ // Use :rest{.+} instead of * because Hono v4's wildcard doesn't
274
+ // capture into c.req.param("*") — it always returns undefined.
275
+ this.router.get("/:parent/:parentId/:rest{.+}", async (c, next) => {
276
+ const rest = c.req.param("rest");
277
+ if (!rest || rest === "undefined") return next();
278
+ const rawPath = `${c.req.param("parent")}/${c.req.param("parentId")}/${rest}`;
279
+ const parsed = parseSubPath(rawPath);
280
+ if (!parsed) return next();
281
+
282
+ const driver = c.get("driver") || this.driver;
283
+
284
+ if (parsed.entityId) {
285
+ // GET /parent/:parentId/child/:id — single entity
286
+ const entity = await driver.fetchEntity({
287
+ path: parsed.collectionPath,
288
+ entityId: parsed.entityId
289
+ });
290
+ if (!entity) throw ApiError.notFound("Entity not found");
291
+ return c.json(this.flattenEntity(entity));
292
+ } else {
293
+ // GET /parent/:parentId/child — list entities
294
+ const queryDict = c.req.query();
295
+ const queryOptions = parseQueryOptions(queryDict);
296
+ const entities = await driver.fetchCollection({
297
+ path: parsed.collectionPath,
298
+ filter: queryOptions.where as FetchCollectionProps["filter"],
299
+ limit: queryOptions.limit,
300
+ orderBy: queryOptions.orderBy?.[0]?.field,
301
+ order: queryOptions.orderBy?.[0]?.direction === "desc" ? "desc" : "asc",
302
+ searchString: queryDict.searchString as string | undefined
303
+ });
304
+ return c.json({
305
+ data: entities.map(e => this.flattenEntity(e)),
306
+ meta: {
307
+ total: entities.length,
308
+ limit: queryOptions.limit,
309
+ offset: queryOptions.offset,
310
+ hasMore: false
311
+ }
312
+ });
313
+ }
314
+ });
315
+
316
+ // POST /<subcollection-path> — create entity
317
+ this.router.post("/:parent/:parentId/:rest{.+}", async (c, next) => {
318
+ const rest = c.req.param("rest");
319
+ if (!rest || rest === "undefined") return next();
320
+ const rawPath = `${c.req.param("parent")}/${c.req.param("parentId")}/${rest}`;
321
+ const parsed = parseSubPath(rawPath);
322
+ if (!parsed || parsed.entityId) return next();
323
+
324
+ const driver = c.get("driver") || this.driver;
325
+ const body = await c.req.json().catch(() => ({}));
326
+
327
+ const entity = await driver.saveEntity({
328
+ path: parsed.collectionPath,
329
+ values: body,
330
+ status: "new"
331
+ });
332
+
333
+ return c.json(this.formatResponse(entity), 201);
334
+ });
335
+
336
+ // PUT /<subcollection-path>/:id — update entity
337
+ this.router.put("/:parent/:parentId/:rest{.+}", async (c, next) => {
338
+ const rest = c.req.param("rest");
339
+ if (!rest || rest === "undefined") return next();
340
+ const rawPath = `${c.req.param("parent")}/${c.req.param("parentId")}/${rest}`;
341
+ const parsed = parseSubPath(rawPath);
342
+ if (!parsed || !parsed.entityId) return next();
343
+
344
+ const driver = c.get("driver") || this.driver;
345
+ const body = await c.req.json().catch(() => ({}));
346
+
347
+ const entity = await driver.saveEntity({
348
+ path: parsed.collectionPath,
349
+ entityId: parsed.entityId,
350
+ values: body,
351
+ status: "existing"
352
+ });
353
+
354
+ return c.json(this.formatResponse(entity));
355
+ });
356
+
357
+ // DELETE /<subcollection-path>/:id — delete entity
358
+ this.router.delete("/:parent/:parentId/:rest{.+}", async (c, next) => {
359
+ const rest = c.req.param("rest");
360
+ if (!rest || rest === "undefined") return next();
361
+ const rawPath = `${c.req.param("parent")}/${c.req.param("parentId")}/${rest}`;
362
+ const parsed = parseSubPath(rawPath);
363
+ if (!parsed || !parsed.entityId) return next();
364
+
365
+ const driver = c.get("driver") || this.driver;
366
+
367
+ const existingEntity = await driver.fetchEntity({
368
+ path: parsed.collectionPath,
369
+ entityId: parsed.entityId
370
+ });
371
+
372
+ if (!existingEntity) throw ApiError.notFound("Entity not found");
373
+
374
+ await driver.deleteEntity({ entity: existingEntity });
375
+
376
+ return new Response(null, { status: 204 });
377
+ });
378
+ }
379
+
380
+ /**
381
+ * Format successful API response - flattened for traditional REST API
382
+ */
383
+ private formatResponse<T>(data: T, meta?: Record<string, unknown>): unknown {
384
+ if (Array.isArray(data)) {
385
+ const flattenedData = data.map(entity => this.flattenEntity(entity));
386
+ if (meta) {
387
+ return {
388
+ data: flattenedData,
389
+ meta
390
+ };
391
+ }
392
+ return flattenedData;
393
+ }
394
+
395
+ if (data && typeof data === "object" && "values" in data) {
396
+ return this.flattenEntity(data as unknown as Entity<Record<string, unknown>>);
397
+ }
398
+
399
+ if (meta) {
400
+ return {
401
+ data,
402
+ meta
403
+ };
404
+ }
405
+ return data;
406
+ }
407
+
408
+ /**
409
+ * Flatten Rebase entity structure to traditional REST format
410
+ */
411
+ private flattenEntity(entity: Entity<Record<string, unknown>>): Record<string, unknown> {
412
+ if (!entity || typeof entity !== "object") {
413
+ return entity;
414
+ }
415
+
416
+ if ("values" in entity && typeof entity.values === "object") {
417
+ return {
418
+ id: entity.id,
419
+ ...entity.values
420
+ };
421
+ }
422
+
423
+ return entity as unknown as Record<string, unknown>;
424
+ }
425
+
426
+ /**
427
+ * Fetch raw collection data without Entity wrapper (fallback for non-Postgres)
428
+ */
429
+ private async fetchRawCollection(driver: DataDriver, collection: EntityCollection, queryOptions: QueryOptions) {
430
+ const entities = await driver.fetchCollection({
431
+ path: collection.slug,
432
+ collection,
433
+ filter: queryOptions.where as FetchCollectionProps["filter"],
434
+ limit: queryOptions.limit,
435
+ orderBy: queryOptions.orderBy?.[0]?.field,
436
+ order: queryOptions.orderBy?.[0]?.direction === "desc" ? "desc" : "asc",
437
+ startAfter: queryOptions.offset ? String(queryOptions.offset) : undefined
438
+ });
439
+
440
+ return entities.map(entity => this.flattenEntity(entity));
441
+ }
442
+
443
+ /**
444
+ * Count raw entities for a collection
445
+ */
446
+ private async countRawEntities(driver: DataDriver, collection: EntityCollection, queryOptions: QueryOptions): Promise<number> {
447
+ return driver.countEntities ? await driver.countEntities({
448
+ path: collection.slug,
449
+ collection,
450
+ filter: queryOptions.where as FetchCollectionProps["filter"]
451
+ }) : 0;
452
+ }
453
+
454
+ /**
455
+ * Fetch single entity raw data without Entity wrapper (fallback)
456
+ */
457
+ private async fetchRawEntity(driver: DataDriver, collection: EntityCollection, entityId: string) {
458
+ const entity = await driver.fetchEntity({
459
+ path: collection.slug,
460
+ entityId,
461
+ collection
462
+ });
463
+
464
+ return entity ? this.flattenEntity(entity) : null;
465
+ }
466
+ }
@@ -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"];
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,39 @@
1
+ import { Hono } from "hono";
2
+ import { AstSchemaEditor } from "./ast-schema-editor";
3
+ import { HonoEnv } from "./types";
4
+
5
+ export function createSchemaEditorRoutes(collectionsDir: string): Hono<HonoEnv> {
6
+ const router = new Hono<HonoEnv>();
7
+ const editor = new AstSchemaEditor(collectionsDir);
8
+
9
+ router.post("/property/save", async (c) => {
10
+ const body = await c.req.json();
11
+ const { collectionId, propertyKey, propertyConfig } = body;
12
+ await editor.saveProperty(collectionId, propertyKey, propertyConfig);
13
+ return c.json({ success: true });
14
+ });
15
+
16
+ router.post("/property/delete", async (c) => {
17
+ const body = await c.req.json();
18
+ const { collectionId, propertyKey } = body;
19
+ await editor.deleteProperty(collectionId, propertyKey);
20
+ return c.json({ success: true });
21
+ });
22
+
23
+ router.post("/collection/save", async (c) => {
24
+ const body = await c.req.json();
25
+ const { collectionId, collectionData } = body;
26
+ await editor.saveCollection(collectionId, collectionData);
27
+ return c.json({ success: true });
28
+ });
29
+
30
+ router.post("/collection/delete", async (c) => {
31
+ const body = await c.req.json();
32
+ const { collectionId } = body;
33
+ await editor.deleteCollection(collectionId);
34
+ return c.json({ success: true });
35
+ });
36
+
37
+ return router;
38
+ }
39
+