@rebasepro/server-core 0.7.0 → 0.9.0

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/README.md +2 -2
  2. package/dist/api/ast-schema-editor.d.ts +21 -0
  3. package/dist/api/collections_for_test/callbacks_test_collection.d.ts +2 -0
  4. package/dist/api/errors.d.ts +53 -0
  5. package/dist/api/index.d.ts +7 -0
  6. package/dist/api/logs-routes.d.ts +37 -0
  7. package/dist/api/openapi-generator.d.ts +16 -0
  8. package/dist/api/rest/api-generator.d.ts +68 -0
  9. package/{src/api/rest/index.ts → dist/api/rest/index.d.ts} +0 -1
  10. package/dist/api/rest/query-parser.d.ts +6 -0
  11. package/dist/api/schema-editor-routes.d.ts +3 -0
  12. package/dist/api/types.d.ts +56 -0
  13. package/dist/auth/adapter-middleware.d.ts +43 -0
  14. package/dist/auth/admin-roles-route.d.ts +18 -0
  15. package/dist/auth/admin-user-ops.d.ts +79 -0
  16. package/dist/auth/admin-users-route.d.ts +28 -0
  17. package/dist/auth/api-keys/api-key-middleware.d.ts +39 -0
  18. package/{src/auth/api-keys/api-key-permission-guard.ts → dist/auth/api-keys/api-key-permission-guard.d.ts} +2 -34
  19. package/dist/auth/api-keys/api-key-routes.d.ts +20 -0
  20. package/dist/auth/api-keys/api-key-store.d.ts +35 -0
  21. package/{src/auth/api-keys/api-key-types.ts → dist/auth/api-keys/api-key-types.d.ts} +0 -6
  22. package/{src/auth/api-keys/index.ts → dist/auth/api-keys/index.d.ts} +2 -22
  23. package/dist/auth/apple-oauth.d.ts +30 -0
  24. package/{src/auth/auth-hooks.ts → dist/auth/auth-hooks.d.ts} +15 -73
  25. package/dist/auth/bitbucket-oauth.d.ts +11 -0
  26. package/dist/auth/builtin-auth-adapter.d.ts +55 -0
  27. package/dist/auth/cookie-utils.d.ts +32 -0
  28. package/{src/auth/crypto-utils.ts → dist/auth/crypto-utils.d.ts} +1 -16
  29. package/dist/auth/custom-auth-adapter.d.ts +39 -0
  30. package/dist/auth/discord-oauth.d.ts +14 -0
  31. package/dist/auth/facebook-oauth.d.ts +14 -0
  32. package/dist/auth/github-oauth.d.ts +15 -0
  33. package/dist/auth/gitlab-oauth.d.ts +13 -0
  34. package/dist/auth/google-oauth.d.ts +47 -0
  35. package/{src/auth/index.ts → dist/auth/index.d.ts} +4 -22
  36. package/{src/auth/interfaces.ts → dist/auth/interfaces.d.ts} +20 -89
  37. package/dist/auth/jwt.d.ts +67 -0
  38. package/dist/auth/linkedin-oauth.d.ts +18 -0
  39. package/dist/auth/magic-link-routes.d.ts +32 -0
  40. package/dist/auth/mfa-crypto.d.ts +23 -0
  41. package/dist/auth/mfa-routes.d.ts +7 -0
  42. package/dist/auth/mfa.d.ts +49 -0
  43. package/dist/auth/microsoft-oauth.d.ts +16 -0
  44. package/dist/auth/middleware.d.ts +158 -0
  45. package/dist/auth/password.d.ts +22 -0
  46. package/dist/auth/rate-limiter.d.ts +50 -0
  47. package/dist/auth/reset-password-admin.d.ts +29 -0
  48. package/dist/auth/rls-scope.d.ts +31 -0
  49. package/dist/auth/routes.d.ts +66 -0
  50. package/dist/auth/session-routes.d.ts +29 -0
  51. package/dist/auth/slack-oauth.d.ts +12 -0
  52. package/dist/auth/spotify-oauth.d.ts +12 -0
  53. package/dist/auth/twitter-oauth.d.ts +18 -0
  54. package/dist/collections/BackendCollectionRegistry.d.ts +13 -0
  55. package/dist/collections/loader.d.ts +5 -0
  56. package/dist/cron/cron-loader.d.ts +18 -0
  57. package/dist/cron/cron-routes.d.ts +14 -0
  58. package/dist/cron/cron-scheduler.d.ts +106 -0
  59. package/dist/cron/cron-store.d.ts +32 -0
  60. package/dist/cron/define-cron.d.ts +28 -0
  61. package/{src/cron/index.ts → dist/cron/index.d.ts} +1 -0
  62. package/dist/db/interfaces.d.ts +18 -0
  63. package/dist/dynamic-import-Dvh-K5fl.js +22 -0
  64. package/dist/dynamic-import-Dvh-K5fl.js.map +1 -0
  65. package/dist/email/index.d.ts +6 -0
  66. package/dist/email/smtp-email-service.d.ts +30 -0
  67. package/dist/email/templates.d.ts +50 -0
  68. package/{src/email/types.ts → dist/email/types.d.ts} +40 -36
  69. package/dist/env.d.ts +102 -0
  70. package/dist/functions/define-function.d.ts +55 -0
  71. package/dist/functions/function-loader.d.ts +18 -0
  72. package/dist/functions/function-routes.d.ts +10 -0
  73. package/{src/functions/index.ts → dist/functions/index.d.ts} +2 -0
  74. package/dist/history/history-routes.d.ts +23 -0
  75. package/{src/history/index.ts → dist/history/index.d.ts} +0 -1
  76. package/dist/index.d.ts +35 -0
  77. package/dist/index.es.js +18184 -0
  78. package/dist/index.es.js.map +1 -0
  79. package/dist/index.umd.js +48629 -0
  80. package/dist/index.umd.js.map +1 -0
  81. package/dist/init/docs.d.ts +4 -0
  82. package/dist/init/health.d.ts +2 -0
  83. package/dist/init/middlewares.d.ts +10 -0
  84. package/dist/init/shutdown.d.ts +65 -0
  85. package/dist/init/storage.d.ts +5 -0
  86. package/dist/init.d.ts +344 -0
  87. package/dist/jwt-BwIn8xmk.js +4200 -0
  88. package/dist/jwt-BwIn8xmk.js.map +1 -0
  89. package/dist/logger-BYU66ENZ.js +94 -0
  90. package/dist/logger-BYU66ENZ.js.map +1 -0
  91. package/dist/ms-DnYXB-Wd.js +162 -0
  92. package/dist/ms-DnYXB-Wd.js.map +1 -0
  93. package/dist/openapi-generator-Z9oYWLf_.js +586 -0
  94. package/dist/openapi-generator-Z9oYWLf_.js.map +1 -0
  95. package/dist/schema-editor-routes-D3ef8zu1.js +219 -0
  96. package/dist/schema-editor-routes-D3ef8zu1.js.map +1 -0
  97. package/dist/serve-spa.d.ts +36 -0
  98. package/dist/services/driver-registry.d.ts +78 -0
  99. package/dist/services/routed-realtime-service.d.ts +43 -0
  100. package/dist/services/webhook-service.d.ts +29 -0
  101. package/dist/singleton.d.ts +51 -0
  102. package/dist/src-B4OLmNVa.js +437 -0
  103. package/dist/src-B4OLmNVa.js.map +1 -0
  104. package/dist/src-B5WkP1Rv.js +24778 -0
  105. package/dist/src-B5WkP1Rv.js.map +1 -0
  106. package/dist/storage/GCSStorageController.d.ts +43 -0
  107. package/dist/storage/LocalStorageController.d.ts +46 -0
  108. package/dist/storage/S3StorageController.d.ts +40 -0
  109. package/dist/storage/image-transform.d.ts +50 -0
  110. package/{src/storage/index.ts → dist/storage/index.d.ts} +6 -21
  111. package/dist/storage/routes.d.ts +70 -0
  112. package/dist/storage/storage-registry.d.ts +78 -0
  113. package/dist/storage/tus-handler.d.ts +53 -0
  114. package/{src/storage/types.ts → dist/storage/types.d.ts} +32 -38
  115. package/dist/types/index.d.ts +11 -0
  116. package/dist/utils/dev-port.d.ts +45 -0
  117. package/dist/utils/dynamic-import.d.ts +25 -0
  118. package/dist/utils/logger.d.ts +31 -0
  119. package/dist/utils/logging.d.ts +9 -0
  120. package/dist/utils/request-id.d.ts +4 -0
  121. package/dist/utils/request-logger.d.ts +19 -0
  122. package/{src/utils/sql.ts → dist/utils/sql.d.ts} +5 -16
  123. package/package.json +49 -22
  124. package/coverage/clover.xml +0 -3739
  125. package/coverage/coverage-final.json +0 -31
  126. package/coverage/lcov-report/base.css +0 -224
  127. package/coverage/lcov-report/block-navigation.js +0 -87
  128. package/coverage/lcov-report/favicon.png +0 -0
  129. package/coverage/lcov-report/index.html +0 -266
  130. package/coverage/lcov-report/prettify.css +0 -1
  131. package/coverage/lcov-report/prettify.js +0 -2
  132. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  133. package/coverage/lcov-report/sorter.js +0 -210
  134. package/coverage/lcov-report/src/api/ast-schema-editor.ts.html +0 -952
  135. package/coverage/lcov-report/src/api/errors.ts.html +0 -472
  136. package/coverage/lcov-report/src/api/graphql/graphql-schema-generator.ts.html +0 -1069
  137. package/coverage/lcov-report/src/api/graphql/index.html +0 -116
  138. package/coverage/lcov-report/src/api/index.html +0 -176
  139. package/coverage/lcov-report/src/api/openapi-generator.ts.html +0 -565
  140. package/coverage/lcov-report/src/api/rest/api-generator.ts.html +0 -994
  141. package/coverage/lcov-report/src/api/rest/index.html +0 -131
  142. package/coverage/lcov-report/src/api/rest/query-parser.ts.html +0 -550
  143. package/coverage/lcov-report/src/api/schema-editor-routes.ts.html +0 -202
  144. package/coverage/lcov-report/src/api/server.ts.html +0 -823
  145. package/coverage/lcov-report/src/auth/admin-routes.ts.html +0 -973
  146. package/coverage/lcov-report/src/auth/index.html +0 -176
  147. package/coverage/lcov-report/src/auth/jwt.ts.html +0 -574
  148. package/coverage/lcov-report/src/auth/middleware.ts.html +0 -745
  149. package/coverage/lcov-report/src/auth/password.ts.html +0 -310
  150. package/coverage/lcov-report/src/auth/services.ts.html +0 -2074
  151. package/coverage/lcov-report/src/collections/index.html +0 -116
  152. package/coverage/lcov-report/src/collections/loader.ts.html +0 -232
  153. package/coverage/lcov-report/src/db/auth-schema.ts.html +0 -523
  154. package/coverage/lcov-report/src/db/data-transformer.ts.html +0 -1753
  155. package/coverage/lcov-report/src/db/entityService.ts.html +0 -700
  156. package/coverage/lcov-report/src/db/index.html +0 -146
  157. package/coverage/lcov-report/src/db/services/EntityFetchService.ts.html +0 -4048
  158. package/coverage/lcov-report/src/db/services/EntityPersistService.ts.html +0 -883
  159. package/coverage/lcov-report/src/db/services/RelationService.ts.html +0 -3121
  160. package/coverage/lcov-report/src/db/services/entity-helpers.ts.html +0 -442
  161. package/coverage/lcov-report/src/db/services/index.html +0 -176
  162. package/coverage/lcov-report/src/db/services/index.ts.html +0 -124
  163. package/coverage/lcov-report/src/generate-drizzle-schema-logic.ts.html +0 -1960
  164. package/coverage/lcov-report/src/index.html +0 -116
  165. package/coverage/lcov-report/src/services/driver-registry.ts.html +0 -631
  166. package/coverage/lcov-report/src/services/index.html +0 -131
  167. package/coverage/lcov-report/src/services/postgresDataDriver.ts.html +0 -3025
  168. package/coverage/lcov-report/src/storage/LocalStorageController.ts.html +0 -1189
  169. package/coverage/lcov-report/src/storage/S3StorageController.ts.html +0 -970
  170. package/coverage/lcov-report/src/storage/index.html +0 -161
  171. package/coverage/lcov-report/src/storage/storage-registry.ts.html +0 -646
  172. package/coverage/lcov-report/src/storage/types.ts.html +0 -451
  173. package/coverage/lcov-report/src/utils/drizzle-conditions.ts.html +0 -3082
  174. package/coverage/lcov-report/src/utils/index.html +0 -116
  175. package/coverage/lcov.info +0 -7179
  176. package/jest.config.cjs +0 -19
  177. package/src/api/ast-schema-editor.ts +0 -289
  178. package/src/api/collections_for_test/callbacks_test_collection.ts +0 -61
  179. package/src/api/errors.ts +0 -278
  180. package/src/api/graphql/graphql-schema-generator.ts +0 -383
  181. package/src/api/graphql/index.ts +0 -2
  182. package/src/api/index.ts +0 -11
  183. package/src/api/logs-routes.ts +0 -132
  184. package/src/api/openapi-generator.ts +0 -727
  185. package/src/api/rest/api-generator-count.test.ts +0 -126
  186. package/src/api/rest/api-generator.ts +0 -747
  187. package/src/api/rest/query-parser.ts +0 -300
  188. package/src/api/schema-editor-routes.ts +0 -41
  189. package/src/api/server.ts +0 -271
  190. package/src/api/types.ts +0 -104
  191. package/src/auth/adapter-middleware.ts +0 -114
  192. package/src/auth/admin-roles-route.ts +0 -36
  193. package/src/auth/admin-user-ops.ts +0 -236
  194. package/src/auth/admin-users-route.ts +0 -302
  195. package/src/auth/api-keys/api-key-middleware.ts +0 -133
  196. package/src/auth/api-keys/api-key-routes.ts +0 -193
  197. package/src/auth/api-keys/api-key-store.ts +0 -340
  198. package/src/auth/apple-oauth.ts +0 -122
  199. package/src/auth/bitbucket-oauth.ts +0 -84
  200. package/src/auth/builtin-auth-adapter.ts +0 -380
  201. package/src/auth/custom-auth-adapter.ts +0 -86
  202. package/src/auth/discord-oauth.ts +0 -85
  203. package/src/auth/facebook-oauth.ts +0 -74
  204. package/src/auth/github-oauth.ts +0 -112
  205. package/src/auth/gitlab-oauth.ts +0 -72
  206. package/src/auth/google-oauth.ts +0 -223
  207. package/src/auth/jwt.ts +0 -208
  208. package/src/auth/linkedin-oauth.ts +0 -83
  209. package/src/auth/magic-link-routes.ts +0 -167
  210. package/src/auth/mfa-crypto.ts +0 -91
  211. package/src/auth/mfa-routes.ts +0 -323
  212. package/src/auth/mfa.ts +0 -161
  213. package/src/auth/microsoft-oauth.ts +0 -90
  214. package/src/auth/middleware.ts +0 -369
  215. package/src/auth/password.ts +0 -77
  216. package/src/auth/rate-limiter.ts +0 -225
  217. package/src/auth/reset-password-admin.ts +0 -160
  218. package/src/auth/rls-scope.ts +0 -58
  219. package/src/auth/routes.ts +0 -802
  220. package/src/auth/session-routes.ts +0 -353
  221. package/src/auth/slack-oauth.ts +0 -73
  222. package/src/auth/spotify-oauth.ts +0 -69
  223. package/src/auth/twitter-oauth.ts +0 -128
  224. package/src/collections/BackendCollectionRegistry.ts +0 -20
  225. package/src/collections/loader.ts +0 -50
  226. package/src/cron/cron-loader.ts +0 -84
  227. package/src/cron/cron-routes.test.ts +0 -265
  228. package/src/cron/cron-routes.ts +0 -85
  229. package/src/cron/cron-scheduler.test.ts +0 -559
  230. package/src/cron/cron-scheduler.ts +0 -581
  231. package/src/cron/cron-store.ts +0 -166
  232. package/src/db/interfaces.ts +0 -60
  233. package/src/email/index.ts +0 -19
  234. package/src/email/smtp-email-service.ts +0 -123
  235. package/src/email/templates.ts +0 -470
  236. package/src/env.ts +0 -227
  237. package/src/functions/function-loader.ts +0 -116
  238. package/src/functions/function-routes.ts +0 -31
  239. package/src/history/history-routes.ts +0 -129
  240. package/src/index.ts +0 -70
  241. package/src/init/docs.ts +0 -47
  242. package/src/init/health.ts +0 -37
  243. package/src/init/middlewares.ts +0 -61
  244. package/src/init/shutdown.ts +0 -56
  245. package/src/init/storage.ts +0 -57
  246. package/src/init.ts +0 -1041
  247. package/src/serve-spa.ts +0 -89
  248. package/src/services/driver-registry.ts +0 -183
  249. package/src/services/routed-realtime-service.ts +0 -113
  250. package/src/services/webhook-service.ts +0 -155
  251. package/src/singleton.ts +0 -70
  252. package/src/storage/LocalStorageController.ts +0 -389
  253. package/src/storage/S3StorageController.ts +0 -300
  254. package/src/storage/image-transform.ts +0 -218
  255. package/src/storage/routes.ts +0 -368
  256. package/src/storage/storage-registry.ts +0 -188
  257. package/src/storage/tus-handler.ts +0 -315
  258. package/src/types/index.ts +0 -27
  259. package/src/utils/dev-port.ts +0 -196
  260. package/src/utils/logger.ts +0 -143
  261. package/src/utils/logging.ts +0 -40
  262. package/src/utils/request-id.ts +0 -40
  263. package/src/utils/request-logger.ts +0 -72
  264. package/test/api-generator.test.ts +0 -588
  265. package/test/api-key-permission-guard.test.ts +0 -144
  266. package/test/ast-schema-editor.test.ts +0 -89
  267. package/test/auth-middleware-hono.test.ts +0 -556
  268. package/test/auth-routes.test.ts +0 -1098
  269. package/test/backend-hooks-data.test.ts +0 -477
  270. package/test/custom-auth-adapter.test.ts +0 -189
  271. package/test/driver-registry.test.ts +0 -282
  272. package/test/email-templates.test.ts +0 -174
  273. package/test/env.test.ts +0 -128
  274. package/test/error-propagation.test.ts +0 -226
  275. package/test/errors-hono.test.ts +0 -133
  276. package/test/errors.test.ts +0 -155
  277. package/test/function-loader.test.ts +0 -127
  278. package/test/graphql-schema-generator.test.ts +0 -554
  279. package/test/jwt-security.test.ts +0 -182
  280. package/test/jwt.test.ts +0 -326
  281. package/test/mfa.test.ts +0 -197
  282. package/test/middleware.test.ts +0 -305
  283. package/test/multi-datasource-routing.test.ts +0 -113
  284. package/test/password.test.ts +0 -165
  285. package/test/query-parser.test.ts +0 -233
  286. package/test/rate-limiter.test.ts +0 -102
  287. package/test/reset-password-admin.test.ts +0 -113
  288. package/test/routed-realtime-service.test.ts +0 -86
  289. package/test/safe-compare.test.ts +0 -66
  290. package/test/singleton.test.ts +0 -59
  291. package/test/smtp-email-service.test.ts +0 -169
  292. package/test/storage-local.test.ts +0 -271
  293. package/test/storage-registry.test.ts +0 -282
  294. package/test/storage-routes.test.ts +0 -222
  295. package/test/storage-s3.test.ts +0 -304
  296. package/test/transform-auth-response.test.ts +0 -305
  297. package/test/webhook-service.test.ts +0 -260
  298. package/tsconfig.json +0 -54
  299. package/tsconfig.prod.json +0 -23
  300. package/vite.config.ts +0 -86
@@ -1,588 +0,0 @@
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
- // RestApiGenerator.getScopedDriver reads from c.get("driver") which is
50
- // normally set by auth middleware. Replicate that here for tests.
51
- app.use("/api/*", async (c, next) => {
52
- c.set("driver", mockDriver);
53
- await next();
54
- });
55
- const generator = new RestApiGenerator(mockCollections, mockDriver);
56
- app.route("/api", generator.generateRoutes());
57
- return app;
58
- }
59
-
60
- /**
61
- * Mount directly at root — eliminates Hono sub-router prefix stripping
62
- * so we can reliably test route matching without nesting interference.
63
- */
64
- function createFlatApp() {
65
- const app = new Hono();
66
- app.onError(errorHandler);
67
- app.use("/*", async (c, next) => {
68
- c.set("driver", mockDriver);
69
- await next();
70
- });
71
- const generator = new RestApiGenerator(mockCollections, mockDriver);
72
- app.route("/", generator.generateRoutes());
73
- return app;
74
- }
75
-
76
- describe("Core Collection Routes", () => {
77
- it("list entities - GET /api/users", async () => {
78
- const app = createApp();
79
- mockDriver.fetchCollection.mockResolvedValue([
80
- { id: "1",
81
- path: "users",
82
- values: { name: "Alice" } } as any
83
- ]);
84
- mockDriver.countEntities!.mockResolvedValue(1);
85
-
86
- const res = await app.request("/api/users?limit=10");
87
- expect(res.status).toBe(200);
88
-
89
- const body = await res.json() as any;
90
- expect(body.data).toEqual([{ id: "1",
91
- name: "Alice" }]);
92
- expect(mockDriver.fetchCollection).toHaveBeenCalledWith(
93
- expect.objectContaining({ path: "users",
94
- limit: 10 })
95
- );
96
- });
97
-
98
- it("get entity - GET /api/users/123", async () => {
99
- const app = createApp();
100
- mockDriver.fetchEntity.mockResolvedValue(
101
- { id: "123",
102
- path: "users",
103
- values: { name: "Alice" } } as any
104
- );
105
-
106
- const res = await app.request("/api/users/123");
107
- expect(res.status).toBe(200);
108
-
109
- const body = await res.json() as any;
110
- expect(body.id).toBe("123");
111
- expect(body.name).toBe("Alice");
112
- expect(mockDriver.fetchEntity).toHaveBeenCalledWith(
113
- expect.objectContaining({ path: "users",
114
- entityId: "123" })
115
- );
116
- });
117
-
118
- it("create entity - POST /api/users", async () => {
119
- const app = createApp();
120
- mockDriver.saveEntity.mockResolvedValue(
121
- { id: "new-1",
122
- path: "users",
123
- values: { name: "Bob" } } as any
124
- );
125
-
126
- const res = await app.request("/api/users", {
127
- method: "POST",
128
- headers: { "Content-Type": "application/json" },
129
- body: JSON.stringify({ name: "Bob" })
130
- });
131
-
132
- expect(res.status).toBe(201);
133
- expect(mockDriver.saveEntity).toHaveBeenCalledWith(
134
- expect.objectContaining({ path: "users",
135
- values: { name: "Bob" },
136
- status: "new" })
137
- );
138
- });
139
-
140
- it("update entity - PUT /api/users/123", async () => {
141
- const app = createApp();
142
- mockDriver.fetchEntity.mockResolvedValue({ id: "123",
143
- path: "users",
144
- values: {} } as any);
145
- mockDriver.saveEntity.mockResolvedValue(
146
- { id: "123",
147
- path: "users",
148
- values: { name: "Bob Jr" } } as any
149
- );
150
-
151
- const res = await app.request("/api/users/123", {
152
- method: "PUT",
153
- headers: { "Content-Type": "application/json" },
154
- body: JSON.stringify({ name: "Bob Jr" })
155
- });
156
-
157
- expect(res.status).toBe(200);
158
- expect(mockDriver.saveEntity).toHaveBeenCalledWith(
159
- expect.objectContaining({ path: "users",
160
- entityId: "123",
161
- values: { name: "Bob Jr" },
162
- status: "existing" })
163
- );
164
- });
165
-
166
- it("delete entity - DELETE /api/users/123", async () => {
167
- const app = createApp();
168
- const existingEntity = { id: "123",
169
- path: "users",
170
- values: {} } as any;
171
- mockDriver.fetchEntity.mockResolvedValue(existingEntity);
172
- mockDriver.deleteEntity.mockResolvedValue();
173
-
174
- const res = await app.request("/api/users/123", { method: "DELETE" });
175
-
176
- expect(res.status).toBe(204);
177
- expect(mockDriver.deleteEntity).toHaveBeenCalledWith(
178
- expect.objectContaining({ entity: existingEntity })
179
- );
180
- });
181
- });
182
- describe("Subcollection Routes", () => {
183
- /**
184
- * These tests use the flat app (no prefix nesting) to avoid
185
- * Hono sub-router routing quirks where `/:slug/:id` eats the request
186
- * before the `/:parent/:parentId/*` wildcard gets a chance.
187
- *
188
- * We use "authors" as the parent slug since it's NOT a registered
189
- * collection, so it can only match the wildcard catch-all.
190
- */
191
-
192
- it("list subcollection - GET /authors/123/posts", async () => {
193
- const app = createFlatApp();
194
- mockDriver.fetchCollection.mockResolvedValue([
195
- { id: "post-1",
196
- path: "authors/123/posts",
197
- values: { title: "Hello" } } as any
198
- ]);
199
-
200
- const res = await app.request("/authors/123/posts");
201
- expect(res.status).toBe(200);
202
-
203
- const body = await res.json() as any;
204
- expect(body.data).toEqual([{ id: "post-1",
205
- title: "Hello" }]);
206
- expect(mockDriver.fetchCollection).toHaveBeenCalledWith(
207
- expect.objectContaining({ path: "authors/123/posts" })
208
- );
209
- });
210
-
211
- it("get subcollection entity - GET /authors/123/posts/456", async () => {
212
- const app = createFlatApp();
213
- mockDriver.fetchEntity.mockResolvedValue(
214
- { id: "456",
215
- path: "authors/123/posts",
216
- values: { title: "Hello" } } as any
217
- );
218
-
219
- const res = await app.request("/authors/123/posts/456");
220
- expect(res.status).toBe(200);
221
-
222
- const body = await res.json() as any;
223
- expect(body.id).toBe("456");
224
- expect(body.title).toBe("Hello");
225
- expect(mockDriver.fetchEntity).toHaveBeenCalledWith(
226
- expect.objectContaining({ path: "authors/123/posts",
227
- entityId: "456" })
228
- );
229
- });
230
-
231
- it("create subcollection entity - POST /authors/123/posts", async () => {
232
- const app = createFlatApp();
233
- mockDriver.saveEntity.mockResolvedValue(
234
- { id: "new-post",
235
- path: "authors/123/posts",
236
- values: { title: "New" } } as any
237
- );
238
-
239
- const res = await app.request("/authors/123/posts", {
240
- method: "POST",
241
- headers: { "Content-Type": "application/json" },
242
- body: JSON.stringify({ title: "New" })
243
- });
244
-
245
- expect(res.status).toBe(201);
246
- expect(mockDriver.saveEntity).toHaveBeenCalledWith(
247
- expect.objectContaining({ path: "authors/123/posts",
248
- status: "new" })
249
- );
250
- });
251
-
252
- it("update subcollection entity - PUT /authors/123/posts/456", async () => {
253
- const app = createFlatApp();
254
- mockDriver.saveEntity.mockResolvedValue(
255
- { id: "456",
256
- path: "authors/123/posts",
257
- values: { title: "Updated" } } as any
258
- );
259
-
260
- const res = await app.request("/authors/123/posts/456", {
261
- method: "PUT",
262
- headers: { "Content-Type": "application/json" },
263
- body: JSON.stringify({ title: "Updated" })
264
- });
265
-
266
- expect(res.status).toBe(200);
267
- expect(mockDriver.saveEntity).toHaveBeenCalledWith(
268
- expect.objectContaining({
269
- path: "authors/123/posts",
270
- entityId: "456",
271
- status: "existing"
272
- })
273
- );
274
- });
275
-
276
- it("delete subcollection entity - DELETE /authors/123/posts/456", async () => {
277
- const app = createFlatApp();
278
- const existingEntity = { id: "456",
279
- path: "authors/123/posts",
280
- values: {} } as any;
281
- mockDriver.fetchEntity.mockResolvedValue(existingEntity);
282
- mockDriver.deleteEntity.mockResolvedValue();
283
-
284
- const res = await app.request("/authors/123/posts/456", { method: "DELETE" });
285
-
286
- expect(res.status).toBe(204);
287
- expect(mockDriver.deleteEntity).toHaveBeenCalledWith(
288
- expect.objectContaining({ entity: existingEntity })
289
- );
290
- });
291
-
292
- it("deeply nested subcollection list - GET /authors/123/posts/456/comments", async () => {
293
- const app = createFlatApp();
294
- mockDriver.fetchCollection.mockResolvedValue([
295
- { id: "c-1",
296
- path: "authors/123/posts/456/comments",
297
- values: { text: "Wow" } } as any
298
- ]);
299
-
300
- const res = await app.request("/authors/123/posts/456/comments");
301
- expect(res.status).toBe(200);
302
-
303
- const body = await res.json() as any;
304
- expect(body.data).toEqual([{ id: "c-1",
305
- text: "Wow" }]);
306
- expect(mockDriver.fetchCollection).toHaveBeenCalledWith(
307
- expect.objectContaining({ path: "authors/123/posts/456/comments" })
308
- );
309
- });
310
-
311
- it("deeply nested entity - GET /authors/123/posts/456/comments/789", async () => {
312
- const app = createFlatApp();
313
- mockDriver.fetchEntity.mockResolvedValue(
314
- { id: "789",
315
- path: "authors/123/posts/456/comments",
316
- values: { text: "Wow" } } as any
317
- );
318
-
319
- const res = await app.request("/authors/123/posts/456/comments/789");
320
- expect(res.status).toBe(200);
321
-
322
- const body = await res.json() as any;
323
- expect(body.id).toBe("789");
324
- expect(mockDriver.fetchEntity).toHaveBeenCalledWith(
325
- expect.objectContaining({
326
- path: "authors/123/posts/456/comments",
327
- entityId: "789"
328
- })
329
- );
330
- });
331
-
332
- it("passes query options when listing subcollection", async () => {
333
- const app = createFlatApp();
334
- mockDriver.fetchCollection.mockResolvedValue([]);
335
-
336
- const res = await app.request("/authors/123/posts?limit=5&orderBy=title:desc&searchString=hello");
337
- expect(res.status).toBe(200);
338
-
339
- expect(mockDriver.fetchCollection).toHaveBeenCalledWith(
340
- expect.objectContaining({
341
- path: "authors/123/posts",
342
- limit: 5,
343
- searchString: "hello"
344
- })
345
- );
346
- });
347
- });
348
-
349
- describe("Subcollection Wildcard Guard (Undefined / Empty Paths)", () => {
350
- /**
351
- * Regression: c.req.param("*") can return "undefined" (literal string)
352
- * when template literal stringifies JS undefined. This caused:
353
- * Error: Relation 'undefined' not found in collection 'authors'
354
- */
355
-
356
- it("GET /authors/123/undefined → 404, no driver calls", async () => {
357
- const app = createFlatApp();
358
- const res = await app.request("/authors/123/undefined");
359
- expect(res.status).toBe(404);
360
- expect(mockDriver.fetchCollection).not.toHaveBeenCalled();
361
- expect(mockDriver.fetchEntity).not.toHaveBeenCalled();
362
- });
363
-
364
- it("POST /authors/123/undefined → 404, no driver calls", async () => {
365
- const app = createFlatApp();
366
- const res = await app.request("/authors/123/undefined", {
367
- method: "POST",
368
- headers: { "Content-Type": "application/json" },
369
- body: JSON.stringify({ title: "test" })
370
- });
371
- expect(res.status).toBe(404);
372
- expect(mockDriver.saveEntity).not.toHaveBeenCalled();
373
- });
374
-
375
- it("PUT /authors/123/undefined → 404, no driver calls", async () => {
376
- const app = createFlatApp();
377
- const res = await app.request("/authors/123/undefined", {
378
- method: "PUT",
379
- headers: { "Content-Type": "application/json" },
380
- body: JSON.stringify({ title: "test" })
381
- });
382
- expect(res.status).toBe(404);
383
- expect(mockDriver.saveEntity).not.toHaveBeenCalled();
384
- });
385
-
386
- it("DELETE /authors/123/undefined → 404, no driver calls", async () => {
387
- const app = createFlatApp();
388
- const res = await app.request("/authors/123/undefined", { method: "DELETE" });
389
- expect(res.status).toBe(404);
390
- expect(mockDriver.deleteEntity).not.toHaveBeenCalled();
391
- });
392
-
393
- it("nested undefined segment is filtered: GET /authors/123/undefined/posts → 404 (not enough valid segments)", async () => {
394
- // After filtering "undefined", segments = ["authors", "123", "posts"] → 3 segments → odd → collection path
395
- // But the wildcard is "undefined/posts", which is NOT the literal "undefined", so the guard won't catch it.
396
- // However, parseSubPath filters "undefined" from segments: ["authors", "123", "posts"] → valid 3-segment path.
397
- const app = createFlatApp();
398
- mockDriver.fetchCollection.mockResolvedValue([]);
399
- const res = await app.request("/authors/123/undefined/posts");
400
- // This should either 200 (if parseSubPath accepts "authors/123/posts") or 404
401
- // Since wildcard = "undefined/posts" which is truthy and not === "undefined", the guard lets it through.
402
- // parseSubPath filters "undefined" segments → ["authors", "123", "posts"] → 3 segments → collection path.
403
- expect(res.status).toBe(200);
404
- expect(mockDriver.fetchCollection).toHaveBeenCalledWith(
405
- expect.objectContaining({ path: "authors/123/posts" })
406
- );
407
- });
408
-
409
- it("returns 404 for bare parent without wildcard - GET /xyz", async () => {
410
- const app = createFlatApp();
411
- const res = await app.request("/xyz");
412
- expect(res.status).toBe(404);
413
- expect(mockDriver.fetchCollection).not.toHaveBeenCalled();
414
- });
415
-
416
- it("returns 404 for only two segments (no relation) - GET /xyz/123", async () => {
417
- // Two segments: would be handled by /:slug/:id route, but "xyz" isn't a registered collection
418
- // so the /:slug route won't match. /:parent/:parentId/* won't match either (no wildcard part).
419
- const app = createFlatApp();
420
- const res = await app.request("/xyz/123");
421
- expect(res.status).toBe(404);
422
- });
423
- });
424
-
425
- describe("Path Parsing Edge Cases via HTTP", () => {
426
- it("numeric IDs are correctly parsed as entityId", async () => {
427
- const app = createFlatApp();
428
- mockDriver.fetchEntity.mockResolvedValue(
429
- { id: "999",
430
- path: "authors/42/books",
431
- values: { title: "Test" } } as any
432
- );
433
-
434
- const res = await app.request("/authors/42/books/999");
435
- expect(res.status).toBe(200);
436
- expect(mockDriver.fetchEntity).toHaveBeenCalledWith(
437
- expect.objectContaining({ path: "authors/42/books",
438
- entityId: "999" })
439
- );
440
- });
441
-
442
- it("UUID-style IDs work as parentId and entityId", async () => {
443
- const app = createFlatApp();
444
- const uuid1 = "a1b2c3d4-e5f6-7890-abcd-ef1234567890";
445
- const uuid2 = "ffffffff-ffff-ffff-ffff-ffffffffffff";
446
- mockDriver.fetchEntity.mockResolvedValue(
447
- { id: uuid2,
448
- path: `authors/${uuid1}/posts`,
449
- values: { title: "UUID" } } as any
450
- );
451
-
452
- const res = await app.request(`/authors/${uuid1}/posts/${uuid2}`);
453
- expect(res.status).toBe(200);
454
- expect(mockDriver.fetchEntity).toHaveBeenCalledWith(
455
- expect.objectContaining({
456
- path: `authors/${uuid1}/posts`,
457
- entityId: uuid2
458
- })
459
- );
460
- });
461
-
462
- it("entity not found returns 404 for subcollection GET", async () => {
463
- const app = createFlatApp();
464
- mockDriver.fetchEntity.mockResolvedValue(undefined);
465
-
466
- const res = await app.request("/authors/123/posts/999");
467
- expect(res.status).toBe(404);
468
- });
469
-
470
- it("POST to even-segment path returns 404 (entityId present, can't create)", async () => {
471
- const app = createFlatApp();
472
- // 4 segments: authors/123/posts/456 — parseSubPath gives entityId="456"
473
- // POST handler checks: if (parsed.entityId) return c.notFound();
474
- const res = await app.request("/authors/123/posts/456", {
475
- method: "POST",
476
- headers: { "Content-Type": "application/json" },
477
- body: JSON.stringify({ title: "nope" })
478
- });
479
- expect(res.status).toBe(404);
480
- expect(mockDriver.saveEntity).not.toHaveBeenCalled();
481
- });
482
-
483
- it("PUT to odd-segment path returns 404 (no entityId, can't update)", async () => {
484
- const app = createFlatApp();
485
- // 3 segments: authors/123/posts — no entityId
486
- // PUT handler checks: if (!parsed.entityId) return c.notFound();
487
- const res = await app.request("/authors/123/posts", {
488
- method: "PUT",
489
- headers: { "Content-Type": "application/json" },
490
- body: JSON.stringify({ title: "nope" })
491
- });
492
- expect(res.status).toBe(404);
493
- expect(mockDriver.saveEntity).not.toHaveBeenCalled();
494
- });
495
-
496
- it("DELETE to odd-segment path returns 404 (no entityId)", async () => {
497
- const app = createFlatApp();
498
- const res = await app.request("/authors/123/posts", { method: "DELETE" });
499
- expect(res.status).toBe(404);
500
- expect(mockDriver.deleteEntity).not.toHaveBeenCalled();
501
- });
502
-
503
- it("DELETE to non-existent entity returns 404", async () => {
504
- const app = createFlatApp();
505
- mockDriver.fetchEntity.mockResolvedValue(undefined);
506
- const res = await app.request("/authors/123/posts/456", { method: "DELETE" });
507
- expect(res.status).toBe(404);
508
- expect(mockDriver.deleteEntity).not.toHaveBeenCalled();
509
- });
510
- });
511
-
512
- describe("Auth Hook Overrides (onAdminCreateUser via AuthAdapter)", () => {
513
-
514
- it("calls adapter.prepareUserCreation and saves user with returned values", async () => {
515
- const mockPrepare = jest.fn().mockResolvedValue({
516
- values: { email: "custom@example.com",
517
- name: "Custom Name",
518
- passwordHash: "custom-hash" },
519
- clearPassword: "custom-temp-password",
520
- hookHandledEmail: true,
521
- invitationSent: true
522
- });
523
-
524
- const mockAuthAdapter = {
525
- id: "test-adapter",
526
- verifyRequest: jest.fn(),
527
- getCapabilities: jest.fn(),
528
- prepareUserCreation: mockPrepare
529
- };
530
-
531
- const app = new Hono();
532
- app.onError(errorHandler);
533
- app.use("/api/*", async (c, next) => {
534
- c.set("driver", mockDriver);
535
- await next();
536
- });
537
-
538
- const authCollections = [
539
- {
540
- slug: "users",
541
- name: "Users",
542
- singularName: "User",
543
- auth: true,
544
- properties: {}
545
- } as any
546
- ];
547
-
548
- const generator = new RestApiGenerator(authCollections, mockDriver, undefined, mockAuthAdapter as any);
549
- app.route("/api", generator.generateRoutes());
550
-
551
- mockDriver.saveEntity.mockResolvedValue({
552
- id: "custom-id",
553
- path: "users",
554
- values: { email: "custom@example.com",
555
- name: "Custom Name",
556
- passwordHash: "custom-hash" }
557
- } as any);
558
-
559
- const res = await app.request("/api/users", {
560
- method: "POST",
561
- headers: { "Content-Type": "application/json" },
562
- body: JSON.stringify({ email: "original@example.com",
563
- name: "Original Name" })
564
- });
565
-
566
- expect(res.status).toBe(201);
567
- const body = await res.json() as any;
568
- expect(body.email).toBe("custom@example.com");
569
- expect(body.invitationSent).toBe(true);
570
- expect(body.temporaryPassword).toBe("custom-temp-password");
571
-
572
- expect(mockPrepare).toHaveBeenCalledWith(
573
- expect.objectContaining({ email: "original@example.com",
574
- name: "Original Name" }),
575
- undefined // auth: true → collectionAuthConfig is undefined
576
- );
577
- expect(mockDriver.saveEntity).toHaveBeenCalledWith(
578
- expect.objectContaining({
579
- path: "users",
580
- values: { email: "custom@example.com",
581
- name: "Custom Name",
582
- passwordHash: "custom-hash" },
583
- status: "new"
584
- })
585
- );
586
- });
587
- });
588
- });