@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,501 @@
1
+ import { Hono } from "hono";
2
+ import { RestApiGenerator } from "../src/api/rest/api-generator";
3
+ import { errorHandler } from "../src/api/errors";
4
+ import { DataDriver } from "../../types/src/controllers/data_driver";
5
+ import { EntityCollection } from "../../types/src/types/collections";
6
+
7
+ describe("RestApiGenerator", () => {
8
+ let mockDriver: jest.Mocked<DataDriver>;
9
+ let mockCollections: EntityCollection[];
10
+
11
+ beforeEach(() => {
12
+ mockDriver = {
13
+ key: "postgres",
14
+ initialised: true,
15
+ fetchCollection: jest.fn(),
16
+ listenCollection: jest.fn(),
17
+ fetchEntity: jest.fn(),
18
+ listenEntity: jest.fn(),
19
+ saveEntity: jest.fn(),
20
+ deleteEntity: jest.fn(),
21
+ checkUniqueField: jest.fn(),
22
+ countEntities: jest.fn(),
23
+ withAuth: jest.fn(),
24
+ admin: {} as any
25
+ } as unknown as jest.Mocked<DataDriver>;
26
+
27
+ mockCollections = [
28
+ {
29
+ slug: "users",
30
+ name: "Users",
31
+ singularName: "User",
32
+ properties: {}
33
+ } as any,
34
+ {
35
+ slug: "posts",
36
+ name: "Posts",
37
+ singularName: "Post",
38
+ properties: {}
39
+ } as any
40
+ ];
41
+ });
42
+
43
+ /**
44
+ * Mount with "/api" prefix — mirrors production routing in init.ts.
45
+ */
46
+ function createApp() {
47
+ const app = new Hono();
48
+ app.onError(errorHandler);
49
+ const generator = new RestApiGenerator(mockCollections, mockDriver);
50
+ app.route("/api", generator.generateRoutes());
51
+ return app;
52
+ }
53
+
54
+ /**
55
+ * Mount directly at root — eliminates Hono sub-router prefix stripping
56
+ * so we can reliably test route matching without nesting interference.
57
+ */
58
+ function createFlatApp() {
59
+ const generator = new RestApiGenerator(mockCollections, mockDriver);
60
+ const app = generator.generateRoutes();
61
+ app.onError(errorHandler);
62
+ return app;
63
+ }
64
+
65
+ describe("Core Collection Routes", () => {
66
+ it("list entities - GET /api/users", async () => {
67
+ const app = createApp();
68
+ mockDriver.fetchCollection.mockResolvedValue([
69
+ { id: "1",
70
+ path: "users",
71
+ values: { name: "Alice" } } as any
72
+ ]);
73
+ mockDriver.countEntities!.mockResolvedValue(1);
74
+
75
+ const res = await app.request("/api/users?limit=10");
76
+ expect(res.status).toBe(200);
77
+
78
+ const body = await res.json() as any;
79
+ expect(body.data).toEqual([{ id: "1",
80
+ name: "Alice" }]);
81
+ expect(mockDriver.fetchCollection).toHaveBeenCalledWith(
82
+ expect.objectContaining({ path: "users",
83
+ limit: 10 })
84
+ );
85
+ });
86
+
87
+ it("get entity - GET /api/users/123", async () => {
88
+ const app = createApp();
89
+ mockDriver.fetchEntity.mockResolvedValue(
90
+ { id: "123",
91
+ path: "users",
92
+ values: { name: "Alice" } } as any
93
+ );
94
+
95
+ const res = await app.request("/api/users/123");
96
+ expect(res.status).toBe(200);
97
+
98
+ const body = await res.json() as any;
99
+ expect(body.id).toBe("123");
100
+ expect(body.name).toBe("Alice");
101
+ expect(mockDriver.fetchEntity).toHaveBeenCalledWith(
102
+ expect.objectContaining({ path: "users",
103
+ entityId: "123" })
104
+ );
105
+ });
106
+
107
+ it("create entity - POST /api/users", async () => {
108
+ const app = createApp();
109
+ mockDriver.saveEntity.mockResolvedValue(
110
+ { id: "new-1",
111
+ path: "users",
112
+ values: { name: "Bob" } } as any
113
+ );
114
+
115
+ const res = await app.request("/api/users", {
116
+ method: "POST",
117
+ headers: { "Content-Type": "application/json" },
118
+ body: JSON.stringify({ name: "Bob" })
119
+ });
120
+
121
+ expect(res.status).toBe(201);
122
+ expect(mockDriver.saveEntity).toHaveBeenCalledWith(
123
+ expect.objectContaining({ path: "users",
124
+ values: { name: "Bob" },
125
+ status: "new" })
126
+ );
127
+ });
128
+
129
+ it("update entity - PUT /api/users/123", async () => {
130
+ const app = createApp();
131
+ mockDriver.fetchEntity.mockResolvedValue({ id: "123",
132
+ path: "users",
133
+ values: {} } as any);
134
+ mockDriver.saveEntity.mockResolvedValue(
135
+ { id: "123",
136
+ path: "users",
137
+ values: { name: "Bob Jr" } } as any
138
+ );
139
+
140
+ const res = await app.request("/api/users/123", {
141
+ method: "PUT",
142
+ headers: { "Content-Type": "application/json" },
143
+ body: JSON.stringify({ name: "Bob Jr" })
144
+ });
145
+
146
+ expect(res.status).toBe(200);
147
+ expect(mockDriver.saveEntity).toHaveBeenCalledWith(
148
+ expect.objectContaining({ path: "users",
149
+ entityId: "123",
150
+ values: { name: "Bob Jr" },
151
+ status: "existing" })
152
+ );
153
+ });
154
+
155
+ it("delete entity - DELETE /api/users/123", async () => {
156
+ const app = createApp();
157
+ const existingEntity = { id: "123",
158
+ path: "users",
159
+ values: {} } as any;
160
+ mockDriver.fetchEntity.mockResolvedValue(existingEntity);
161
+ mockDriver.deleteEntity.mockResolvedValue();
162
+
163
+ const res = await app.request("/api/users/123", { method: "DELETE" });
164
+
165
+ expect(res.status).toBe(204);
166
+ expect(mockDriver.deleteEntity).toHaveBeenCalledWith(
167
+ expect.objectContaining({ entity: existingEntity })
168
+ );
169
+ });
170
+ });
171
+
172
+ describe("Subcollection Routes", () => {
173
+ /**
174
+ * These tests use the flat app (no prefix nesting) to avoid
175
+ * Hono sub-router routing quirks where `/:slug/:id` eats the request
176
+ * before the `/:parent/:parentId/*` wildcard gets a chance.
177
+ *
178
+ * We use "authors" as the parent slug since it's NOT a registered
179
+ * collection, so it can only match the wildcard catch-all.
180
+ */
181
+
182
+ it("list subcollection - GET /authors/123/posts", async () => {
183
+ const app = createFlatApp();
184
+ mockDriver.fetchCollection.mockResolvedValue([
185
+ { id: "post-1",
186
+ path: "authors/123/posts",
187
+ values: { title: "Hello" } } as any
188
+ ]);
189
+
190
+ const res = await app.request("/authors/123/posts");
191
+ expect(res.status).toBe(200);
192
+
193
+ const body = await res.json() as any;
194
+ expect(body.data).toEqual([{ id: "post-1",
195
+ title: "Hello" }]);
196
+ expect(mockDriver.fetchCollection).toHaveBeenCalledWith(
197
+ expect.objectContaining({ path: "authors/123/posts" })
198
+ );
199
+ });
200
+
201
+ it("get subcollection entity - GET /authors/123/posts/456", async () => {
202
+ const app = createFlatApp();
203
+ mockDriver.fetchEntity.mockResolvedValue(
204
+ { id: "456",
205
+ path: "authors/123/posts",
206
+ values: { title: "Hello" } } as any
207
+ );
208
+
209
+ const res = await app.request("/authors/123/posts/456");
210
+ expect(res.status).toBe(200);
211
+
212
+ const body = await res.json() as any;
213
+ expect(body.id).toBe("456");
214
+ expect(body.title).toBe("Hello");
215
+ expect(mockDriver.fetchEntity).toHaveBeenCalledWith(
216
+ expect.objectContaining({ path: "authors/123/posts",
217
+ entityId: "456" })
218
+ );
219
+ });
220
+
221
+ it("create subcollection entity - POST /authors/123/posts", async () => {
222
+ const app = createFlatApp();
223
+ mockDriver.saveEntity.mockResolvedValue(
224
+ { id: "new-post",
225
+ path: "authors/123/posts",
226
+ values: { title: "New" } } as any
227
+ );
228
+
229
+ const res = await app.request("/authors/123/posts", {
230
+ method: "POST",
231
+ headers: { "Content-Type": "application/json" },
232
+ body: JSON.stringify({ title: "New" })
233
+ });
234
+
235
+ expect(res.status).toBe(201);
236
+ expect(mockDriver.saveEntity).toHaveBeenCalledWith(
237
+ expect.objectContaining({ path: "authors/123/posts",
238
+ status: "new" })
239
+ );
240
+ });
241
+
242
+ it("update subcollection entity - PUT /authors/123/posts/456", async () => {
243
+ const app = createFlatApp();
244
+ mockDriver.saveEntity.mockResolvedValue(
245
+ { id: "456",
246
+ path: "authors/123/posts",
247
+ values: { title: "Updated" } } as any
248
+ );
249
+
250
+ const res = await app.request("/authors/123/posts/456", {
251
+ method: "PUT",
252
+ headers: { "Content-Type": "application/json" },
253
+ body: JSON.stringify({ title: "Updated" })
254
+ });
255
+
256
+ expect(res.status).toBe(200);
257
+ expect(mockDriver.saveEntity).toHaveBeenCalledWith(
258
+ expect.objectContaining({
259
+ path: "authors/123/posts",
260
+ entityId: "456",
261
+ status: "existing"
262
+ })
263
+ );
264
+ });
265
+
266
+ it("delete subcollection entity - DELETE /authors/123/posts/456", async () => {
267
+ const app = createFlatApp();
268
+ const existingEntity = { id: "456",
269
+ path: "authors/123/posts",
270
+ values: {} } as any;
271
+ mockDriver.fetchEntity.mockResolvedValue(existingEntity);
272
+ mockDriver.deleteEntity.mockResolvedValue();
273
+
274
+ const res = await app.request("/authors/123/posts/456", { method: "DELETE" });
275
+
276
+ expect(res.status).toBe(204);
277
+ expect(mockDriver.deleteEntity).toHaveBeenCalledWith(
278
+ expect.objectContaining({ entity: existingEntity })
279
+ );
280
+ });
281
+
282
+ it("deeply nested subcollection list - GET /authors/123/posts/456/comments", async () => {
283
+ const app = createFlatApp();
284
+ mockDriver.fetchCollection.mockResolvedValue([
285
+ { id: "c-1",
286
+ path: "authors/123/posts/456/comments",
287
+ values: { text: "Wow" } } as any
288
+ ]);
289
+
290
+ const res = await app.request("/authors/123/posts/456/comments");
291
+ expect(res.status).toBe(200);
292
+
293
+ const body = await res.json() as any;
294
+ expect(body.data).toEqual([{ id: "c-1",
295
+ text: "Wow" }]);
296
+ expect(mockDriver.fetchCollection).toHaveBeenCalledWith(
297
+ expect.objectContaining({ path: "authors/123/posts/456/comments" })
298
+ );
299
+ });
300
+
301
+ it("deeply nested entity - GET /authors/123/posts/456/comments/789", async () => {
302
+ const app = createFlatApp();
303
+ mockDriver.fetchEntity.mockResolvedValue(
304
+ { id: "789",
305
+ path: "authors/123/posts/456/comments",
306
+ values: { text: "Wow" } } as any
307
+ );
308
+
309
+ const res = await app.request("/authors/123/posts/456/comments/789");
310
+ expect(res.status).toBe(200);
311
+
312
+ const body = await res.json() as any;
313
+ expect(body.id).toBe("789");
314
+ expect(mockDriver.fetchEntity).toHaveBeenCalledWith(
315
+ expect.objectContaining({
316
+ path: "authors/123/posts/456/comments",
317
+ entityId: "789"
318
+ })
319
+ );
320
+ });
321
+
322
+ it("passes query options when listing subcollection", async () => {
323
+ const app = createFlatApp();
324
+ mockDriver.fetchCollection.mockResolvedValue([]);
325
+
326
+ const res = await app.request("/authors/123/posts?limit=5&orderBy=title:desc&searchString=hello");
327
+ expect(res.status).toBe(200);
328
+
329
+ expect(mockDriver.fetchCollection).toHaveBeenCalledWith(
330
+ expect.objectContaining({
331
+ path: "authors/123/posts",
332
+ limit: 5,
333
+ searchString: "hello"
334
+ })
335
+ );
336
+ });
337
+ });
338
+
339
+ describe("Subcollection Wildcard Guard (Undefined / Empty Paths)", () => {
340
+ /**
341
+ * Regression: c.req.param("*") can return "undefined" (literal string)
342
+ * when template literal stringifies JS undefined. This caused:
343
+ * Error: Relation 'undefined' not found in collection 'authors'
344
+ */
345
+
346
+ it("GET /authors/123/undefined → 404, no driver calls", async () => {
347
+ const app = createFlatApp();
348
+ const res = await app.request("/authors/123/undefined");
349
+ expect(res.status).toBe(404);
350
+ expect(mockDriver.fetchCollection).not.toHaveBeenCalled();
351
+ expect(mockDriver.fetchEntity).not.toHaveBeenCalled();
352
+ });
353
+
354
+ it("POST /authors/123/undefined → 404, no driver calls", async () => {
355
+ const app = createFlatApp();
356
+ const res = await app.request("/authors/123/undefined", {
357
+ method: "POST",
358
+ headers: { "Content-Type": "application/json" },
359
+ body: JSON.stringify({ title: "test" })
360
+ });
361
+ expect(res.status).toBe(404);
362
+ expect(mockDriver.saveEntity).not.toHaveBeenCalled();
363
+ });
364
+
365
+ it("PUT /authors/123/undefined → 404, no driver calls", async () => {
366
+ const app = createFlatApp();
367
+ const res = await app.request("/authors/123/undefined", {
368
+ method: "PUT",
369
+ headers: { "Content-Type": "application/json" },
370
+ body: JSON.stringify({ title: "test" })
371
+ });
372
+ expect(res.status).toBe(404);
373
+ expect(mockDriver.saveEntity).not.toHaveBeenCalled();
374
+ });
375
+
376
+ it("DELETE /authors/123/undefined → 404, no driver calls", async () => {
377
+ const app = createFlatApp();
378
+ const res = await app.request("/authors/123/undefined", { method: "DELETE" });
379
+ expect(res.status).toBe(404);
380
+ expect(mockDriver.deleteEntity).not.toHaveBeenCalled();
381
+ });
382
+
383
+ it("nested undefined segment is filtered: GET /authors/123/undefined/posts → 404 (not enough valid segments)", async () => {
384
+ // After filtering "undefined", segments = ["authors", "123", "posts"] → 3 segments → odd → collection path
385
+ // But the wildcard is "undefined/posts", which is NOT the literal "undefined", so the guard won't catch it.
386
+ // However, parseSubPath filters "undefined" from segments: ["authors", "123", "posts"] → valid 3-segment path.
387
+ const app = createFlatApp();
388
+ mockDriver.fetchCollection.mockResolvedValue([]);
389
+ const res = await app.request("/authors/123/undefined/posts");
390
+ // This should either 200 (if parseSubPath accepts "authors/123/posts") or 404
391
+ // Since wildcard = "undefined/posts" which is truthy and not === "undefined", the guard lets it through.
392
+ // parseSubPath filters "undefined" segments → ["authors", "123", "posts"] → 3 segments → collection path.
393
+ expect(res.status).toBe(200);
394
+ expect(mockDriver.fetchCollection).toHaveBeenCalledWith(
395
+ expect.objectContaining({ path: "authors/123/posts" })
396
+ );
397
+ });
398
+
399
+ it("returns 404 for bare parent without wildcard - GET /xyz", async () => {
400
+ const app = createFlatApp();
401
+ const res = await app.request("/xyz");
402
+ expect(res.status).toBe(404);
403
+ expect(mockDriver.fetchCollection).not.toHaveBeenCalled();
404
+ });
405
+
406
+ it("returns 404 for only two segments (no relation) - GET /xyz/123", async () => {
407
+ // Two segments: would be handled by /:slug/:id route, but "xyz" isn't a registered collection
408
+ // so the /:slug route won't match. /:parent/:parentId/* won't match either (no wildcard part).
409
+ const app = createFlatApp();
410
+ const res = await app.request("/xyz/123");
411
+ expect(res.status).toBe(404);
412
+ });
413
+ });
414
+
415
+ describe("Path Parsing Edge Cases via HTTP", () => {
416
+ it("numeric IDs are correctly parsed as entityId", async () => {
417
+ const app = createFlatApp();
418
+ mockDriver.fetchEntity.mockResolvedValue(
419
+ { id: "999",
420
+ path: "authors/42/books",
421
+ values: { title: "Test" } } as any
422
+ );
423
+
424
+ const res = await app.request("/authors/42/books/999");
425
+ expect(res.status).toBe(200);
426
+ expect(mockDriver.fetchEntity).toHaveBeenCalledWith(
427
+ expect.objectContaining({ path: "authors/42/books",
428
+ entityId: "999" })
429
+ );
430
+ });
431
+
432
+ it("UUID-style IDs work as parentId and entityId", async () => {
433
+ const app = createFlatApp();
434
+ const uuid1 = "a1b2c3d4-e5f6-7890-abcd-ef1234567890";
435
+ const uuid2 = "ffffffff-ffff-ffff-ffff-ffffffffffff";
436
+ mockDriver.fetchEntity.mockResolvedValue(
437
+ { id: uuid2,
438
+ path: `authors/${uuid1}/posts`,
439
+ values: { title: "UUID" } } as any
440
+ );
441
+
442
+ const res = await app.request(`/authors/${uuid1}/posts/${uuid2}`);
443
+ expect(res.status).toBe(200);
444
+ expect(mockDriver.fetchEntity).toHaveBeenCalledWith(
445
+ expect.objectContaining({
446
+ path: `authors/${uuid1}/posts`,
447
+ entityId: uuid2
448
+ })
449
+ );
450
+ });
451
+
452
+ it("entity not found returns 404 for subcollection GET", async () => {
453
+ const app = createFlatApp();
454
+ mockDriver.fetchEntity.mockResolvedValue(undefined);
455
+
456
+ const res = await app.request("/authors/123/posts/999");
457
+ expect(res.status).toBe(404);
458
+ });
459
+
460
+ it("POST to even-segment path returns 404 (entityId present, can't create)", async () => {
461
+ const app = createFlatApp();
462
+ // 4 segments: authors/123/posts/456 — parseSubPath gives entityId="456"
463
+ // POST handler checks: if (parsed.entityId) return c.notFound();
464
+ const res = await app.request("/authors/123/posts/456", {
465
+ method: "POST",
466
+ headers: { "Content-Type": "application/json" },
467
+ body: JSON.stringify({ title: "nope" })
468
+ });
469
+ expect(res.status).toBe(404);
470
+ expect(mockDriver.saveEntity).not.toHaveBeenCalled();
471
+ });
472
+
473
+ it("PUT to odd-segment path returns 404 (no entityId, can't update)", async () => {
474
+ const app = createFlatApp();
475
+ // 3 segments: authors/123/posts — no entityId
476
+ // PUT handler checks: if (!parsed.entityId) return c.notFound();
477
+ const res = await app.request("/authors/123/posts", {
478
+ method: "PUT",
479
+ headers: { "Content-Type": "application/json" },
480
+ body: JSON.stringify({ title: "nope" })
481
+ });
482
+ expect(res.status).toBe(404);
483
+ expect(mockDriver.saveEntity).not.toHaveBeenCalled();
484
+ });
485
+
486
+ it("DELETE to odd-segment path returns 404 (no entityId)", async () => {
487
+ const app = createFlatApp();
488
+ const res = await app.request("/authors/123/posts", { method: "DELETE" });
489
+ expect(res.status).toBe(404);
490
+ expect(mockDriver.deleteEntity).not.toHaveBeenCalled();
491
+ });
492
+
493
+ it("DELETE to non-existent entity returns 404", async () => {
494
+ const app = createFlatApp();
495
+ mockDriver.fetchEntity.mockResolvedValue(undefined);
496
+ const res = await app.request("/authors/123/posts/456", { method: "DELETE" });
497
+ expect(res.status).toBe(404);
498
+ expect(mockDriver.deleteEntity).not.toHaveBeenCalled();
499
+ });
500
+ });
501
+ });
@@ -0,0 +1,63 @@
1
+ import { AstSchemaEditor } from "../src/api/ast-schema-editor";
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+ import * as os from "os";
5
+
6
+ describe("AstSchemaEditor", () => {
7
+ let testDir: string;
8
+ let editor: AstSchemaEditor;
9
+
10
+ beforeEach(() => {
11
+ testDir = fs.mkdtempSync(path.join(os.tmpdir(), "ast-schema-editor-test-"));
12
+ editor = new AstSchemaEditor(testDir);
13
+ });
14
+
15
+ afterEach(() => {
16
+ fs.rmSync(testDir, { recursive: true,
17
+ force: true });
18
+ });
19
+
20
+ it("should merge properties while preserving existing functions when saving a collection", async () => {
21
+ // Setup initial file
22
+ const fileContent = `import { EntityCollection } from "@rebasepro/types";
23
+
24
+ const productsCollection: EntityCollection = {
25
+ name: "Products",
26
+ slug: "products",
27
+ properties: {
28
+ id: { type: "string" },
29
+ name: { type: "string" },
30
+ category: {
31
+ type: "reference",
32
+ target: () => categoriesCollection
33
+ }
34
+ }
35
+ };
36
+
37
+ export default productsCollection;
38
+ `;
39
+ fs.writeFileSync(path.join(testDir, "products.ts"), fileContent);
40
+
41
+ // We want to update the collection data, simulating what the frontend sends
42
+ const updatedData = {
43
+ name: "Updated Products",
44
+ slug: "products",
45
+ properties: {
46
+ id: { type: "string" },
47
+ name: { type: "string",
48
+ description: "Product name" },
49
+ category: {
50
+ type: "reference"
51
+ // Notice target is dropped since the frontend REST payload wouldn't have it serialized
52
+ }
53
+ }
54
+ };
55
+
56
+ await editor.saveCollection("products", updatedData);
57
+
58
+ const newContent = fs.readFileSync(path.join(testDir, "products.ts"), "utf-8");
59
+ expect(newContent).toContain('name: "Updated Products"');
60
+ expect(newContent).toContain("target: () => categoriesCollection");
61
+ expect(newContent).toContain('description: "Product name"');
62
+ });
63
+ });