@rebasepro/server-core 0.4.0 → 0.6.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 (282) hide show
  1. package/README.md +62 -25
  2. package/dist/{server-core/src/api → api}/errors.d.ts +20 -2
  3. package/dist/{server-core/src/api → api}/rest/api-generator.d.ts +8 -2
  4. package/dist/{server-core/src/api → api}/types.d.ts +2 -0
  5. package/dist/auth/admin-user-ops.d.ts +79 -0
  6. package/dist/{server-core/src/auth → auth}/api-keys/index.d.ts +2 -2
  7. package/dist/{server-core/src/auth → auth}/auth-hooks.d.ts +37 -9
  8. package/dist/{server-core/src/auth → auth}/builtin-auth-adapter.d.ts +4 -4
  9. package/dist/{server-core/src/auth → auth}/index.d.ts +5 -2
  10. package/dist/{server-core/src/auth → auth}/jwt.d.ts +10 -0
  11. package/dist/auth/mfa-routes.d.ts +6 -0
  12. package/dist/auth/reset-password-admin.d.ts +29 -0
  13. package/dist/auth/session-routes.d.ts +25 -0
  14. package/dist/backend-CIxN4FVm.js +15 -0
  15. package/dist/backend-CIxN4FVm.js.map +1 -0
  16. package/dist/chunk-Dze3rakg.js +42 -0
  17. package/dist/{server-core/src/cron → cron}/cron-scheduler.d.ts +1 -1
  18. package/dist/dist-CZKP-Xz4.js +832 -0
  19. package/dist/dist-CZKP-Xz4.js.map +1 -0
  20. package/dist/env.d.ts +102 -0
  21. package/dist/from-VbwD7xRf.js +3849 -0
  22. package/dist/from-VbwD7xRf.js.map +1 -0
  23. package/dist/{server-core/src/index.d.ts → index.d.ts} +1 -0
  24. package/dist/index.es.js +60871 -50424
  25. package/dist/index.es.js.map +1 -1
  26. package/dist/index.umd.js +73151 -51256
  27. package/dist/index.umd.js.map +1 -1
  28. package/dist/init/docs.d.ts +4 -0
  29. package/dist/init/health.d.ts +2 -0
  30. package/dist/init/middlewares.d.ts +10 -0
  31. package/dist/init/shutdown.d.ts +11 -0
  32. package/dist/init/storage.d.ts +5 -0
  33. package/dist/{server-core/src/init.d.ts → init.d.ts} +4 -4
  34. package/dist/jwt-DHcQRGC3.js +4168 -0
  35. package/dist/jwt-DHcQRGC3.js.map +1 -0
  36. package/dist/logger-BYU66ENZ.js +94 -0
  37. package/dist/logger-BYU66ENZ.js.map +1 -0
  38. package/dist/ms-BeBSuOXN.js +125 -0
  39. package/dist/ms-BeBSuOXN.js.map +1 -0
  40. package/dist/multipart-parser-CedBDOeC.js +299 -0
  41. package/dist/multipart-parser-CedBDOeC.js.map +1 -0
  42. package/dist/src-COaj0G3P.js +1182 -0
  43. package/dist/src-COaj0G3P.js.map +1 -0
  44. package/dist/{server-core/src/storage → storage}/image-transform.d.ts +3 -1
  45. package/dist/utils/request-id.d.ts +4 -0
  46. package/package.json +29 -29
  47. package/src/api/collections_for_test/callbacks_test_collection.ts +7 -6
  48. package/src/api/errors.ts +95 -20
  49. package/src/api/graphql/graphql-schema-generator.ts +15 -10
  50. package/src/api/logs-routes.ts +5 -2
  51. package/src/api/openapi-generator.ts +1 -1
  52. package/src/api/rest/api-generator-count.test.ts +21 -10
  53. package/src/api/rest/api-generator.ts +151 -36
  54. package/src/api/rest/query-parser.ts +18 -12
  55. package/src/api/server.ts +13 -2
  56. package/src/api/types.ts +2 -0
  57. package/src/auth/adapter-middleware.ts +17 -10
  58. package/src/auth/admin-user-ops.ts +236 -0
  59. package/src/auth/api-keys/api-key-middleware.ts +15 -9
  60. package/src/auth/api-keys/api-key-permission-guard.ts +1 -1
  61. package/src/auth/api-keys/api-key-routes.ts +3 -3
  62. package/src/auth/api-keys/api-key-store.ts +14 -8
  63. package/src/auth/api-keys/index.ts +2 -2
  64. package/src/auth/apple-oauth.ts +4 -3
  65. package/src/auth/auth-hooks.ts +50 -10
  66. package/src/auth/bitbucket-oauth.ts +5 -4
  67. package/src/auth/builtin-auth-adapter.ts +64 -54
  68. package/src/auth/custom-auth-adapter.ts +4 -5
  69. package/src/auth/discord-oauth.ts +5 -4
  70. package/src/auth/facebook-oauth.ts +5 -4
  71. package/src/auth/github-oauth.ts +6 -5
  72. package/src/auth/gitlab-oauth.ts +5 -4
  73. package/src/auth/google-oauth.ts +2 -1
  74. package/src/auth/index.ts +6 -2
  75. package/src/auth/jwt.ts +15 -4
  76. package/src/auth/linkedin-oauth.ts +4 -3
  77. package/src/auth/mfa-routes.ts +299 -0
  78. package/src/auth/mfa.ts +2 -1
  79. package/src/auth/microsoft-oauth.ts +5 -4
  80. package/src/auth/middleware.ts +5 -4
  81. package/src/auth/rate-limiter.ts +1 -1
  82. package/src/auth/reset-password-admin.ts +144 -0
  83. package/src/auth/rls-scope.ts +1 -1
  84. package/src/auth/routes.ts +52 -600
  85. package/src/auth/session-routes.ts +341 -0
  86. package/src/auth/slack-oauth.ts +6 -5
  87. package/src/auth/spotify-oauth.ts +5 -4
  88. package/src/auth/twitter-oauth.ts +4 -3
  89. package/src/collections/loader.ts +5 -4
  90. package/src/cron/cron-scheduler.test.ts +23 -11
  91. package/src/cron/cron-scheduler.ts +14 -8
  92. package/src/cron/cron-store.ts +1 -1
  93. package/src/email/smtp-email-service.ts +3 -2
  94. package/src/env.ts +11 -10
  95. package/src/index.ts +1 -0
  96. package/src/init/docs.ts +47 -0
  97. package/src/init/health.ts +37 -0
  98. package/src/init/middlewares.ts +61 -0
  99. package/src/init/shutdown.ts +56 -0
  100. package/src/init/storage.ts +57 -0
  101. package/src/init.ts +93 -295
  102. package/src/serve-spa.ts +12 -5
  103. package/src/services/driver-registry.ts +4 -3
  104. package/src/storage/S3StorageController.ts +1 -2
  105. package/src/storage/image-transform.ts +25 -10
  106. package/src/storage/routes.ts +30 -11
  107. package/src/storage/storage-registry.ts +4 -3
  108. package/src/storage/tus-handler.ts +12 -12
  109. package/src/utils/request-id.ts +40 -0
  110. package/src/utils/request-logger.ts +6 -0
  111. package/test/api-generator.test.ts +90 -2
  112. package/test/api-key-permission-guard.test.ts +24 -12
  113. package/test/auth-routes.test.ts +5 -3
  114. package/test/backend-hooks-data.test.ts +99 -30
  115. package/test/custom-auth-adapter.test.ts +16 -15
  116. package/test/email-templates.test.ts +10 -5
  117. package/test/env.test.ts +10 -10
  118. package/test/function-loader.test.ts +7 -4
  119. package/test/graphql-schema-generator.test.ts +554 -0
  120. package/test/jwt-security.test.ts +1 -1
  121. package/test/query-parser.test.ts +0 -1
  122. package/test/reset-password-admin.test.ts +113 -0
  123. package/test/singleton.test.ts +5 -5
  124. package/test/smtp-email-service.test.ts +21 -21
  125. package/test/webhook-service.test.ts +22 -11
  126. package/tsconfig.json +2 -0
  127. package/tsconfig.prod.json +3 -0
  128. package/vite.config.ts +5 -5
  129. package/dist/common/src/collections/CollectionRegistry.d.ts +0 -56
  130. package/dist/common/src/collections/default-collections.d.ts +0 -9
  131. package/dist/common/src/collections/index.d.ts +0 -2
  132. package/dist/common/src/data/buildRebaseData.d.ts +0 -14
  133. package/dist/common/src/data/query_builder.d.ts +0 -55
  134. package/dist/common/src/index.d.ts +0 -4
  135. package/dist/common/src/util/builders.d.ts +0 -57
  136. package/dist/common/src/util/callbacks.d.ts +0 -6
  137. package/dist/common/src/util/collections.d.ts +0 -11
  138. package/dist/common/src/util/common.d.ts +0 -2
  139. package/dist/common/src/util/conditions.d.ts +0 -26
  140. package/dist/common/src/util/entities.d.ts +0 -58
  141. package/dist/common/src/util/enums.d.ts +0 -3
  142. package/dist/common/src/util/index.d.ts +0 -16
  143. package/dist/common/src/util/navigation_from_path.d.ts +0 -34
  144. package/dist/common/src/util/navigation_utils.d.ts +0 -20
  145. package/dist/common/src/util/parent_references_from_path.d.ts +0 -6
  146. package/dist/common/src/util/paths.d.ts +0 -14
  147. package/dist/common/src/util/permissions.d.ts +0 -6
  148. package/dist/common/src/util/references.d.ts +0 -2
  149. package/dist/common/src/util/relations.d.ts +0 -22
  150. package/dist/common/src/util/resolutions.d.ts +0 -72
  151. package/dist/common/src/util/storage.d.ts +0 -24
  152. package/dist/index-Cr1D21av.js +0 -49
  153. package/dist/index-Cr1D21av.js.map +0 -1
  154. package/dist/server-core/src/auth/admin-routes.d.ts +0 -27
  155. package/dist/server-core/src/env.d.ts +0 -137
  156. package/dist/types/src/controllers/analytics_controller.d.ts +0 -7
  157. package/dist/types/src/controllers/auth.d.ts +0 -104
  158. package/dist/types/src/controllers/client.d.ts +0 -168
  159. package/dist/types/src/controllers/collection_registry.d.ts +0 -46
  160. package/dist/types/src/controllers/customization_controller.d.ts +0 -60
  161. package/dist/types/src/controllers/data.d.ts +0 -207
  162. package/dist/types/src/controllers/data_driver.d.ts +0 -218
  163. package/dist/types/src/controllers/database_admin.d.ts +0 -11
  164. package/dist/types/src/controllers/dialogs_controller.d.ts +0 -36
  165. package/dist/types/src/controllers/effective_role.d.ts +0 -4
  166. package/dist/types/src/controllers/email.d.ts +0 -36
  167. package/dist/types/src/controllers/index.d.ts +0 -18
  168. package/dist/types/src/controllers/local_config_persistence.d.ts +0 -20
  169. package/dist/types/src/controllers/navigation.d.ts +0 -225
  170. package/dist/types/src/controllers/registry.d.ts +0 -63
  171. package/dist/types/src/controllers/side_dialogs_controller.d.ts +0 -67
  172. package/dist/types/src/controllers/side_entity_controller.d.ts +0 -97
  173. package/dist/types/src/controllers/snackbar.d.ts +0 -24
  174. package/dist/types/src/controllers/storage.d.ts +0 -171
  175. package/dist/types/src/index.d.ts +0 -4
  176. package/dist/types/src/rebase_context.d.ts +0 -122
  177. package/dist/types/src/types/auth_adapter.d.ts +0 -301
  178. package/dist/types/src/types/backend.d.ts +0 -536
  179. package/dist/types/src/types/backend_hooks.d.ts +0 -172
  180. package/dist/types/src/types/builders.d.ts +0 -15
  181. package/dist/types/src/types/chips.d.ts +0 -5
  182. package/dist/types/src/types/collections.d.ts +0 -941
  183. package/dist/types/src/types/component_ref.d.ts +0 -47
  184. package/dist/types/src/types/cron.d.ts +0 -102
  185. package/dist/types/src/types/data_source.d.ts +0 -64
  186. package/dist/types/src/types/database_adapter.d.ts +0 -94
  187. package/dist/types/src/types/entities.d.ts +0 -145
  188. package/dist/types/src/types/entity_actions.d.ts +0 -104
  189. package/dist/types/src/types/entity_callbacks.d.ts +0 -173
  190. package/dist/types/src/types/entity_link_builder.d.ts +0 -7
  191. package/dist/types/src/types/entity_overrides.d.ts +0 -10
  192. package/dist/types/src/types/entity_views.d.ts +0 -87
  193. package/dist/types/src/types/export_import.d.ts +0 -21
  194. package/dist/types/src/types/formex.d.ts +0 -40
  195. package/dist/types/src/types/index.d.ts +0 -28
  196. package/dist/types/src/types/locales.d.ts +0 -4
  197. package/dist/types/src/types/modify_collections.d.ts +0 -5
  198. package/dist/types/src/types/plugins.d.ts +0 -282
  199. package/dist/types/src/types/properties.d.ts +0 -1181
  200. package/dist/types/src/types/property_config.d.ts +0 -74
  201. package/dist/types/src/types/relations.d.ts +0 -336
  202. package/dist/types/src/types/slots.d.ts +0 -262
  203. package/dist/types/src/types/translations.d.ts +0 -900
  204. package/dist/types/src/types/user_management_delegate.d.ts +0 -86
  205. package/dist/types/src/types/websockets.d.ts +0 -78
  206. package/dist/types/src/users/index.d.ts +0 -1
  207. package/dist/types/src/users/user.d.ts +0 -50
  208. package/src/auth/admin-routes.ts +0 -534
  209. package/test/admin-routes.test.ts +0 -544
  210. package/test/backend-hooks-admin.test.ts +0 -388
  211. package/test.ts +0 -6
  212. /package/dist/{server-core/src/api → api}/ast-schema-editor.d.ts +0 -0
  213. /package/dist/{server-core/src/api → api}/collections_for_test/callbacks_test_collection.d.ts +0 -0
  214. /package/dist/{server-core/src/api → api}/graphql/graphql-schema-generator.d.ts +0 -0
  215. /package/dist/{server-core/src/api → api}/graphql/index.d.ts +0 -0
  216. /package/dist/{server-core/src/api → api}/index.d.ts +0 -0
  217. /package/dist/{server-core/src/api → api}/logs-routes.d.ts +0 -0
  218. /package/dist/{server-core/src/api → api}/openapi-generator.d.ts +0 -0
  219. /package/dist/{server-core/src/api → api}/rest/index.d.ts +0 -0
  220. /package/dist/{server-core/src/api → api}/rest/query-parser.d.ts +0 -0
  221. /package/dist/{server-core/src/api → api}/schema-editor-routes.d.ts +0 -0
  222. /package/dist/{server-core/src/api → api}/server.d.ts +0 -0
  223. /package/dist/{server-core/src/auth → auth}/adapter-middleware.d.ts +0 -0
  224. /package/dist/{server-core/src/auth → auth}/api-keys/api-key-middleware.d.ts +0 -0
  225. /package/dist/{server-core/src/auth → auth}/api-keys/api-key-permission-guard.d.ts +0 -0
  226. /package/dist/{server-core/src/auth → auth}/api-keys/api-key-routes.d.ts +0 -0
  227. /package/dist/{server-core/src/auth → auth}/api-keys/api-key-store.d.ts +0 -0
  228. /package/dist/{server-core/src/auth → auth}/api-keys/api-key-types.d.ts +0 -0
  229. /package/dist/{server-core/src/auth → auth}/apple-oauth.d.ts +0 -0
  230. /package/dist/{server-core/src/auth → auth}/bitbucket-oauth.d.ts +0 -0
  231. /package/dist/{server-core/src/auth → auth}/crypto-utils.d.ts +0 -0
  232. /package/dist/{server-core/src/auth → auth}/custom-auth-adapter.d.ts +0 -0
  233. /package/dist/{server-core/src/auth → auth}/discord-oauth.d.ts +0 -0
  234. /package/dist/{server-core/src/auth → auth}/facebook-oauth.d.ts +0 -0
  235. /package/dist/{server-core/src/auth → auth}/github-oauth.d.ts +0 -0
  236. /package/dist/{server-core/src/auth → auth}/gitlab-oauth.d.ts +0 -0
  237. /package/dist/{server-core/src/auth → auth}/google-oauth.d.ts +0 -0
  238. /package/dist/{server-core/src/auth → auth}/interfaces.d.ts +0 -0
  239. /package/dist/{server-core/src/auth → auth}/linkedin-oauth.d.ts +0 -0
  240. /package/dist/{server-core/src/auth → auth}/mfa.d.ts +0 -0
  241. /package/dist/{server-core/src/auth → auth}/microsoft-oauth.d.ts +0 -0
  242. /package/dist/{server-core/src/auth → auth}/middleware.d.ts +0 -0
  243. /package/dist/{server-core/src/auth → auth}/password.d.ts +0 -0
  244. /package/dist/{server-core/src/auth → auth}/rate-limiter.d.ts +0 -0
  245. /package/dist/{server-core/src/auth → auth}/rls-scope.d.ts +0 -0
  246. /package/dist/{server-core/src/auth → auth}/routes.d.ts +0 -0
  247. /package/dist/{server-core/src/auth → auth}/slack-oauth.d.ts +0 -0
  248. /package/dist/{server-core/src/auth → auth}/spotify-oauth.d.ts +0 -0
  249. /package/dist/{server-core/src/auth → auth}/twitter-oauth.d.ts +0 -0
  250. /package/dist/{server-core/src/collections → collections}/BackendCollectionRegistry.d.ts +0 -0
  251. /package/dist/{server-core/src/collections → collections}/loader.d.ts +0 -0
  252. /package/dist/{server-core/src/cron → cron}/cron-loader.d.ts +0 -0
  253. /package/dist/{server-core/src/cron → cron}/cron-routes.d.ts +0 -0
  254. /package/dist/{server-core/src/cron → cron}/cron-store.d.ts +0 -0
  255. /package/dist/{server-core/src/cron → cron}/index.d.ts +0 -0
  256. /package/dist/{server-core/src/db → db}/interfaces.d.ts +0 -0
  257. /package/dist/{server-core/src/email → email}/index.d.ts +0 -0
  258. /package/dist/{server-core/src/email → email}/smtp-email-service.d.ts +0 -0
  259. /package/dist/{server-core/src/email → email}/templates.d.ts +0 -0
  260. /package/dist/{server-core/src/email → email}/types.d.ts +0 -0
  261. /package/dist/{server-core/src/functions → functions}/function-loader.d.ts +0 -0
  262. /package/dist/{server-core/src/functions → functions}/function-routes.d.ts +0 -0
  263. /package/dist/{server-core/src/functions → functions}/index.d.ts +0 -0
  264. /package/dist/{server-core/src/history → history}/history-routes.d.ts +0 -0
  265. /package/dist/{server-core/src/history → history}/index.d.ts +0 -0
  266. /package/dist/{server-core/src/serve-spa.d.ts → serve-spa.d.ts} +0 -0
  267. /package/dist/{server-core/src/services → services}/driver-registry.d.ts +0 -0
  268. /package/dist/{server-core/src/services → services}/webhook-service.d.ts +0 -0
  269. /package/dist/{server-core/src/singleton.d.ts → singleton.d.ts} +0 -0
  270. /package/dist/{server-core/src/storage → storage}/LocalStorageController.d.ts +0 -0
  271. /package/dist/{server-core/src/storage → storage}/S3StorageController.d.ts +0 -0
  272. /package/dist/{server-core/src/storage → storage}/index.d.ts +0 -0
  273. /package/dist/{server-core/src/storage → storage}/routes.d.ts +0 -0
  274. /package/dist/{server-core/src/storage → storage}/storage-registry.d.ts +0 -0
  275. /package/dist/{server-core/src/storage → storage}/tus-handler.d.ts +0 -0
  276. /package/dist/{server-core/src/storage → storage}/types.d.ts +0 -0
  277. /package/dist/{server-core/src/types → types}/index.d.ts +0 -0
  278. /package/dist/{server-core/src/utils → utils}/dev-port.d.ts +0 -0
  279. /package/dist/{server-core/src/utils → utils}/logger.d.ts +0 -0
  280. /package/dist/{server-core/src/utils → utils}/logging.d.ts +0 -0
  281. /package/dist/{server-core/src/utils → utils}/request-logger.d.ts +0 -0
  282. /package/dist/{server-core/src/utils → utils}/sql.d.ts +0 -0
@@ -0,0 +1,554 @@
1
+ import { GraphQLSchemaGenerator } from "../src/api/graphql/graphql-schema-generator";
2
+ import { DataDriver } from "../../types/src/controllers/data_driver";
3
+ import { EntityCollection } from "../../types/src/types/collections";
4
+ import {
5
+ GraphQLSchema,
6
+ GraphQLObjectType,
7
+ GraphQLNonNull,
8
+ GraphQLString,
9
+ GraphQLFloat,
10
+ GraphQLBoolean,
11
+ GraphQLList,
12
+ GraphQLInputObjectType,
13
+ graphql,
14
+ printSchema
15
+ } from "graphql";
16
+
17
+ describe("GraphQLSchemaGenerator", () => {
18
+ let mockDriver: jest.Mocked<DataDriver>;
19
+ let collections: EntityCollection[];
20
+
21
+ beforeEach(() => {
22
+ mockDriver = {
23
+ key: "postgres",
24
+ initialised: true,
25
+ fetchCollection: jest.fn(),
26
+ listenCollection: jest.fn(),
27
+ fetchEntity: jest.fn(),
28
+ listenEntity: jest.fn(),
29
+ saveEntity: jest.fn(),
30
+ deleteEntity: jest.fn(),
31
+ checkUniqueField: jest.fn(),
32
+ countEntities: jest.fn(),
33
+ withAuth: jest.fn(),
34
+ admin: {} as any
35
+ } as unknown as jest.Mocked<DataDriver>;
36
+
37
+ collections = [
38
+ {
39
+ slug: "posts",
40
+ name: "Posts",
41
+ singularName: "Post",
42
+ description: "Blog posts",
43
+ properties: {
44
+ title: { name: "Title",
45
+ type: "string",
46
+ validation: { required: true } },
47
+ body: { name: "Body",
48
+ type: "string" },
49
+ views: { name: "Views",
50
+ type: "number" },
51
+ published: { name: "Published",
52
+ type: "boolean" },
53
+ createdAt: { name: "Created At",
54
+ type: "date" }
55
+ }
56
+ } as unknown as EntityCollection,
57
+ {
58
+ slug: "authors",
59
+ name: "Authors",
60
+ singularName: "Author",
61
+ properties: {
62
+ name: { name: "Name",
63
+ type: "string",
64
+ validation: { required: true } },
65
+ bio: { name: "Bio",
66
+ type: "string" }
67
+ }
68
+ } as unknown as EntityCollection
69
+ ];
70
+ });
71
+
72
+ // ─── Schema Generation ──────────────────────────────────────────
73
+
74
+ describe("generateSchema", () => {
75
+ it("returns a valid GraphQLSchema", () => {
76
+ const generator = new GraphQLSchemaGenerator(collections, mockDriver);
77
+ const schema = generator.generateSchema();
78
+ expect(schema).toBeInstanceOf(GraphQLSchema);
79
+ });
80
+
81
+ it("creates Query and Mutation root types", () => {
82
+ const generator = new GraphQLSchemaGenerator(collections, mockDriver);
83
+ const schema = generator.generateSchema();
84
+ expect(schema.getQueryType()).toBeDefined();
85
+ expect(schema.getMutationType()).toBeDefined();
86
+ });
87
+
88
+ it("generates a printable SDL without errors", () => {
89
+ const generator = new GraphQLSchemaGenerator(collections, mockDriver);
90
+ const schema = generator.generateSchema();
91
+ const sdl = printSchema(schema);
92
+ expect(sdl).toContain("type Post");
93
+ expect(sdl).toContain("type Author");
94
+ expect(sdl).toContain("type Query");
95
+ expect(sdl).toContain("type Mutation");
96
+ });
97
+ });
98
+
99
+ // ─── Entity Types ───────────────────────────────────────────────
100
+
101
+ describe("entity types", () => {
102
+ it("creates an object type for each collection", () => {
103
+ const generator = new GraphQLSchemaGenerator(collections, mockDriver);
104
+ const schema = generator.generateSchema();
105
+ const typeMap = schema.getTypeMap();
106
+ expect(typeMap["Post"]).toBeInstanceOf(GraphQLObjectType);
107
+ expect(typeMap["Author"]).toBeInstanceOf(GraphQLObjectType);
108
+ });
109
+
110
+ it("generates an id field as NonNull String", () => {
111
+ const generator = new GraphQLSchemaGenerator(collections, mockDriver);
112
+ const schema = generator.generateSchema();
113
+ const postType = schema.getType("Post") as GraphQLObjectType;
114
+ const fields = postType.getFields();
115
+ expect(fields.id).toBeDefined();
116
+ expect(fields.id.type).toBeInstanceOf(GraphQLNonNull);
117
+ expect((fields.id.type as GraphQLNonNull<any>).ofType).toBe(GraphQLString);
118
+ });
119
+
120
+ it("maps string properties to GraphQLString", () => {
121
+ const generator = new GraphQLSchemaGenerator(collections, mockDriver);
122
+ const schema = generator.generateSchema();
123
+ const postType = schema.getType("Post") as GraphQLObjectType;
124
+ const bodyField = postType.getFields().body;
125
+ expect(bodyField.type).toBe(GraphQLString);
126
+ });
127
+
128
+ it("maps number properties to GraphQLFloat", () => {
129
+ const generator = new GraphQLSchemaGenerator(collections, mockDriver);
130
+ const schema = generator.generateSchema();
131
+ const postType = schema.getType("Post") as GraphQLObjectType;
132
+ const viewsField = postType.getFields().views;
133
+ expect(viewsField.type).toBe(GraphQLFloat);
134
+ });
135
+
136
+ it("maps boolean properties to GraphQLBoolean", () => {
137
+ const generator = new GraphQLSchemaGenerator(collections, mockDriver);
138
+ const schema = generator.generateSchema();
139
+ const postType = schema.getType("Post") as GraphQLObjectType;
140
+ const publishedField = postType.getFields().published;
141
+ expect(publishedField.type).toBe(GraphQLBoolean);
142
+ });
143
+
144
+ it("maps date properties to GraphQLString", () => {
145
+ const generator = new GraphQLSchemaGenerator(collections, mockDriver);
146
+ const schema = generator.generateSchema();
147
+ const postType = schema.getType("Post") as GraphQLObjectType;
148
+ const createdAtField = postType.getFields().createdAt;
149
+ // Date is non-null because the original property definition doesn't set required,
150
+ // so it should be nullable GraphQLString
151
+ expect(createdAtField).toBeDefined();
152
+ });
153
+
154
+ it("wraps required properties in NonNull", () => {
155
+ const generator = new GraphQLSchemaGenerator(collections, mockDriver);
156
+ const schema = generator.generateSchema();
157
+ const postType = schema.getType("Post") as GraphQLObjectType;
158
+ const titleField = postType.getFields().title;
159
+ expect(titleField.type).toBeInstanceOf(GraphQLNonNull);
160
+ });
161
+
162
+ it("skips relation properties from entity fields", () => {
163
+ const withRelation: EntityCollection[] = [
164
+ {
165
+ slug: "comments",
166
+ name: "Comments",
167
+ singularName: "Comment",
168
+ properties: {
169
+ text: { name: "Text",
170
+ type: "string" },
171
+ author: { name: "Author",
172
+ type: "relation" }
173
+ }
174
+ } as unknown as EntityCollection
175
+ ];
176
+ const generator = new GraphQLSchemaGenerator(withRelation, mockDriver);
177
+ const schema = generator.generateSchema();
178
+ const commentType = schema.getType("Comment") as GraphQLObjectType;
179
+ const fields = commentType.getFields();
180
+ expect(fields.text).toBeDefined();
181
+ expect(fields.author).toBeUndefined();
182
+ });
183
+ });
184
+
185
+ // ─── Input Types ────────────────────────────────────────────────
186
+
187
+ describe("input types", () => {
188
+ it("creates input types for each collection", () => {
189
+ const generator = new GraphQLSchemaGenerator(collections, mockDriver);
190
+ const schema = generator.generateSchema();
191
+ const typeMap = schema.getTypeMap();
192
+ expect(typeMap["PostInput"]).toBeInstanceOf(GraphQLInputObjectType);
193
+ expect(typeMap["AuthorInput"]).toBeInstanceOf(GraphQLInputObjectType);
194
+ });
195
+
196
+ it("input types skip relation properties", () => {
197
+ const withRelation: EntityCollection[] = [
198
+ {
199
+ slug: "comments",
200
+ name: "Comments",
201
+ singularName: "Comment",
202
+ properties: {
203
+ text: { name: "Text",
204
+ type: "string" },
205
+ post: { name: "Post",
206
+ type: "relation" }
207
+ }
208
+ } as unknown as EntityCollection
209
+ ];
210
+ const generator = new GraphQLSchemaGenerator(withRelation, mockDriver);
211
+ const schema = generator.generateSchema();
212
+ const inputType = schema.getType("CommentInput") as GraphQLInputObjectType;
213
+ const fields = inputType.getFields();
214
+ expect(fields.text).toBeDefined();
215
+ expect(fields.post).toBeUndefined();
216
+ });
217
+ });
218
+
219
+ // ─── Query Resolvers ────────────────────────────────────────────
220
+
221
+ describe("query resolvers", () => {
222
+ it("creates single-entity and list queries for each collection", () => {
223
+ const generator = new GraphQLSchemaGenerator(collections, mockDriver);
224
+ const schema = generator.generateSchema();
225
+ const queryType = schema.getQueryType()!;
226
+ const fields = queryType.getFields();
227
+
228
+ // Single entity queries (lowercase singular name)
229
+ expect(fields.post).toBeDefined();
230
+ expect(fields.author).toBeDefined();
231
+
232
+ // List queries (slug)
233
+ expect(fields.posts).toBeDefined();
234
+ expect(fields.authors).toBeDefined();
235
+ });
236
+
237
+ it("single-entity query calls fetchEntity with correct args", async () => {
238
+ const generator = new GraphQLSchemaGenerator(collections, mockDriver);
239
+ const schema = generator.generateSchema();
240
+
241
+ const mockEntity = {
242
+ id: "1",
243
+ path: "posts",
244
+ values: { title: "Hello World",
245
+ body: "Content" }
246
+ };
247
+ mockDriver.fetchEntity.mockResolvedValue(mockEntity as any);
248
+
249
+ const result = await graphql({
250
+ schema,
251
+ source: "query { post(id: \"1\") { id title body } }",
252
+ contextValue: { driver: mockDriver }
253
+ });
254
+
255
+ expect(result.errors).toBeUndefined();
256
+ expect(mockDriver.fetchEntity).toHaveBeenCalledWith(
257
+ expect.objectContaining({
258
+ path: "posts",
259
+ entityId: "1"
260
+ })
261
+ );
262
+ });
263
+
264
+ it("list query calls fetchCollection with limit and offset", async () => {
265
+ const generator = new GraphQLSchemaGenerator(collections, mockDriver);
266
+ const schema = generator.generateSchema();
267
+
268
+ mockDriver.fetchCollection.mockResolvedValue([]);
269
+
270
+ const result = await graphql({
271
+ schema,
272
+ source: "query { posts(limit: 10, offset: 5) { id title } }",
273
+ contextValue: { driver: mockDriver }
274
+ });
275
+
276
+ expect(result.errors).toBeUndefined();
277
+ expect(mockDriver.fetchCollection).toHaveBeenCalledWith(
278
+ expect.objectContaining({
279
+ path: "posts",
280
+ limit: 10,
281
+ startAfter: "5"
282
+ })
283
+ );
284
+ });
285
+
286
+ it("list query parses JSON where filter", async () => {
287
+ const generator = new GraphQLSchemaGenerator(collections, mockDriver);
288
+ const schema = generator.generateSchema();
289
+
290
+ mockDriver.fetchCollection.mockResolvedValue([]);
291
+
292
+ const result = await graphql({
293
+ schema,
294
+ source: "query { posts(where: \"{\\\"published\\\": true}\") { id } }",
295
+ contextValue: { driver: mockDriver }
296
+ });
297
+
298
+ expect(result.errors).toBeUndefined();
299
+ expect(mockDriver.fetchCollection).toHaveBeenCalledWith(
300
+ expect.objectContaining({
301
+ filter: { published: true }
302
+ })
303
+ );
304
+ });
305
+
306
+ it("list query rejects invalid JSON where filter", async () => {
307
+ const generator = new GraphQLSchemaGenerator(collections, mockDriver);
308
+ const schema = generator.generateSchema();
309
+
310
+ const result = await graphql({
311
+ schema,
312
+ source: "query { posts(where: \"not-json\") { id } }",
313
+ contextValue: { driver: mockDriver }
314
+ });
315
+
316
+ expect(result.errors).toBeDefined();
317
+ expect(result.errors![0].message).toContain("Invalid 'where' filter");
318
+ });
319
+
320
+ it("list query rejects non-object JSON where filter", async () => {
321
+ const generator = new GraphQLSchemaGenerator(collections, mockDriver);
322
+ const schema = generator.generateSchema();
323
+
324
+ const result = await graphql({
325
+ schema,
326
+ source: "query { posts(where: \"[1,2,3]\") { id } }",
327
+ contextValue: { driver: mockDriver }
328
+ });
329
+
330
+ expect(result.errors).toBeDefined();
331
+ expect(result.errors![0].message).toContain("Filter must be a JSON object");
332
+ });
333
+ });
334
+
335
+ // ─── Mutation Resolvers ─────────────────────────────────────────
336
+
337
+ describe("mutation resolvers", () => {
338
+ it("creates create, update, and delete mutations for each collection", () => {
339
+ const generator = new GraphQLSchemaGenerator(collections, mockDriver);
340
+ const schema = generator.generateSchema();
341
+ const mutationType = schema.getMutationType()!;
342
+ const fields = mutationType.getFields();
343
+
344
+ expect(fields.createPost).toBeDefined();
345
+ expect(fields.updatePost).toBeDefined();
346
+ expect(fields.deletePost).toBeDefined();
347
+ expect(fields.createAuthor).toBeDefined();
348
+ expect(fields.updateAuthor).toBeDefined();
349
+ expect(fields.deleteAuthor).toBeDefined();
350
+ });
351
+
352
+ it("create mutation calls saveEntity with status 'new'", async () => {
353
+ const generator = new GraphQLSchemaGenerator(collections, mockDriver);
354
+ const schema = generator.generateSchema();
355
+
356
+ const mockEntity = {
357
+ id: "1",
358
+ path: "posts",
359
+ values: { title: "New Post" }
360
+ };
361
+ mockDriver.saveEntity.mockResolvedValue(mockEntity as any);
362
+
363
+ const result = await graphql({
364
+ schema,
365
+ source: "mutation { createPost(input: { title: \"New Post\" }) { id } }",
366
+ contextValue: { driver: mockDriver }
367
+ });
368
+
369
+ expect(result.errors).toBeUndefined();
370
+ expect(mockDriver.saveEntity).toHaveBeenCalledWith(
371
+ expect.objectContaining({
372
+ path: "posts",
373
+ values: { title: "New Post" },
374
+ status: "new"
375
+ })
376
+ );
377
+ });
378
+
379
+ it("update mutation calls saveEntity with status 'existing' and entityId", async () => {
380
+ const generator = new GraphQLSchemaGenerator(collections, mockDriver);
381
+ const schema = generator.generateSchema();
382
+
383
+ const mockEntity = {
384
+ id: "1",
385
+ path: "posts",
386
+ values: { title: "Updated" }
387
+ };
388
+ mockDriver.saveEntity.mockResolvedValue(mockEntity as any);
389
+
390
+ const result = await graphql({
391
+ schema,
392
+ source: "mutation { updatePost(id: \"1\", input: { title: \"Updated\" }) { id } }",
393
+ contextValue: { driver: mockDriver }
394
+ });
395
+
396
+ expect(result.errors).toBeUndefined();
397
+ expect(mockDriver.saveEntity).toHaveBeenCalledWith(
398
+ expect.objectContaining({
399
+ path: "posts",
400
+ entityId: "1",
401
+ values: { title: "Updated" },
402
+ status: "existing"
403
+ })
404
+ );
405
+ });
406
+
407
+ it("delete mutation returns true on success", async () => {
408
+ const generator = new GraphQLSchemaGenerator(collections, mockDriver);
409
+ const schema = generator.generateSchema();
410
+
411
+ const mockEntity = { id: "1",
412
+ path: "posts",
413
+ values: {} };
414
+ mockDriver.fetchEntity.mockResolvedValue(mockEntity as any);
415
+ mockDriver.deleteEntity.mockResolvedValue(undefined);
416
+
417
+ const result = await graphql({
418
+ schema,
419
+ source: "mutation { deletePost(id: \"1\") }",
420
+ contextValue: { driver: mockDriver }
421
+ });
422
+
423
+ expect(result.errors).toBeUndefined();
424
+ expect(result.data?.deletePost).toBe(true);
425
+ expect(mockDriver.deleteEntity).toHaveBeenCalledWith(
426
+ expect.objectContaining({
427
+ entity: mockEntity
428
+ })
429
+ );
430
+ });
431
+
432
+ it("delete mutation returns false when entity not found", async () => {
433
+ const generator = new GraphQLSchemaGenerator(collections, mockDriver);
434
+ const schema = generator.generateSchema();
435
+
436
+ mockDriver.fetchEntity.mockResolvedValue(undefined as any);
437
+
438
+ const result = await graphql({
439
+ schema,
440
+ source: "mutation { deletePost(id: \"nonexistent\") }",
441
+ contextValue: { driver: mockDriver }
442
+ });
443
+
444
+ expect(result.errors).toBeUndefined();
445
+ expect(result.data?.deletePost).toBe(false);
446
+ });
447
+
448
+ it("delete mutation returns false when deleteEntity throws", async () => {
449
+ const generator = new GraphQLSchemaGenerator(collections, mockDriver);
450
+ const schema = generator.generateSchema();
451
+
452
+ const mockEntity = { id: "1",
453
+ path: "posts",
454
+ values: {} };
455
+ mockDriver.fetchEntity.mockResolvedValue(mockEntity as any);
456
+ mockDriver.deleteEntity.mockRejectedValue(new Error("DB error"));
457
+
458
+ const result = await graphql({
459
+ schema,
460
+ source: "mutation { deletePost(id: \"1\") }",
461
+ contextValue: { driver: mockDriver }
462
+ });
463
+
464
+ expect(result.errors).toBeUndefined();
465
+ expect(result.data?.deletePost).toBe(false);
466
+ });
467
+ });
468
+
469
+ // ─── Edge Cases ─────────────────────────────────────────────────
470
+
471
+ describe("edge cases", () => {
472
+ it("handles empty collections array", () => {
473
+ const generator = new GraphQLSchemaGenerator([], mockDriver);
474
+ const schema = generator.generateSchema();
475
+ expect(schema).toBeInstanceOf(GraphQLSchema);
476
+ });
477
+
478
+ it("handles collection with array property", () => {
479
+ const arrayCollection: EntityCollection[] = [
480
+ {
481
+ slug: "tags",
482
+ name: "Tags",
483
+ singularName: "Tag",
484
+ properties: {
485
+ labels: { name: "Labels",
486
+ type: "array" }
487
+ }
488
+ } as unknown as EntityCollection
489
+ ];
490
+ const generator = new GraphQLSchemaGenerator(arrayCollection, mockDriver);
491
+ const schema = generator.generateSchema();
492
+ const tagType = schema.getType("Tag") as GraphQLObjectType;
493
+ const labelsField = tagType.getFields().labels;
494
+ expect(labelsField.type).toBeInstanceOf(GraphQLList);
495
+ });
496
+
497
+ it("handles collection with vector property", () => {
498
+ const vectorCollection: EntityCollection[] = [
499
+ {
500
+ slug: "embeddings",
501
+ name: "Embeddings",
502
+ singularName: "Embedding",
503
+ properties: {
504
+ embedding: { name: "Embedding",
505
+ type: "vector" }
506
+ }
507
+ } as unknown as EntityCollection
508
+ ];
509
+ const generator = new GraphQLSchemaGenerator(vectorCollection, mockDriver);
510
+ const schema = generator.generateSchema();
511
+ const embeddingType = schema.getType("Embedding") as GraphQLObjectType;
512
+ const embeddingField = embeddingType.getFields().embedding;
513
+ expect(embeddingField.type).toBeInstanceOf(GraphQLList);
514
+ });
515
+
516
+ it("handles collection with binary property", () => {
517
+ const binaryCollection: EntityCollection[] = [
518
+ {
519
+ slug: "files",
520
+ name: "Files",
521
+ singularName: "File",
522
+ properties: {
523
+ data: { name: "Data",
524
+ type: "binary" }
525
+ }
526
+ } as unknown as EntityCollection
527
+ ];
528
+ const generator = new GraphQLSchemaGenerator(binaryCollection, mockDriver);
529
+ const schema = generator.generateSchema();
530
+ const fileType = schema.getType("File") as GraphQLObjectType;
531
+ const dataField = fileType.getFields().data;
532
+ expect(dataField.type).toBe(GraphQLString);
533
+ });
534
+
535
+ it("deduplicates types when generateSchema is called", () => {
536
+ const dupeCollections: EntityCollection[] = [
537
+ {
538
+ slug: "items",
539
+ name: "Items",
540
+ singularName: "Item",
541
+ properties: {
542
+ name: { name: "Name",
543
+ type: "string" }
544
+ }
545
+ } as unknown as EntityCollection
546
+ ];
547
+ const generator = new GraphQLSchemaGenerator(dupeCollections, mockDriver);
548
+ // Calling generateSchema should not throw even if types are created internally multiple times
549
+ const schema = generator.generateSchema();
550
+ expect(schema.getType("Item")).toBeDefined();
551
+ expect(schema.getType("ItemInput")).toBeDefined();
552
+ });
553
+ });
554
+ });
@@ -66,7 +66,7 @@ refreshExpiresIn: "30d" });
66
66
 
67
67
  it("throws when secret is not configured", () => {
68
68
  // Force empty secret
69
- // eslint-disable-next-line @typescript-eslint/no-require-imports
69
+
70
70
  Object.defineProperty(require("../src/auth/jwt"), "jwtConfig", { value: { secret: "" },
71
71
  writable: true });
72
72
  // This won't work since jwtConfig is module-scoped, but generateAccessToken has its own check
@@ -157,7 +157,6 @@ describe("parseQueryOptions — PostgREST filters", () => {
157
157
  });
158
158
 
159
159
 
160
-
161
160
  // ─────────────────────────────────────────────────────────────
162
161
  // parseQueryOptions — Sorting
163
162
  // ─────────────────────────────────────────────────────────────
@@ -0,0 +1,113 @@
1
+ import { Hono } from "hono";
2
+ import { createResetPasswordRoute } from "../src/auth/reset-password-admin";
3
+ import type { AuthRepository } from "../src/auth/interfaces";
4
+ import { configureJwt, generateAccessToken } from "../src/auth/jwt";
5
+ import { errorHandler } from "../src/api/errors";
6
+
7
+ const TEST_SECRET = "integration-test-secret-key-that-is-definitely-32-chars-long!!";
8
+
9
+ describe("createResetPasswordRoute & onAdminResetPassword", () => {
10
+ let mockAuthRepo: jest.Mocked<AuthRepository>;
11
+ let mockEmailService: { send: jest.Mock; isConfigured: jest.Mock };
12
+
13
+ beforeAll(() => {
14
+ configureJwt({ secret: TEST_SECRET,
15
+ accessExpiresIn: "1h" });
16
+ });
17
+
18
+ beforeEach(() => {
19
+ mockAuthRepo = {
20
+ getUserById: jest.fn(),
21
+ getUserRoleIds: jest.fn().mockResolvedValue(["admin"]),
22
+ updatePassword: jest.fn().mockResolvedValue(undefined),
23
+ createPasswordResetToken: jest.fn().mockResolvedValue(undefined)
24
+ } as unknown as jest.Mocked<AuthRepository>;
25
+
26
+ mockEmailService = {
27
+ send: jest.fn().mockResolvedValue(undefined),
28
+ isConfigured: jest.fn().mockReturnValue(true)
29
+ };
30
+ });
31
+
32
+ function createApp(authHooks?: any) {
33
+ const app = new Hono();
34
+ app.onError(errorHandler);
35
+ const adminRoutes = createResetPasswordRoute({
36
+ authRepo: mockAuthRepo,
37
+ emailService: mockEmailService as any,
38
+ emailConfig: {
39
+ from: "test@example.com",
40
+ resetPasswordUrl: "https://reset.com"
41
+ },
42
+ authHooks
43
+ });
44
+ app.route("/api/admin", adminRoutes);
45
+ return app;
46
+ }
47
+
48
+ it("falls back to default password reset logic when no hook is provided", async () => {
49
+ mockAuthRepo.getUserById.mockResolvedValue({
50
+ id: "user-123",
51
+ email: "user@example.com",
52
+ displayName: "User One"
53
+ } as any);
54
+
55
+ const app = createApp();
56
+ const adminToken = generateAccessToken("admin-user", ["admin"]);
57
+
58
+ const res = await app.request("/api/admin/users/user-123/reset-password", {
59
+ method: "POST",
60
+ headers: {
61
+ "Authorization": `Bearer ${adminToken}`
62
+ }
63
+ });
64
+
65
+ expect(res.status).toBe(200);
66
+ const body = await res.json() as any;
67
+ expect(body.invitationSent).toBe(true);
68
+ expect(body.user.uid).toBe("user-123");
69
+ expect(mockAuthRepo.createPasswordResetToken).toHaveBeenCalled();
70
+ expect(mockEmailService.send).toHaveBeenCalled();
71
+ });
72
+
73
+ it("calls onAdminResetPassword hook when provided", async () => {
74
+ mockAuthRepo.getUserById.mockResolvedValue({
75
+ id: "user-123",
76
+ email: "user@example.com",
77
+ displayName: "User One"
78
+ } as any);
79
+
80
+ const onAdminResetPasswordMock = jest.fn().mockResolvedValue({
81
+ temporaryPassword: "hook-temp-password",
82
+ invitationSent: false
83
+ });
84
+
85
+ const app = createApp({
86
+ onAdminResetPassword: onAdminResetPasswordMock
87
+ });
88
+ const adminToken = generateAccessToken("admin-user", ["admin"]);
89
+
90
+ const res = await app.request("/api/admin/users/user-123/reset-password", {
91
+ method: "POST",
92
+ headers: {
93
+ "Authorization": `Bearer ${adminToken}`
94
+ }
95
+ });
96
+
97
+ expect(res.status).toBe(200);
98
+ const body = await res.json() as any;
99
+ expect(body.invitationSent).toBe(false);
100
+ expect(body.temporaryPassword).toBe("hook-temp-password");
101
+ expect(body.user.uid).toBe("user-123");
102
+
103
+ expect(onAdminResetPasswordMock).toHaveBeenCalledWith(
104
+ "user-123",
105
+ expect.objectContaining({
106
+ authRepo: mockAuthRepo,
107
+ emailService: mockEmailService
108
+ })
109
+ );
110
+ expect(mockAuthRepo.createPasswordResetToken).not.toHaveBeenCalled();
111
+ expect(mockEmailService.send).not.toHaveBeenCalled();
112
+ });
113
+ });