@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,715 @@
1
+ import { EntityCollection, Property, StringProperty, NumberProperty, ArrayProperty, MapProperty, Relation } from "@rebasepro/types";
2
+
3
+ /**
4
+ * OpenAPI 3.0.3 specification generator.
5
+ *
6
+ * Produces a spec that exactly mirrors the REST API consumed by the
7
+ * Rebase SDK client (`@rebasepro/client`).
8
+ *
9
+ * Routes are mounted at `{basePath}/data/{slug}` by `initializeRebaseBackend`.
10
+ */
11
+
12
+ export interface OpenApiGeneratorOptions {
13
+ /** Base path for the API (e.g. "/api"). Defaults to "/api". */
14
+ basePath?: string;
15
+ /** Whether auth is enabled on data routes. Defaults to true. */
16
+ requireAuth?: boolean;
17
+ }
18
+
19
+ export function generateOpenApiSpec(
20
+ collections: EntityCollection[],
21
+ options: OpenApiGeneratorOptions = {}
22
+ ): Record<string, unknown> {
23
+ const basePath = options.basePath ?? "/api";
24
+ const requireAuth = options.requireAuth ?? true;
25
+
26
+ const spec: Record<string, unknown> = {
27
+ openapi: "3.0.3",
28
+ info: {
29
+ title: "Rebase API",
30
+ version: "1.0.0",
31
+ description:
32
+ "Auto-generated REST API from Rebase collection definitions. " +
33
+ "This is the same API consumed by the `@rebasepro/client` SDK."
34
+ },
35
+ servers: [
36
+ {
37
+ url: basePath,
38
+ description: "API Server"
39
+ }
40
+ ],
41
+ paths: {} as Record<string, unknown>,
42
+ components: {
43
+ schemas: {
44
+ ErrorResponse: {
45
+ type: "object",
46
+ properties: {
47
+ error: {
48
+ type: "object",
49
+ required: ["message", "code"],
50
+ properties: {
51
+ message: { type: "string" },
52
+ code: { type: "string" },
53
+ details: {}
54
+ }
55
+ }
56
+ }
57
+ },
58
+ PaginationMeta: {
59
+ type: "object",
60
+ properties: {
61
+ total: { type: "integer",
62
+ description: "Total number of matching records" },
63
+ limit: { type: "integer",
64
+ description: "Page size used for this query" },
65
+ offset: { type: "integer",
66
+ description: "Number of records skipped" },
67
+ hasMore: { type: "boolean",
68
+ description: "Whether more records exist beyond this page" }
69
+ }
70
+ }
71
+ } as Record<string, unknown>,
72
+ securitySchemes: {} as Record<string, unknown>
73
+ },
74
+ tags: [] as Array<{ name: string; description?: string }>
75
+ };
76
+
77
+ // ── Security Schemes ─────────────────────────────────────────────────
78
+ if (requireAuth) {
79
+ (spec.components as Record<string, unknown>).securitySchemes = {
80
+ bearerAuth: {
81
+ type: "http",
82
+ scheme: "bearer",
83
+ bearerFormat: "JWT",
84
+ description:
85
+ "JWT access token obtained from `POST /auth/login` or `POST /auth/register`. " +
86
+ "Can also be a static service key for server-to-server authentication."
87
+ },
88
+ queryToken: {
89
+ type: "apiKey",
90
+ in: "query",
91
+ name: "token",
92
+ description: "Alternative: pass the JWT or service key as a `token` query parameter."
93
+ }
94
+ };
95
+ (spec as Record<string, unknown>).security = [
96
+ { bearerAuth: [] },
97
+ { queryToken: [] }
98
+ ];
99
+ }
100
+
101
+ const paths = spec.paths as Record<string, unknown>;
102
+ const schemas = (spec.components as Record<string, unknown>).schemas as Record<string, unknown>;
103
+ const tags = spec.tags as Array<{ name: string; description?: string }>;
104
+
105
+ // ── Collection routes ────────────────────────────────────────────────
106
+ for (const collection of (collections || [])) {
107
+ const schemaName = toPascalCase(collection.singularName || collection.name);
108
+ const slug = collection.slug;
109
+
110
+ tags.push({
111
+ name: collection.name,
112
+ description: collection.description || `CRUD operations for ${collection.name}`
113
+ });
114
+
115
+ // Build component schema for this collection
116
+ schemas[schemaName] = buildCollectionSchema(collection);
117
+
118
+ // Build an "input" schema (no read-only/auto fields like autoValue dates)
119
+ schemas[`${schemaName}Input`] = buildCollectionInputSchema(collection);
120
+
121
+ const dataPath = `/data/${slug}`;
122
+
123
+ // ── GET /data/{slug} — List entities ──────────────────────────
124
+ paths[dataPath] = {
125
+ get: {
126
+ tags: [collection.name],
127
+ summary: `List ${collection.name}`,
128
+ operationId: `list${schemaName}`,
129
+ parameters: [
130
+ { name: "limit",
131
+ in: "query",
132
+ schema: { type: "integer",
133
+ default: 20,
134
+ maximum: 100 },
135
+ description: "Maximum number of records to return" },
136
+ { name: "offset",
137
+ in: "query",
138
+ schema: { type: "integer",
139
+ default: 0 },
140
+ description: "Number of records to skip" },
141
+ { name: "page",
142
+ in: "query",
143
+ schema: { type: "integer",
144
+ minimum: 1 },
145
+ description: "Page number (alternative to offset). Calculates offset as (page-1)*limit" },
146
+ {
147
+ name: "orderBy",
148
+ in: "query",
149
+ schema: { type: "string" },
150
+ description: "Sort field and direction. Accepts `field:asc` or `field:desc`, or a JSON array `[{\"field\":\"name\",\"direction\":\"asc\"}]`",
151
+ example: "created_at:desc"
152
+ },
153
+ {
154
+ name: "where",
155
+ in: "query",
156
+ schema: { type: "string" },
157
+ description: "JSON object filter. Example: `{\"status\":[\"==\",\"active\"]}`"
158
+ },
159
+ {
160
+ name: "include",
161
+ in: "query",
162
+ schema: { type: "string" },
163
+ description: "Comma-separated list of relations to include (eager-load). Use `*` for all relations.",
164
+ example: "author,tags"
165
+ },
166
+ {
167
+ name: "fields",
168
+ in: "query",
169
+ schema: { type: "string" },
170
+ description: "Comma-separated list of fields to return (field selection)",
171
+ example: "id,name,created_at"
172
+ },
173
+ {
174
+ name: "searchString",
175
+ in: "query",
176
+ schema: { type: "string" },
177
+ description: "Full-text search query"
178
+ },
179
+ ...buildFilterParameters(collection)
180
+ ],
181
+ responses: {
182
+ 200: {
183
+ description: "Paginated list of entities",
184
+ content: {
185
+ "application/json": {
186
+ schema: {
187
+ type: "object",
188
+ properties: {
189
+ data: {
190
+ type: "array",
191
+ items: { $ref: `#/components/schemas/${schemaName}` }
192
+ },
193
+ meta: { $ref: "#/components/schemas/PaginationMeta" }
194
+ }
195
+ }
196
+ }
197
+ }
198
+ },
199
+ ...errorResponses(requireAuth)
200
+ }
201
+ },
202
+ post: {
203
+ tags: [collection.name],
204
+ summary: `Create ${collection.singularName || collection.name}`,
205
+ operationId: `create${schemaName}`,
206
+ requestBody: {
207
+ required: true,
208
+ content: {
209
+ "application/json": {
210
+ schema: { $ref: `#/components/schemas/${schemaName}Input` }
211
+ }
212
+ }
213
+ },
214
+ responses: {
215
+ 201: {
216
+ description: "Created entity",
217
+ content: {
218
+ "application/json": {
219
+ schema: { $ref: `#/components/schemas/${schemaName}` }
220
+ }
221
+ }
222
+ },
223
+ ...errorResponses(requireAuth)
224
+ }
225
+ }
226
+ };
227
+
228
+ // ── GET/PUT/DELETE /data/{slug}/{id} ──────────────────────────
229
+ const entityPath = `/data/${slug}/{id}`;
230
+ paths[entityPath] = {
231
+ get: {
232
+ tags: [collection.name],
233
+ summary: `Get ${collection.singularName || collection.name} by ID`,
234
+ operationId: `get${schemaName}ById`,
235
+ parameters: [
236
+ { name: "id",
237
+ in: "path",
238
+ required: true,
239
+ schema: { type: "string" },
240
+ description: "Entity ID" },
241
+ {
242
+ name: "include",
243
+ in: "query",
244
+ schema: { type: "string" },
245
+ description: "Comma-separated list of relations to include",
246
+ example: "author,tags"
247
+ }
248
+ ],
249
+ responses: {
250
+ 200: {
251
+ description: "Entity found",
252
+ content: {
253
+ "application/json": {
254
+ schema: { $ref: `#/components/schemas/${schemaName}` }
255
+ }
256
+ }
257
+ },
258
+ 404: { description: "Entity not found",
259
+ content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
260
+ ...errorResponses(requireAuth)
261
+ }
262
+ },
263
+ put: {
264
+ tags: [collection.name],
265
+ summary: `Update ${collection.singularName || collection.name}`,
266
+ operationId: `update${schemaName}`,
267
+ parameters: [
268
+ { name: "id",
269
+ in: "path",
270
+ required: true,
271
+ schema: { type: "string" },
272
+ description: "Entity ID" }
273
+ ],
274
+ requestBody: {
275
+ required: true,
276
+ content: {
277
+ "application/json": {
278
+ schema: { $ref: `#/components/schemas/${schemaName}Input` }
279
+ }
280
+ }
281
+ },
282
+ responses: {
283
+ 200: {
284
+ description: "Updated entity",
285
+ content: {
286
+ "application/json": {
287
+ schema: { $ref: `#/components/schemas/${schemaName}` }
288
+ }
289
+ }
290
+ },
291
+ 404: { description: "Entity not found",
292
+ content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
293
+ ...errorResponses(requireAuth)
294
+ }
295
+ },
296
+ delete: {
297
+ tags: [collection.name],
298
+ summary: `Delete ${collection.singularName || collection.name}`,
299
+ operationId: `delete${schemaName}`,
300
+ parameters: [
301
+ { name: "id",
302
+ in: "path",
303
+ required: true,
304
+ schema: { type: "string" },
305
+ description: "Entity ID" }
306
+ ],
307
+ responses: {
308
+ 204: { description: "Deleted successfully" },
309
+ 404: { description: "Entity not found",
310
+ content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
311
+ ...errorResponses(requireAuth)
312
+ }
313
+ }
314
+ };
315
+
316
+ // ── Subcollection routes ──────────────────────────────────────
317
+ const relations = (collection as EntityCollection & { relations?: Relation[] }).relations;
318
+ if (relations && relations.length > 0) {
319
+ for (const relation of relations) {
320
+ const relationName = relation.relationName;
321
+ if (!relationName) continue;
322
+
323
+ let targetName: string;
324
+ try {
325
+ const targetCollection = relation.target();
326
+ targetName = targetCollection.singularName || targetCollection.name;
327
+ } catch {
328
+ targetName = relationName;
329
+ }
330
+ const targetSchema = toPascalCase(targetName);
331
+
332
+ const subPath = `/data/${slug}/{parentId}/${relationName}`;
333
+
334
+ // Only add if the schema exists (target collection is also registered)
335
+ paths[subPath] = {
336
+ get: {
337
+ tags: [collection.name],
338
+ summary: `List ${relationName} for a ${collection.singularName || collection.name}`,
339
+ operationId: `list${schemaName}${toPascalCase(relationName)}`,
340
+ parameters: [
341
+ { name: "parentId",
342
+ in: "path",
343
+ required: true,
344
+ schema: { type: "string" },
345
+ description: `${collection.singularName || collection.name} ID` },
346
+ { name: "limit",
347
+ in: "query",
348
+ schema: { type: "integer",
349
+ default: 20 } },
350
+ { name: "offset",
351
+ in: "query",
352
+ schema: { type: "integer",
353
+ default: 0 } },
354
+ { name: "orderBy",
355
+ in: "query",
356
+ schema: { type: "string" } },
357
+ { name: "searchString",
358
+ in: "query",
359
+ schema: { type: "string" } }
360
+ ],
361
+ responses: {
362
+ 200: {
363
+ description: `List of related ${relationName}`,
364
+ content: {
365
+ "application/json": {
366
+ schema: {
367
+ type: "object",
368
+ properties: {
369
+ data: {
370
+ type: "array",
371
+ items: schemas[targetSchema]
372
+ ? { $ref: `#/components/schemas/${targetSchema}` }
373
+ : { type: "object" }
374
+ },
375
+ meta: { $ref: "#/components/schemas/PaginationMeta" }
376
+ }
377
+ }
378
+ }
379
+ }
380
+ },
381
+ ...errorResponses(requireAuth)
382
+ }
383
+ }
384
+ };
385
+ }
386
+ }
387
+ }
388
+
389
+ return spec;
390
+ }
391
+
392
+ // ── Helpers ──────────────────────────────────────────────────────────────
393
+
394
+ /**
395
+ * Build the component schema for a collection (output / read shape).
396
+ * All fields are included (including relation foreign keys).
397
+ */
398
+ function buildCollectionSchema(collection: EntityCollection): Record<string, unknown> {
399
+ const properties: Record<string, unknown> = {
400
+ id: { type: "string",
401
+ description: "Unique identifier" }
402
+ };
403
+ const required: string[] = ["id"];
404
+
405
+ for (const [key, property] of Object.entries(collection.properties)) {
406
+ // Skip relation properties — they are virtual and not part of the REST payload
407
+ if (property.type === "relation") continue;
408
+
409
+ properties[key] = convertPropertyToSchema(property);
410
+
411
+ if (property.validation?.required) {
412
+ required.push(key);
413
+ }
414
+ }
415
+
416
+ return {
417
+ type: "object",
418
+ required: required.length > 0 ? required : undefined,
419
+ properties
420
+ };
421
+ }
422
+
423
+ /**
424
+ * Build an input schema (for POST/PUT) — excludes auto-generated fields.
425
+ */
426
+ function buildCollectionInputSchema(collection: EntityCollection): Record<string, unknown> {
427
+ const properties: Record<string, unknown> = {};
428
+ const required: string[] = [];
429
+
430
+ for (const [key, property] of Object.entries(collection.properties)) {
431
+ if (property.type === "relation") continue;
432
+
433
+ // Skip auto-value date fields from the input schema
434
+ if (property.type === "date" && property.autoValue) continue;
435
+
436
+ // Skip auto-generated ID fields
437
+ if ("isId" in property && property.isId && property.isId !== "manual" && property.isId !== true) continue;
438
+
439
+ properties[key] = convertPropertyToSchema(property);
440
+
441
+ if (property.validation?.required) {
442
+ required.push(key);
443
+ }
444
+ }
445
+
446
+ // Allow explicit ID for create (optional)
447
+ properties["id"] = {
448
+ type: "string",
449
+ description: "Optional: client-assigned ID. If omitted, the server generates one."
450
+ };
451
+
452
+ return {
453
+ type: "object",
454
+ required: required.length > 0 ? required : undefined,
455
+ properties
456
+ };
457
+ }
458
+
459
+ /**
460
+ * Convert a Rebase Property to an OpenAPI 3.0 schema object.
461
+ */
462
+ function convertPropertyToSchema(property: Property): Record<string, unknown> {
463
+ const base: Record<string, unknown> = {};
464
+
465
+ if (property.name) {
466
+ base.description = property.name;
467
+ }
468
+
469
+ switch (property.type) {
470
+ case "string": {
471
+ const sp = property as StringProperty;
472
+ base.type = "string";
473
+
474
+ if (sp.enum) {
475
+ const enumValues = resolveEnumValues(sp.enum);
476
+ if (enumValues.length > 0) {
477
+ base.enum = enumValues;
478
+ }
479
+ }
480
+
481
+ if (sp.validation) {
482
+ if (sp.validation.min !== undefined) base.minLength = sp.validation.min;
483
+ if (sp.validation.max !== undefined) base.maxLength = sp.validation.max;
484
+ if (sp.validation.length !== undefined) {
485
+ base.minLength = sp.validation.length;
486
+ base.maxLength = sp.validation.length;
487
+ }
488
+ if (sp.validation.matches !== undefined) {
489
+ base.pattern = String(sp.validation.matches);
490
+ }
491
+ }
492
+
493
+ if (sp.email) base.format = "email";
494
+ if (sp.url) base.format = "uri";
495
+ if (sp.storage) base.format = "uri";
496
+ if (sp.markdown) base.description = (base.description || "") + " (Markdown)";
497
+
498
+ return base;
499
+ }
500
+
501
+ case "number": {
502
+ const np = property as NumberProperty;
503
+ const isInteger = np.validation?.integer || np.columnType === "integer" || np.columnType === "serial" || np.columnType === "bigserial" || np.columnType === "bigint";
504
+ base.type = isInteger ? "integer" : "number";
505
+
506
+ if (np.enum) {
507
+ const enumValues = resolveEnumValues(np.enum);
508
+ if (enumValues.length > 0) {
509
+ base.enum = enumValues;
510
+ }
511
+ }
512
+
513
+ if (np.validation) {
514
+ if (np.validation.min !== undefined) base.minimum = np.validation.min;
515
+ if (np.validation.max !== undefined) base.maximum = np.validation.max;
516
+ if (np.validation.moreThan !== undefined) {
517
+ base.minimum = np.validation.moreThan;
518
+ base.exclusiveMinimum = true;
519
+ }
520
+ if (np.validation.lessThan !== undefined) {
521
+ base.maximum = np.validation.lessThan;
522
+ base.exclusiveMaximum = true;
523
+ }
524
+ }
525
+
526
+ return base;
527
+ }
528
+
529
+ case "boolean":
530
+ base.type = "boolean";
531
+ return base;
532
+
533
+ case "date": {
534
+ base.type = "string";
535
+ if (property.mode === "date") {
536
+ base.format = "date";
537
+ } else {
538
+ base.format = "date-time";
539
+ }
540
+ if (property.autoValue) {
541
+ base.readOnly = true;
542
+ base.description = (base.description || "") +
543
+ (property.autoValue === "on_create" ? " (Auto-set on creation)" : " (Auto-updated)");
544
+ }
545
+ return base;
546
+ }
547
+
548
+ case "geopoint":
549
+ base.type = "object";
550
+ base.properties = {
551
+ latitude: { type: "number" },
552
+ longitude: { type: "number" }
553
+ };
554
+ base.required = ["latitude", "longitude"];
555
+ return base;
556
+
557
+ case "reference":
558
+ base.type = "string";
559
+ base.description = (base.description || "") + " (Reference ID)";
560
+ return base;
561
+
562
+ case "array": {
563
+ const ap = property as ArrayProperty;
564
+ base.type = "array";
565
+
566
+ if (ap.oneOf) {
567
+ // Discriminated union (e.g., content blocks)
568
+ const typeField = ap.oneOf.typeField || "type";
569
+ const valueField = ap.oneOf.valueField || "value";
570
+ const variants: Record<string, unknown>[] = [];
571
+
572
+ for (const [variantKey, variantProp] of Object.entries(ap.oneOf.properties)) {
573
+ variants.push({
574
+ type: "object",
575
+ properties: {
576
+ [typeField]: { type: "string",
577
+ enum: [variantKey] },
578
+ [valueField]: convertPropertyToSchema(variantProp)
579
+ },
580
+ required: [typeField, valueField]
581
+ });
582
+ }
583
+
584
+ base.items = { oneOf: variants };
585
+ } else if (ap.of) {
586
+ if (Array.isArray(ap.of)) {
587
+ base.items = { oneOf: ap.of.map(p => convertPropertyToSchema(p)) };
588
+ } else {
589
+ base.items = convertPropertyToSchema(ap.of);
590
+ }
591
+ } else {
592
+ base.items = {};
593
+ }
594
+
595
+ if (ap.validation) {
596
+ if (ap.validation.min !== undefined) base.minItems = ap.validation.min;
597
+ if (ap.validation.max !== undefined) base.maxItems = ap.validation.max;
598
+ }
599
+
600
+ return base;
601
+ }
602
+
603
+ case "map": {
604
+ const mp = property as MapProperty;
605
+ base.type = "object";
606
+
607
+ if (mp.properties) {
608
+ const props: Record<string, unknown> = {};
609
+ const req: string[] = [];
610
+
611
+ for (const [key, subProp] of Object.entries(mp.properties)) {
612
+ props[key] = convertPropertyToSchema(subProp);
613
+ if (subProp.validation?.required) {
614
+ req.push(key);
615
+ }
616
+ }
617
+
618
+ base.properties = props;
619
+ if (req.length > 0) base.required = req;
620
+ } else if (mp.keyValue) {
621
+ base.additionalProperties = true;
622
+ }
623
+
624
+ return base;
625
+ }
626
+
627
+ default:
628
+ base.type = "string";
629
+ return base;
630
+ }
631
+ }
632
+
633
+ /**
634
+ * Resolve EnumValues (array or record) into a flat array of enum values.
635
+ */
636
+ function resolveEnumValues(enumDef: Record<string | number, unknown> | Array<{ id: string | number }>): Array<string | number> {
637
+ if (Array.isArray(enumDef)) {
638
+ return enumDef.map(e => (typeof e === "object" && e !== null && "id" in e) ? e.id : e as string | number);
639
+ }
640
+ return Object.keys(enumDef).map(k => {
641
+ // Preserve numeric keys as numbers
642
+ const num = Number(k);
643
+ return isNaN(num) ? k : num;
644
+ });
645
+ }
646
+
647
+ /**
648
+ * Build PostgREST-style filter parameters for a collection.
649
+ * These are additional query parameters like `?status=eq.active&price=gte.100`.
650
+ */
651
+ function buildFilterParameters(collection: EntityCollection): Array<Record<string, unknown>> {
652
+ const params: Array<Record<string, unknown>> = [];
653
+
654
+ for (const [key, property] of Object.entries(collection.properties)) {
655
+ if (property.type === "relation" || property.type === "map" || property.type === "array" || property.type === "geopoint") {
656
+ continue;
657
+ }
658
+
659
+ params.push({
660
+ name: key,
661
+ in: "query",
662
+ required: false,
663
+ schema: { type: "string" },
664
+ description:
665
+ `Filter by \`${key}\`. Supports PostgREST operators: ` +
666
+ "`eq.value`, `neq.value`, `gt.value`, `gte.value`, `lt.value`, `lte.value`, " +
667
+ "`in.(a,b,c)`, `nin.(a,b,c)`, `cs.value` (array-contains), `csa.(a,b)` (array-contains-any). " +
668
+ "Plain values imply equality.",
669
+ example: property.type === "string" ? "eq.active" : property.type === "number" ? "gte.100" : undefined
670
+ });
671
+ }
672
+
673
+ return params;
674
+ }
675
+
676
+ /**
677
+ * Standard error responses included on every endpoint.
678
+ */
679
+ function errorResponses(requireAuth: boolean): Record<string, unknown> {
680
+ const responses: Record<string, unknown> = {
681
+ 400: {
682
+ description: "Bad request",
683
+ content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } }
684
+ },
685
+ 500: {
686
+ description: "Internal server error",
687
+ content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } }
688
+ }
689
+ };
690
+
691
+ if (requireAuth) {
692
+ responses[401] = {
693
+ description: "Authentication required or invalid token",
694
+ content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } }
695
+ };
696
+ responses[403] = {
697
+ description: "Insufficient permissions",
698
+ content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } }
699
+ };
700
+ }
701
+
702
+ return responses;
703
+ }
704
+
705
+ /**
706
+ * Convert a string to PascalCase for schema names.
707
+ */
708
+ function toPascalCase(str: string): string {
709
+ return str
710
+ .replace(/[^a-zA-Z0-9]+/g, " ")
711
+ .split(" ")
712
+ .filter(Boolean)
713
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
714
+ .join("");
715
+ }