@graphoria/server 0.4.0 → 0.5.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 (150) hide show
  1. package/README.md +4 -0
  2. package/dist/src/analyzeQuery/depthLimit.d.ts +1 -13
  3. package/dist/src/analyzeQuery/depthLimit.d.ts.map +1 -1
  4. package/dist/src/authentication/capabilities.d.ts +1 -1
  5. package/dist/src/authentication/capabilities.d.ts.map +1 -1
  6. package/dist/src/authentication/tokenRepository.d.ts.map +1 -1
  7. package/dist/src/config/types/db.d.ts +25 -0
  8. package/dist/src/config/types/db.d.ts.map +1 -1
  9. package/dist/src/configuration/getSchemas/index.d.ts +40 -0
  10. package/dist/src/configuration/getSchemas/index.d.ts.map +1 -1
  11. package/dist/src/configuration/getSchemas/mergeEntities/index.d.ts +20 -0
  12. package/dist/src/configuration/getSchemas/mergeEntities/index.d.ts.map +1 -1
  13. package/dist/src/configuration/gql/handleGraphQLRequestFactory.d.ts.map +1 -1
  14. package/dist/src/configuration/index.d.ts +30 -0
  15. package/dist/src/configuration/index.d.ts.map +1 -1
  16. package/dist/src/console/api.d.ts.map +1 -1
  17. package/dist/src/cron/index.d.ts.map +1 -1
  18. package/dist/src/databases/common.d.ts +20 -6
  19. package/dist/src/databases/common.d.ts.map +1 -1
  20. package/dist/src/databases/core/executor.d.ts +4 -6
  21. package/dist/src/databases/core/executor.d.ts.map +1 -1
  22. package/dist/src/databases/core/function-mapping.d.ts +1 -2
  23. package/dist/src/databases/core/function-mapping.d.ts.map +1 -1
  24. package/dist/src/databases/core/query-builder.d.ts +5 -0
  25. package/dist/src/databases/core/query-builder.d.ts.map +1 -1
  26. package/dist/src/databases/engines/mssql/getStructure.d.ts +5 -0
  27. package/dist/src/databases/engines/mssql/getStructure.d.ts.map +1 -1
  28. package/dist/src/databases/engines/mssql/query/index.d.ts +2 -2
  29. package/dist/src/databases/engines/mssql/query/index.d.ts.map +1 -1
  30. package/dist/src/databases/engines/mysql/getStructure.d.ts +5 -0
  31. package/dist/src/databases/engines/mysql/getStructure.d.ts.map +1 -1
  32. package/dist/src/databases/engines/mysql/query/index.d.ts +2 -2
  33. package/dist/src/databases/engines/mysql/query/index.d.ts.map +1 -1
  34. package/dist/src/databases/engines/postgresql/getStructure.d.ts +5 -0
  35. package/dist/src/databases/engines/postgresql/getStructure.d.ts.map +1 -1
  36. package/dist/src/databases/engines/postgresql/query/index.d.ts +2 -2
  37. package/dist/src/databases/engines/postgresql/query/index.d.ts.map +1 -1
  38. package/dist/src/databases/high-level-operations.d.ts +5 -0
  39. package/dist/src/databases/high-level-operations.d.ts.map +1 -1
  40. package/dist/src/databases/metadata/structure.d.ts +5 -0
  41. package/dist/src/databases/metadata/structure.d.ts.map +1 -1
  42. package/dist/src/databases/transformers/data-transformers.d.ts +30 -0
  43. package/dist/src/databases/transformers/data-transformers.d.ts.map +1 -1
  44. package/dist/src/index.d.ts +4 -0
  45. package/dist/src/index.d.ts.map +1 -1
  46. package/dist/src/logging/slowQuery.d.ts +29 -0
  47. package/dist/src/logging/slowQuery.d.ts.map +1 -0
  48. package/dist/src/logging/slowQuery.test.d.ts +2 -0
  49. package/dist/src/logging/slowQuery.test.d.ts.map +1 -0
  50. package/dist/src/observability/health.d.ts +23 -0
  51. package/dist/src/observability/health.d.ts.map +1 -0
  52. package/dist/src/observability/health.test.d.ts +2 -0
  53. package/dist/src/observability/health.test.d.ts.map +1 -0
  54. package/dist/src/observability/httpMetrics.d.ts +7 -0
  55. package/dist/src/observability/httpMetrics.d.ts.map +1 -0
  56. package/dist/src/observability/httpMetrics.test.d.ts +2 -0
  57. package/dist/src/observability/httpMetrics.test.d.ts.map +1 -0
  58. package/dist/src/observability/httpTracing.d.ts +11 -0
  59. package/dist/src/observability/httpTracing.d.ts.map +1 -0
  60. package/dist/src/observability/httpTracing.test.d.ts +2 -0
  61. package/dist/src/observability/httpTracing.test.d.ts.map +1 -0
  62. package/dist/src/observability/metrics.d.ts +81 -0
  63. package/dist/src/observability/metrics.d.ts.map +1 -0
  64. package/dist/src/observability/metrics.perf.test.d.ts +2 -0
  65. package/dist/src/observability/metrics.perf.test.d.ts.map +1 -0
  66. package/dist/src/observability/metrics.test.d.ts +2 -0
  67. package/dist/src/observability/metrics.test.d.ts.map +1 -0
  68. package/dist/src/observability/metricsRoute.d.ts +19 -0
  69. package/dist/src/observability/metricsRoute.d.ts.map +1 -0
  70. package/dist/src/observability/metricsRoute.test.d.ts +2 -0
  71. package/dist/src/observability/metricsRoute.test.d.ts.map +1 -0
  72. package/dist/src/observability/tracing.d.ts +92 -0
  73. package/dist/src/observability/tracing.d.ts.map +1 -0
  74. package/dist/src/observability/tracing.perf.test.d.ts +2 -0
  75. package/dist/src/observability/tracing.perf.test.d.ts.map +1 -0
  76. package/dist/src/observability/tracing.test.d.ts +2 -0
  77. package/dist/src/observability/tracing.test.d.ts.map +1 -0
  78. package/dist/src/queues/kafka.d.ts +6 -0
  79. package/dist/src/queues/kafka.d.ts.map +1 -1
  80. package/dist/src/queues/kafka.test.d.ts +2 -0
  81. package/dist/src/queues/kafka.test.d.ts.map +1 -0
  82. package/dist/src/queues/rabbitmq.d.ts.map +1 -1
  83. package/dist/src/remoteREST/proxy.d.ts.map +1 -1
  84. package/dist/src/remoteSchemas/proxy.d.ts.map +1 -1
  85. package/dist/src/singletons/cache/redisClient.d.ts +2 -2
  86. package/dist/src/singletons/cache/redisClient.d.ts.map +1 -1
  87. package/dist/src/singletons/databases.d.ts +1 -0
  88. package/dist/src/singletons/databases.d.ts.map +1 -1
  89. package/dist/src/singletons/env.d.ts +29 -0
  90. package/dist/src/singletons/env.d.ts.map +1 -1
  91. package/dist/src/singletons/queues.d.ts +1 -0
  92. package/dist/src/singletons/queues.d.ts.map +1 -1
  93. package/dist/src/subscriptions/strategies/database.d.ts.map +1 -1
  94. package/dist/src/subscriptions/utils/polling.d.ts +1 -0
  95. package/dist/src/subscriptions/utils/polling.d.ts.map +1 -1
  96. package/dist/src/subscriptions/utils/polling.test.d.ts +2 -0
  97. package/dist/src/subscriptions/utils/polling.test.d.ts.map +1 -0
  98. package/dist/src/types/env.d.ts +89 -0
  99. package/dist/src/types/env.d.ts.map +1 -1
  100. package/dist/src/types/zod/configuration.d.ts +25 -0
  101. package/dist/src/types/zod/configuration.d.ts.map +1 -1
  102. package/dist/src/types/zod/db.d.ts +85 -0
  103. package/dist/src/types/zod/db.d.ts.map +1 -1
  104. package/dist/src/utils/rateLimit.d.ts.map +1 -1
  105. package/dist/src/utils/redis.d.ts +17 -0
  106. package/dist/src/utils/redis.d.ts.map +1 -0
  107. package/dist/src/utils/redis.test.d.ts +2 -0
  108. package/dist/src/utils/redis.test.d.ts.map +1 -0
  109. package/dist/src/utils/responses.d.ts +3 -0
  110. package/dist/src/utils/responses.d.ts.map +1 -1
  111. package/dist/tsconfig.tsbuildinfo +1 -1
  112. package/package.json +4 -1
  113. package/playgrounds/console/index.html +8 -8
  114. package/playgrounds/graphiql/index.html +114 -114
  115. package/playgrounds/scalar/index.html +1 -1
  116. package/src/analyzeQuery/depthLimit.ts +15 -5
  117. package/src/authentication/capabilities.ts +3 -1
  118. package/src/authentication/tokenRepository.ts +2 -3
  119. package/src/config/types/db.ts +16 -2
  120. package/src/configuration/gql/handleGraphQLRequestFactory.ts +233 -152
  121. package/src/console/api.ts +1 -6
  122. package/src/cron/index.ts +59 -36
  123. package/src/databases/common.ts +109 -10
  124. package/src/databases/core/executor.ts +96 -18
  125. package/src/databases/core/function-mapping.ts +1 -1
  126. package/src/databases/engines/mssql/query/index.ts +4 -1
  127. package/src/databases/engines/mysql/query/index.ts +5 -2
  128. package/src/databases/engines/postgresql/query/index.ts +5 -2
  129. package/src/databases/high-level-operations.ts +44 -2
  130. package/src/index.ts +147 -62
  131. package/src/logging/slowQuery.ts +59 -0
  132. package/src/observability/health.ts +67 -0
  133. package/src/observability/httpMetrics.ts +37 -0
  134. package/src/observability/httpTracing.ts +48 -0
  135. package/src/observability/metrics.ts +243 -0
  136. package/src/observability/metricsRoute.ts +33 -0
  137. package/src/observability/tracing.ts +421 -0
  138. package/src/queues/kafka.ts +45 -2
  139. package/src/queues/rabbitmq.ts +37 -1
  140. package/src/remoteREST/proxy.ts +17 -0
  141. package/src/remoteSchemas/proxy.ts +17 -0
  142. package/src/singletons/cache/redisClient.ts +4 -5
  143. package/src/singletons/databases.ts +5 -0
  144. package/src/singletons/queues.ts +9 -3
  145. package/src/subscriptions/strategies/database.ts +2 -0
  146. package/src/subscriptions/utils/polling.ts +80 -21
  147. package/src/types/env.ts +27 -0
  148. package/src/utils/rateLimit.ts +6 -1
  149. package/src/utils/redis.ts +71 -0
  150. package/src/utils/responses.ts +6 -0
@@ -2,7 +2,7 @@
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="UTF-8" />
5
- <link rel="icon" type="image/svg+xml" href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0OCIgaGVpZ2h0PSI0NiIgZmlsbD0ibm9uZSIgdmlld0JveD0iMCAwIDQ4IDQ2Ij48cGF0aCBmaWxsPSIjODYzYmZmIiBkPSJNMjUuOTQ2IDQ0LjkzOGMtLjY2NC44NDUtMi4wMjEuMzc1LTIuMDIxLS42OThWMzMuOTM3YTIuMjYgMi4yNiAwIDAgMC0yLjI2Mi0yLjI2MkgxMC4yODdjLS45MiAwLTEuNDU2LTEuMDQtLjkyLTEuNzg4bDcuNDgtMTAuNDcxYzEuMDctMS40OTcgMC0zLjU3OC0xLjg0Mi0zLjU3OEgxLjIzN2MtLjkyIDAtMS40NTYtMS4wNC0uOTItMS43ODhMMTAuMDEzLjQ3NGMuMjE0LS4yOTcuNTU2LS40NzQuOTItLjQ3NGgyOC44OTRjLjkyIDAgMS40NTYgMS4wNC45MiAxLjc4OGwtNy40OCAxMC40NzFjLTEuMDcgMS40OTggMCAzLjU3OSAxLjg0MiAzLjU3OWgxMS4zNzdjLjk0MyAwIDEuNDczIDEuMDg4Ljg5IDEuODNMMjUuOTQ3IDQ0Ljk0eiIgc3R5bGU9ImZpbGw6Izg2M2JmZjtmaWxsOmNvbG9yKGRpc3BsYXktcDMgLjUyNTIgLjIzIDEpO2ZpbGwtb3BhY2l0eToxIi8+PG1hc2sgaWQ9ImEiIHdpZHRoPSI0OCIgaGVpZ2h0PSI0NiIgeD0iMCIgeT0iMCIgbWFza1VuaXRzPSJ1c2VyU3BhY2VPblVzZSIgc3R5bGU9Im1hc2stdHlwZTphbHBoYSI+PHBhdGggZmlsbD0iIzAwMCIgZD0iTTI1Ljg0MiA0NC45MzhjLS42NjQuODQ0LTIuMDIxLjM3NS0yLjAyMS0uNjk4VjMzLjkzN2EyLjI2IDIuMjYgMCAwIDAtMi4yNjItMi4yNjJIMTAuMTgzYy0uOTIgMC0xLjQ1Ni0xLjA0LS45Mi0xLjc4OGw3LjQ4LTEwLjQ3MWMxLjA3LTEuNDk4IDAtMy41NzktMS44NDItMy41NzlIMS4xMzNjLS45MiAwLTEuNDU2LTEuMDQtLjkyLTEuNzg3TDkuOTEuNDczYy4yMTQtLjI5Ny41NTYtLjQ3NC45Mi0uNDc0aDI4Ljg5NGMuOTIgMCAxLjQ1NiAxLjA0LjkyIDEuNzg4bC03LjQ4IDEwLjQ3MWMtMS4wNyAxLjQ5OCAwIDMuNTc4IDEuODQyIDMuNTc4aDExLjM3N2MuOTQzIDAgMS40NzMgMS4wODguODkgMS44MzJMMjUuODQzIDQ0Ljk0eiIgc3R5bGU9ImZpbGw6IzAwMDtmaWxsLW9wYWNpdHk6MSIvPjwvbWFzaz48ZyBtYXNrPSJ1cmwoI2EpIj48ZyBmaWx0ZXI9InVybCgjYikiPjxlbGxpcHNlIGN4PSI1LjUwOCIgY3k9IjE0LjcwNCIgZmlsbD0iI2VkZTZmZiIgcng9IjUuNTA4IiByeT0iMTQuNzA0IiBzdHlsZT0iZmlsbDojZWRlNmZmO2ZpbGw6Y29sb3IoZGlzcGxheS1wMyAuOTI3NSAuOTAzMyAxKTtmaWxsLW9wYWNpdHk6MSIgdHJhbnNmb3JtPSJtYXRyaXgoLjAwMzI0IDEgMSAtLjAwMzI0IC00LjQ3IDMxLjUxNikiLz48L2c+PGcgZmlsdGVyPSJ1cmwoI2MpIj48ZWxsaXBzZSBjeD0iMTAuMzk5IiBjeT0iMjkuODUxIiBmaWxsPSIjZWRlNmZmIiByeD0iMTAuMzk5IiByeT0iMjkuODUxIiBzdHlsZT0iZmlsbDojZWRlNmZmO2ZpbGw6Y29sb3IoZGlzcGxheS1wMyAuOTI3NSAuOTAzMyAxKTtmaWxsLW9wYWNpdHk6MSIgdHJhbnNmb3JtPSJtYXRyaXgoLjAwMzI0IDEgMSAtLjAwMzI0IC0zOS4zMjggNy44ODMpIi8+PC9nPjxnIGZpbHRlcj0idXJsKCNkKSI+PGVsbGlwc2UgY3g9IjUuNTA4IiBjeT0iMzAuNDg3IiBmaWxsPSIjN2UxNGZmIiByeD0iNS41MDgiIHJ5PSIzMC40ODciIHN0eWxlPSJmaWxsOiM3ZTE0ZmY7ZmlsbDpjb2xvcihkaXNwbGF5LXAzIC40OTIyIC4wNzY3IDEpO2ZpbGwtb3BhY2l0eToxIiB0cmFuc2Zvcm09InJvdGF0ZSg4OS44MTQgLTI1LjkxMyAtMTQuNjM5KXNjYWxlKDEgLTEpIi8+PC9nPjxnIGZpbHRlcj0idXJsKCNlKSI+PGVsbGlwc2UgY3g9IjUuNTA4IiBjeT0iMzAuNTk5IiBmaWxsPSIjN2UxNGZmIiByeD0iNS41MDgiIHJ5PSIzMC41OTkiIHN0eWxlPSJmaWxsOiM3ZTE0ZmY7ZmlsbDpjb2xvcihkaXNwbGF5LXAzIC40OTIyIC4wNzY3IDEpO2ZpbGwtb3BhY2l0eToxIiB0cmFuc2Zvcm09InJvdGF0ZSg4OS44MTQgLTMyLjY0NCAtMy4zMzQpc2NhbGUoMSAtMSkiLz48L2c+PGcgZmlsdGVyPSJ1cmwoI2YpIj48ZWxsaXBzZSBjeD0iNS41MDgiIGN5PSIzMC41OTkiIGZpbGw9IiM3ZTE0ZmYiIHJ4PSI1LjUwOCIgcnk9IjMwLjU5OSIgc3R5bGU9ImZpbGw6IzdlMTRmZjtmaWxsOmNvbG9yKGRpc3BsYXktcDMgLjQ5MjIgLjA3NjcgMSk7ZmlsbC1vcGFjaXR5OjEiIHRyYW5zZm9ybT0ibWF0cml4KC4wMDMyNCAxIDEgLS4wMDMyNCAtMzQuMzQgMzAuNDcpIi8+PC9nPjxnIGZpbHRlcj0idXJsKCNnKSI+PGVsbGlwc2UgY3g9IjE0LjA3MiIgY3k9IjIyLjA3OCIgZmlsbD0iI2VkZTZmZiIgcng9IjE0LjA3MiIgcnk9IjIyLjA3OCIgc3R5bGU9ImZpbGw6I2VkZTZmZjtmaWxsOmNvbG9yKGRpc3BsYXktcDMgLjkyNzUgLjkwMzMgMSk7ZmlsbC1vcGFjaXR5OjEiIHRyYW5zZm9ybT0icm90YXRlKDkzLjM1IDI0LjUwNiA0OC40OTMpc2NhbGUoLTEgMSkiLz48L2c+PGcgZmlsdGVyPSJ1cmwoI2gpIj48ZWxsaXBzZSBjeD0iMy40NyIgY3k9IjIxLjUwMSIgZmlsbD0iIzdlMTRmZiIgcng9IjMuNDciIHJ5PSIyMS41MDEiIHN0eWxlPSJmaWxsOiM3ZTE0ZmY7ZmlsbDpjb2xvcihkaXNwbGF5LXAzIC40OTIyIC4wNzY3IDEpO2ZpbGwtb3BhY2l0eToxIiB0cmFuc2Zvcm09InJvdGF0ZSg4OS4wMDkgMjguNzA4IDQ3LjU5KXNjYWxlKC0xIDEpIi8+PC9nPjxnIGZpbHRlcj0idXJsKCNpKSI+PGVsbGlwc2UgY3g9IjMuNDciIGN5PSIyMS41MDEiIGZpbGw9IiM3ZTE0ZmYiIHJ4PSIzLjQ3IiByeT0iMjEuNTAxIiBzdHlsZT0iZmlsbDojN2UxNGZmO2ZpbGw6Y29sb3IoZGlzcGxheS1wMyAuNDkyMiAuMDc2NyAxKTtmaWxsLW9wYWNpdHk6MSIgdHJhbnNmb3JtPSJyb3RhdGUoODkuMDA5IDI4LjcwOCA0Ny41OSlzY2FsZSgtMSAxKSIvPjwvZz48ZyBmaWx0ZXI9InVybCgjaikiPjxlbGxpcHNlIGN4PSIuMzg3IiBjeT0iOC45NzIiIGZpbGw9IiM3ZTE0ZmYiIHJ4PSI0LjQwNyIgcnk9IjI5LjEwOCIgc3R5bGU9ImZpbGw6IzdlMTRmZjtmaWxsOmNvbG9yKGRpc3BsYXktcDMgLjQ5MjIgLjA3NjcgMSk7ZmlsbC1vcGFjaXR5OjEiIHRyYW5zZm9ybT0icm90YXRlKDM5LjUxIC4zODcgOC45NzIpIi8+PC9nPjxnIGZpbHRlcj0idXJsKCNrKSI+PGVsbGlwc2UgY3g9IjQ3LjUyMyIgY3k9Ii02LjA5MiIgZmlsbD0iIzdlMTRmZiIgcng9IjQuNDA3IiByeT0iMjkuMTA4IiBzdHlsZT0iZmlsbDojN2UxNGZmO2ZpbGw6Y29sb3IoZGlzcGxheS1wMyAuNDkyMiAuMDc2NyAxKTtmaWxsLW9wYWNpdHk6MSIgdHJhbnNmb3JtPSJyb3RhdGUoMzcuODkyIDQ3LjUyMyAtNi4wOTIpIi8+PC9nPjxnIGZpbHRlcj0idXJsKCNsKSI+PGVsbGlwc2UgY3g9IjQxLjQxMiIgY3k9IjYuMzMzIiBmaWxsPSIjNDdiZmZmIiByeD0iNS45NzEiIHJ5PSI5LjY2NSIgc3R5bGU9ImZpbGw6IzQ3YmZmZjtmaWxsOmNvbG9yKGRpc3BsYXktcDMgLjI3OTkgLjc0OCAxKTtmaWxsLW9wYWNpdHk6MSIgdHJhbnNmb3JtPSJyb3RhdGUoMzcuODkyIDQxLjQxMiA2LjMzMykiLz48L2c+PGcgZmlsdGVyPSJ1cmwoI20pIj48ZWxsaXBzZSBjeD0iLTEuODc5IiBjeT0iMzguMzMyIiBmaWxsPSIjN2UxNGZmIiByeD0iNC40MDciIHJ5PSIyOS4xMDgiIHN0eWxlPSJmaWxsOiM3ZTE0ZmY7ZmlsbDpjb2xvcihkaXNwbGF5LXAzIC40OTIyIC4wNzY3IDEpO2ZpbGwtb3BhY2l0eToxIiB0cmFuc2Zvcm09InJvdGF0ZSgzNy44OTIgLTEuODggMzguMzMyKSIvPjwvZz48ZyBmaWx0ZXI9InVybCgjbikiPjxlbGxpcHNlIGN4PSItMS44NzkiIGN5PSIzOC4zMzIiIGZpbGw9IiM3ZTE0ZmYiIHJ4PSI0LjQwNyIgcnk9IjI5LjEwOCIgc3R5bGU9ImZpbGw6IzdlMTRmZjtmaWxsOmNvbG9yKGRpc3BsYXktcDMgLjQ5MjIgLjA3NjcgMSk7ZmlsbC1vcGFjaXR5OjEiIHRyYW5zZm9ybT0icm90YXRlKDM3Ljg5MiAtMS44OCAzOC4zMzIpIi8+PC9nPjxnIGZpbHRlcj0idXJsKCNvKSI+PGVsbGlwc2UgY3g9IjM1LjY1MSIgY3k9IjI5LjkwNyIgZmlsbD0iIzdlMTRmZiIgcng9IjQuNDA3IiByeT0iMjkuMTA4IiBzdHlsZT0iZmlsbDojN2UxNGZmO2ZpbGw6Y29sb3IoZGlzcGxheS1wMyAuNDkyMiAuMDc2NyAxKTtmaWxsLW9wYWNpdHk6MSIgdHJhbnNmb3JtPSJyb3RhdGUoMzcuODkyIDM1LjY1MSAyOS45MDcpIi8+PC9nPjxnIGZpbHRlcj0idXJsKCNwKSI+PGVsbGlwc2UgY3g9IjM4LjQxOCIgY3k9IjMyLjQiIGZpbGw9IiM0N2JmZmYiIHJ4PSI1Ljk3MSIgcnk9IjE1LjI5NyIgc3R5bGU9ImZpbGw6IzQ3YmZmZjtmaWxsOmNvbG9yKGRpc3BsYXktcDMgLjI3OTkgLjc0OCAxKTtmaWxsLW9wYWNpdHk6MSIgdHJhbnNmb3JtPSJyb3RhdGUoMzcuODkyIDM4LjQxOCAzMi40KSIvPjwvZz48L2c+PGRlZnM+PGZpbHRlciBpZD0iYiIgd2lkdGg9IjYwLjA0NSIgaGVpZ2h0PSI0MS42NTQiIHg9Ii0xOS43NyIgeT0iMTYuMTQ5IiBjb2xvci1pbnRlcnBvbGF0aW9uLWZpbHRlcnM9InNSR0IiIGZpbHRlclVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PGZlRmxvb2QgZmxvb2Qtb3BhY2l0eT0iMCIgcmVzdWx0PSJCYWNrZ3JvdW5kSW1hZ2VGaXgiLz48ZmVCbGVuZCBpbj0iU291cmNlR3JhcGhpYyIgaW4yPSJCYWNrZ3JvdW5kSW1hZ2VGaXgiIHJlc3VsdD0ic2hhcGUiLz48ZmVHYXVzc2lhbkJsdXIgcmVzdWx0PSJlZmZlY3QxX2ZvcmVncm91bmRCbHVyXzIwMDJfMTcxNTgiIHN0ZERldmlhdGlvbj0iNy42NTkiLz48L2ZpbHRlcj48ZmlsdGVyIGlkPSJjIiB3aWR0aD0iOTAuMzQiIGhlaWdodD0iNTEuNDM3IiB4PSItNTQuNjEzIiB5PSItNy41MzMiIGNvbG9yLWludGVycG9sYXRpb24tZmlsdGVycz0ic1JHQiIgZmlsdGVyVW5pdHM9InVzZXJTcGFjZU9uVXNlIj48ZmVGbG9vZCBmbG9vZC1vcGFjaXR5PSIwIiByZXN1bHQ9IkJhY2tncm91bmRJbWFnZUZpeCIvPjxmZUJsZW5kIGluPSJTb3VyY2VHcmFwaGljIiBpbjI9IkJhY2tncm91bmRJbWFnZUZpeCIgcmVzdWx0PSJzaGFwZSIvPjxmZUdhdXNzaWFuQmx1ciByZXN1bHQ9ImVmZmVjdDFfZm9yZWdyb3VuZEJsdXJfMjAwMl8xNzE1OCIgc3RkRGV2aWF0aW9uPSI3LjY1OSIvPjwvZmlsdGVyPjxmaWx0ZXIgaWQ9ImQiIHdpZHRoPSI3OS4zNTUiIGhlaWdodD0iMjkuNCIgeD0iLTQ5LjY0IiB5PSIyLjAzIiBjb2xvci1pbnRlcnBvbGF0aW9uLWZpbHRlcnM9InNSR0IiIGZpbHRlclVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PGZlRmxvb2QgZmxvb2Qtb3BhY2l0eT0iMCIgcmVzdWx0PSJCYWNrZ3JvdW5kSW1hZ2VGaXgiLz48ZmVCbGVuZCBpbj0iU291cmNlR3JhcGhpYyIgaW4yPSJCYWNrZ3JvdW5kSW1hZ2VGaXgiIHJlc3VsdD0ic2hhcGUiLz48ZmVHYXVzc2lhbkJsdXIgcmVzdWx0PSJlZmZlY3QxX2ZvcmVncm91bmRCbHVyXzIwMDJfMTcxNTgiIHN0ZERldmlhdGlvbj0iNC41OTYiLz48L2ZpbHRlcj48ZmlsdGVyIGlkPSJlIiB3aWR0aD0iNzkuNTc5IiBoZWlnaHQ9IjI5LjQiIHg9Ii00NS4wNDUiIHk9IjIwLjAyOSIgY29sb3ItaW50ZXJwb2xhdGlvbi1maWx0ZXJzPSJzUkdCIiBmaWx0ZXJVbml0cz0idXNlclNwYWNlT25Vc2UiPjxmZUZsb29kIGZsb29kLW9wYWNpdHk9IjAiIHJlc3VsdD0iQmFja2dyb3VuZEltYWdlRml4Ii8+PGZlQmxlbmQgaW49IlNvdXJjZUdyYXBoaWMiIGluMj0iQmFja2dyb3VuZEltYWdlRml4IiByZXN1bHQ9InNoYXBlIi8+PGZlR2F1c3NpYW5CbHVyIHJlc3VsdD0iZWZmZWN0MV9mb3JlZ3JvdW5kQmx1cl8yMDAyXzE3MTU4IiBzdGREZXZpYXRpb249IjQuNTk2Ii8+PC9maWx0ZXI+PGZpbHRlciBpZD0iZiIgd2lkdGg9Ijc5LjU3OSIgaGVpZ2h0PSIyOS40IiB4PSItNDMuNTEzIiB5PSIyMS4xNzgiIGNvbG9yLWludGVycG9sYXRpb24tZmlsdGVycz0ic1JHQiIgZmlsdGVyVW5pdHM9InVzZXJTcGFjZU9uVXNlIj48ZmVGbG9vZCBmbG9vZC1vcGFjaXR5PSIwIiByZXN1bHQ9IkJhY2tncm91bmRJbWFnZUZpeCIvPjxmZUJsZW5kIGluPSJTb3VyY2VHcmFwaGljIiBpbjI9IkJhY2tncm91bmRJbWFnZUZpeCIgcmVzdWx0PSJzaGFwZSIvPjxmZUdhdXNzaWFuQmx1ciByZXN1bHQ9ImVmZmVjdDFfZm9yZWdyb3VuZEJsdXJfMjAwMl8xNzE1OCIgc3RkRGV2aWF0aW9uPSI0LjU5NiIvPjwvZmlsdGVyPjxmaWx0ZXIgaWQ9ImciIHdpZHRoPSI3NC43NDkiIGhlaWdodD0iNTguODUyIiB4PSIxNS43NTYiIHk9Ii0xNy45MDEiIGNvbG9yLWludGVycG9sYXRpb24tZmlsdGVycz0ic1JHQiIgZmlsdGVyVW5pdHM9InVzZXJTcGFjZU9uVXNlIj48ZmVGbG9vZCBmbG9vZC1vcGFjaXR5PSIwIiByZXN1bHQ9IkJhY2tncm91bmRJbWFnZUZpeCIvPjxmZUJsZW5kIGluPSJTb3VyY2VHcmFwaGljIiBpbjI9IkJhY2tncm91bmRJbWFnZUZpeCIgcmVzdWx0PSJzaGFwZSIvPjxmZUdhdXNzaWFuQmx1ciByZXN1bHQ9ImVmZmVjdDFfZm9yZWdyb3VuZEJsdXJfMjAwMl8xNzE1OCIgc3RkRGV2aWF0aW9uPSI3LjY1OSIvPjwvZmlsdGVyPjxmaWx0ZXIgaWQ9ImgiIHdpZHRoPSI2MS4zNzciIGhlaWdodD0iMjUuMzYyIiB4PSIyMy41NDgiIHk9IjIuMjg0IiBjb2xvci1pbnRlcnBvbGF0aW9uLWZpbHRlcnM9InNSR0IiIGZpbHRlclVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PGZlRmxvb2QgZmxvb2Qtb3BhY2l0eT0iMCIgcmVzdWx0PSJCYWNrZ3JvdW5kSW1hZ2VGaXgiLz48ZmVCbGVuZCBpbj0iU291cmNlR3JhcGhpYyIgaW4yPSJCYWNrZ3JvdW5kSW1hZ2VGaXgiIHJlc3VsdD0ic2hhcGUiLz48ZmVHYXVzc2lhbkJsdXIgcmVzdWx0PSJlZmZlY3QxX2ZvcmVncm91bmRCbHVyXzIwMDJfMTcxNTgiIHN0ZERldmlhdGlvbj0iNC41OTYiLz48L2ZpbHRlcj48ZmlsdGVyIGlkPSJpIiB3aWR0aD0iNjEuMzc3IiBoZWlnaHQ9IjI1LjM2MiIgeD0iMjMuNTQ4IiB5PSIyLjI4NCIgY29sb3ItaW50ZXJwb2xhdGlvbi1maWx0ZXJzPSJzUkdCIiBmaWx0ZXJVbml0cz0idXNlclNwYWNlT25Vc2UiPjxmZUZsb29kIGZsb29kLW9wYWNpdHk9IjAiIHJlc3VsdD0iQmFja2dyb3VuZEltYWdlRml4Ii8+PGZlQmxlbmQgaW49IlNvdXJjZUdyYXBoaWMiIGluMj0iQmFja2dyb3VuZEltYWdlRml4IiByZXN1bHQ9InNoYXBlIi8+PGZlR2F1c3NpYW5CbHVyIHJlc3VsdD0iZWZmZWN0MV9mb3JlZ3JvdW5kQmx1cl8yMDAyXzE3MTU4IiBzdGREZXZpYXRpb249IjQuNTk2Ii8+PC9maWx0ZXI+PGZpbHRlciBpZD0iaiIgd2lkdGg9IjU2LjA0NSIgaGVpZ2h0PSI2My42NDkiIHg9Ii0yNy42MzYiIHk9Ii0yMi44NTMiIGNvbG9yLWludGVycG9sYXRpb24tZmlsdGVycz0ic1JHQiIgZmlsdGVyVW5pdHM9InVzZXJTcGFjZU9uVXNlIj48ZmVGbG9vZCBmbG9vZC1vcGFjaXR5PSIwIiByZXN1bHQ9IkJhY2tncm91bmRJbWFnZUZpeCIvPjxmZUJsZW5kIGluPSJTb3VyY2VHcmFwaGljIiBpbjI9IkJhY2tncm91bmRJbWFnZUZpeCIgcmVzdWx0PSJzaGFwZSIvPjxmZUdhdXNzaWFuQmx1ciByZXN1bHQ9ImVmZmVjdDFfZm9yZWdyb3VuZEJsdXJfMjAwMl8xNzE1OCIgc3RkRGV2aWF0aW9uPSI0LjU5NiIvPjwvZmlsdGVyPjxmaWx0ZXIgaWQ9ImsiIHdpZHRoPSI1NC44MTQiIGhlaWdodD0iNjQuNjQ2IiB4PSIyMC4xMTYiIHk9Ii0zOC40MTUiIGNvbG9yLWludGVycG9sYXRpb24tZmlsdGVycz0ic1JHQiIgZmlsdGVyVW5pdHM9InVzZXJTcGFjZU9uVXNlIj48ZmVGbG9vZCBmbG9vZC1vcGFjaXR5PSIwIiByZXN1bHQ9IkJhY2tncm91bmRJbWFnZUZpeCIvPjxmZUJsZW5kIGluPSJTb3VyY2VHcmFwaGljIiBpbjI9IkJhY2tncm91bmRJbWFnZUZpeCIgcmVzdWx0PSJzaGFwZSIvPjxmZUdhdXNzaWFuQmx1ciByZXN1bHQ9ImVmZmVjdDFfZm9yZWdyb3VuZEJsdXJfMjAwMl8xNzE1OCIgc3RkRGV2aWF0aW9uPSI0LjU5NiIvPjwvZmlsdGVyPjxmaWx0ZXIgaWQ9ImwiIHdpZHRoPSIzMy41NDEiIGhlaWdodD0iMzUuMzEzIiB4PSIyNC42NDEiIHk9Ii0xMS4zMjMiIGNvbG9yLWludGVycG9sYXRpb24tZmlsdGVycz0ic1JHQiIgZmlsdGVyVW5pdHM9InVzZXJTcGFjZU9uVXNlIj48ZmVGbG9vZCBmbG9vZC1vcGFjaXR5PSIwIiByZXN1bHQ9IkJhY2tncm91bmRJbWFnZUZpeCIvPjxmZUJsZW5kIGluPSJTb3VyY2VHcmFwaGljIiBpbjI9IkJhY2tncm91bmRJbWFnZUZpeCIgcmVzdWx0PSJzaGFwZSIvPjxmZUdhdXNzaWFuQmx1ciByZXN1bHQ9ImVmZmVjdDFfZm9yZWdyb3VuZEJsdXJfMjAwMl8xNzE1OCIgc3RkRGV2aWF0aW9uPSI0LjU5NiIvPjwvZmlsdGVyPjxmaWx0ZXIgaWQ9Im0iIHdpZHRoPSI1NC44MTQiIGhlaWdodD0iNjQuNjQ2IiB4PSItMjkuMjg2IiB5PSI2LjAwOSIgY29sb3ItaW50ZXJwb2xhdGlvbi1maWx0ZXJzPSJzUkdCIiBmaWx0ZXJVbml0cz0idXNlclNwYWNlT25Vc2UiPjxmZUZsb29kIGZsb29kLW9wYWNpdHk9IjAiIHJlc3VsdD0iQmFja2dyb3VuZEltYWdlRml4Ii8+PGZlQmxlbmQgaW49IlNvdXJjZUdyYXBoaWMiIGluMj0iQmFja2dyb3VuZEltYWdlRml4IiByZXN1bHQ9InNoYXBlIi8+PGZlR2F1c3NpYW5CbHVyIHJlc3VsdD0iZWZmZWN0MV9mb3JlZ3JvdW5kQmx1cl8yMDAyXzE3MTU4IiBzdGREZXZpYXRpb249IjQuNTk2Ii8+PC9maWx0ZXI+PGZpbHRlciBpZD0ibiIgd2lkdGg9IjU0LjgxNCIgaGVpZ2h0PSI2NC42NDYiIHg9Ii0yOS4yODYiIHk9IjYuMDA5IiBjb2xvci1pbnRlcnBvbGF0aW9uLWZpbHRlcnM9InNSR0IiIGZpbHRlclVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PGZlRmxvb2QgZmxvb2Qtb3BhY2l0eT0iMCIgcmVzdWx0PSJCYWNrZ3JvdW5kSW1hZ2VGaXgiLz48ZmVCbGVuZCBpbj0iU291cmNlR3JhcGhpYyIgaW4yPSJCYWNrZ3JvdW5kSW1hZ2VGaXgiIHJlc3VsdD0ic2hhcGUiLz48ZmVHYXVzc2lhbkJsdXIgcmVzdWx0PSJlZmZlY3QxX2ZvcmVncm91bmRCbHVyXzIwMDJfMTcxNTgiIHN0ZERldmlhdGlvbj0iNC41OTYiLz48L2ZpbHRlcj48ZmlsdGVyIGlkPSJvIiB3aWR0aD0iNTQuODE0IiBoZWlnaHQ9IjY0LjY0NiIgeD0iOC4yNDQiIHk9Ii0yLjQxNiIgY29sb3ItaW50ZXJwb2xhdGlvbi1maWx0ZXJzPSJzUkdCIiBmaWx0ZXJVbml0cz0idXNlclNwYWNlT25Vc2UiPjxmZUZsb29kIGZsb29kLW9wYWNpdHk9IjAiIHJlc3VsdD0iQmFja2dyb3VuZEltYWdlRml4Ii8+PGZlQmxlbmQgaW49IlNvdXJjZUdyYXBoaWMiIGluMj0iQmFja2dyb3VuZEltYWdlRml4IiByZXN1bHQ9InNoYXBlIi8+PGZlR2F1c3NpYW5CbHVyIHJlc3VsdD0iZWZmZWN0MV9mb3JlZ3JvdW5kQmx1cl8yMDAyXzE3MTU4IiBzdGREZXZpYXRpb249IjQuNTk2Ii8+PC9maWx0ZXI+PGZpbHRlciBpZD0icCIgd2lkdGg9IjM5LjQwOSIgaGVpZ2h0PSI0My42MjMiIHg9IjE4LjcxMyIgeT0iMTAuNTg4IiBjb2xvci1pbnRlcnBvbGF0aW9uLWZpbHRlcnM9InNSR0IiIGZpbHRlclVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PGZlRmxvb2QgZmxvb2Qtb3BhY2l0eT0iMCIgcmVzdWx0PSJCYWNrZ3JvdW5kSW1hZ2VGaXgiLz48ZmVCbGVuZCBpbj0iU291cmNlR3JhcGhpYyIgaW4yPSJCYWNrZ3JvdW5kSW1hZ2VGaXgiIHJlc3VsdD0ic2hhcGUiLz48ZmVHYXVzc2lhbkJsdXIgcmVzdWx0PSJlZmZlY3QxX2ZvcmVncm91bmRCbHVyXzIwMDJfMTcxNTgiIHN0ZERldmlhdGlvbj0iNC41OTYiLz48L2ZpbHRlcj48L2RlZnM+PC9zdmc+" />
5
+ <link rel="icon" type="image/svg+xml" href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI2NCIgaGVpZ2h0PSI2NCIgdmlld0JveD0iMCAwIDY0IDY0IiBmaWxsPSJub25lIiByb2xlPSJpbWciIGFyaWEtbGFiZWw9IkdyYXBob3JpYSI+CiAgPHN0eWxlPi5ibGFkZXtzdHJva2U6IzBGMTcyMH1AbWVkaWEgKHByZWZlcnMtY29sb3Itc2NoZW1lOmRhcmspey5ibGFkZXtzdHJva2U6I0ZCRkNGQ319PC9zdHlsZT4KICA8ZyBzdHJva2U9IiMxMkEzQkYiIHN0cm9rZS13aWR0aD0iNC42IiBzdHJva2UtbGluZWNhcD0icm91bmQiPgogICAgPHBhdGggZD0iTTM5LjMxIDguMDkgQTI1IDI1IDAgMCAxIDU2LjM2IDM3LjYzIj48L3BhdGg+CiAgICA8cGF0aCBkPSJNNDkuMDUgNTAuMjkgQTI1IDI1IDAgMCAxIDE0Ljk1IDUwLjI5Ij48L3BhdGg+CiAgICA8cGF0aCBkPSJNNy42NCAzNy42MyBBMjUgMjUgMCAwIDEgMjQuNjkgOC4wOSI+PC9wYXRoPgogIDwvZz4KICA8ZyBjbGFzcz0iYmxhZGUiIHN0cm9rZS13aWR0aD0iNC44IiBzdHJva2UtbGluZWNhcD0icm91bmQiPgogICAgPHBhdGggZD0iTTM0LjA2IDIzLjc1IEEyNCAyNCAwIDAgMSAzMS4zNyAxNC4wMSI+PC9wYXRoPgogICAgPHBhdGggZD0iTTM4LjExIDM3LjkxIEEyNCAyNCAwIDAgMSA0Ny44OSA0MC40NSI+PC9wYXRoPgogICAgPHBhdGggZD0iTTIzLjgzIDM0LjM0IEEyNCAyNCAwIDAgMSAxNi43NCA0MS41NCI+PC9wYXRoPgogIDwvZz4KICA8Y2lyY2xlIGN4PSIzMiIgY3k9IjciIHI9IjUuMiIgZmlsbD0iIzEyQTNCRiI+PC9jaXJjbGU+CiAgPGNpcmNsZSBjeD0iNTMuNjUiIGN5PSI0NC41IiByPSI1LjIiIGZpbGw9IiMxMkEzQkYiPjwvY2lyY2xlPgogIDxjaXJjbGUgY3g9IjEwLjM1IiBjeT0iNDQuNSIgcj0iNS4yIiBmaWxsPSIjMTJBM0JGIj48L2NpcmNsZT4KICA8Y2lyY2xlIGN4PSIzMiIgY3k9IjMyIiByPSI3IiBmaWxsPSIjMTRCMzg5Ij48L2NpcmNsZT4KPC9zdmc+" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
7
  <title>Graphoria - Scalar Playground</title>
8
8
  <style>html,body{height:100%;margin:0;padding:0}#scalar-client{width:100%!important}.missing-endpoint{padding:32px;font-family:monospace}.token-banner{position:fixed;color:#fff;z-index:10001;animation:3s forwards fadeOut;background:#22c55e;border-radius:6px;padding:8px 16px;font-family:system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Noto Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif;font-size:14px;top:10px;right:10px}@keyframes fadeOut{0%,70%{opacity:1}to{opacity:0;visibility:hidden}}
@@ -77,6 +77,16 @@ function computeDepth(
77
77
  * const errors = validate(schema, parse(query), [depthLimitRule(10)]);
78
78
  * ```
79
79
  */
80
+ /**
81
+ * Identity, not message matching: a caller classifying a rejection needs to
82
+ * tell this rule's error from an ordinary validation error, and the message is
83
+ * not a contract. Weak so a held error never pins the entry.
84
+ */
85
+ const depthErrors = new WeakSet<GraphQLError>();
86
+
87
+ export const isDepthLimitError = (error: unknown): boolean =>
88
+ error instanceof GraphQLError && depthErrors.has(error);
89
+
80
90
  export const depthLimitRule =
81
91
  (maxDepth: number) =>
82
92
  (context: ValidationContext): ASTVisitor => {
@@ -92,12 +102,12 @@ export const depthLimitRule =
92
102
  if (depth > maxDepth) {
93
103
  const operationName = def.name?.value ?? "anonymous";
94
104
 
95
- context.reportError(
96
- new GraphQLError(
97
- `Query depth of ${depth} exceeds the maximum allowed depth of ${maxDepth} (operation: "${operationName}"). Raise MAX_QUERY_DEPTH to allow deeper queries.`,
98
- { nodes: [def] },
99
- ),
105
+ const error = new GraphQLError(
106
+ `Query depth of ${depth} exceeds the maximum allowed depth of ${maxDepth} (operation: "${operationName}"). Raise MAX_QUERY_DEPTH to allow deeper queries.`,
107
+ { nodes: [def] },
100
108
  );
109
+ depthErrors.add(error);
110
+ context.reportError(error);
101
111
  }
102
112
  }
103
113
  },
@@ -3,7 +3,7 @@ import type { Env } from "../types/env";
3
3
  import { logger } from "../logging";
4
4
  import { matchesAnySecret } from "./secrets";
5
5
 
6
- export type Capability = "console:read" | "console:write" | "ai" | "mcp";
6
+ export type Capability = "console:read" | "console:write" | "ai" | "mcp" | "metrics";
7
7
 
8
8
  /** `superset` is true when the admin secret, not a scoped credential, matched. */
9
9
  export type CapabilityGrant = { superset: boolean };
@@ -27,6 +27,8 @@ const scopedSecretsFor = (env: Env, capability: Capability): string[][] => {
27
27
  return [env.ai.secrets];
28
28
  case "mcp":
29
29
  return [env.ai.mcp.secrets];
30
+ case "metrics":
31
+ return [env.metrics.secrets];
30
32
  }
31
33
  };
32
34
 
@@ -1,7 +1,6 @@
1
- import { RedisClient as ValkeyClient } from "bun";
2
-
3
1
  import { parseDurationToMs } from "./duration";
4
2
  import { logger } from "../logging";
3
+ import { createRedisClient } from "../utils/redis";
5
4
 
6
5
  export type TokenRepository = {
7
6
  saveJti(jti: string, expiresIn: string): Promise<void>;
@@ -80,4 +79,4 @@ export const createTokenRepository = (redisUrl: string): TokenRepository =>
80
79
  // Bun's RedisClient exposes the same hset/hmget/expire surface we need but
81
80
  // its declared types are wider than TokenRepositoryClient. The cast is the
82
81
  // structural-typing bridge and is intentional.
83
- createTokenRepositoryWithClient(new ValkeyClient(redisUrl) as unknown as TokenRepositoryClient);
82
+ createTokenRepositoryWithClient(createRedisClient(redisUrl) as unknown as TokenRepositoryClient);
@@ -51,14 +51,28 @@ export const RelationshipConditionZod = z
51
51
  operator: z.enum(RELATIONSHIP_CONDITION_OPERATORS).optional().default("eq"),
52
52
  /** Literal compared against the column (omit for `is_null` / `is_not_null`) */
53
53
  value: z.union([z.string(), z.number(), z.boolean()]).optional(),
54
+ /**
55
+ * Column on an ancestor table, compared against this condition's column
56
+ * instead of a literal. The ancestor must sit above the relationship in the
57
+ * query path; the nearest match wins, and a query that does not traverse it
58
+ * is rejected rather than joined without the predicate.
59
+ */
60
+ ancestor: z
61
+ .strictObject({ schema: z.string(), name: z.string(), column: z.string() })
62
+ .optional(),
54
63
  })
55
64
  .refine((cond) => (cond.source === undefined) !== (cond.target === undefined), {
56
65
  message: 'Relationship condition must set exactly one of "source" or "target"',
57
66
  })
58
67
  .refine(
59
68
  (cond) =>
60
- cond.operator === "is_null" || cond.operator === "is_not_null" || cond.value !== undefined,
61
- { message: 'Relationship condition requires a "value" unless operator is is_null/is_not_null' },
69
+ cond.operator === "is_null" || cond.operator === "is_not_null"
70
+ ? cond.ancestor === undefined
71
+ : (cond.value === undefined) !== (cond.ancestor === undefined),
72
+ {
73
+ message:
74
+ 'Relationship condition requires exactly one of "value" or "ancestor", and neither operand may be an "ancestor" when operator is is_null/is_not_null',
75
+ },
62
76
  );
63
77
 
64
78
  export type RelationshipCondition = z.input<typeof RelationshipConditionZod>;
@@ -12,7 +12,7 @@ import type { SessionContext } from "../../utils/sessionVariables";
12
12
 
13
13
  import { analyzeQuery } from "../../analyzeQuery";
14
14
  import { checkQueryCost } from "../../analyzeQuery/costLimit";
15
- import { depthLimitRule } from "../../analyzeQuery/depthLimit";
15
+ import { depthLimitRule, isDepthLimitError } from "../../analyzeQuery/depthLimit";
16
16
  import { resolveVariableRef, resolveVariables } from "../../analyzeQuery/resolveVariables";
17
17
  import { callStoredProcedure, executeQueryJSON, generateSQL } from "../../databases";
18
18
  import { proxyRemoteField } from "../../remoteSchemas/proxy";
@@ -25,6 +25,8 @@ import { EntitySource } from "../../types/resolver";
25
25
  import { filterResultBySelection } from "../../utils/selection";
26
26
  import { handleAuthMeQuery, handleAuthMutation } from "./gqlAuthOperations";
27
27
  import { logger } from "../../logging";
28
+ import { incMetric, isMetricsEnabled, observeMetric } from "../../observability/metrics";
29
+ import { startSpan, withActiveSpan } from "../../observability/tracing";
28
30
 
29
31
  // Handle GraphQL query
30
32
  export const handleGraphQLRequestFactory = (
@@ -107,7 +109,7 @@ export const handleGraphQLRequestFactory = (
107
109
  };
108
110
  },
109
111
 
110
- [EntitySource.STORED_PROCEDURE]: async (field, variables) => {
112
+ [EntitySource.STORED_PROCEDURE]: async (field, variables, queryAnalysis, _req, session) => {
111
113
  const sp = entities.mutationsMap[field.name];
112
114
 
113
115
  if (!sp) {
@@ -124,6 +126,14 @@ export const handleGraphQLRequestFactory = (
124
126
  const result = await callStoredProcedure(
125
127
  sp,
126
128
  argumentsReplaced as Record<string, string | number | boolean | null>,
129
+ {
130
+ operation: {
131
+ type: "mutation",
132
+ name: queryAnalysis.operations[0]?.name ?? null,
133
+ fields: [field.name],
134
+ },
135
+ role: session?.role,
136
+ },
127
137
  );
128
138
 
129
139
  return {
@@ -236,6 +246,7 @@ export const handleGraphQLRequestFactory = (
236
246
  ) => {
237
247
  const enforceDepthLimit = options?.enforceDepthLimit ?? true;
238
248
  const enforceCostLimit = options?.enforceCostLimit ?? true;
249
+ let costRejected = false;
239
250
  const entry = getCacheEntry(query);
240
251
  // Unparseable queries aren't cached; let parse surface the syntax error
241
252
  const document = entry?.document ?? parse(query);
@@ -263,7 +274,20 @@ export const handleGraphQLRequestFactory = (
263
274
  env.maxQueryCost,
264
275
  );
265
276
 
266
- if (costError) validationErrors = [costError];
277
+ if (costError) {
278
+ validationErrors = [costError];
279
+ costRejected = true;
280
+ }
281
+ }
282
+
283
+ if (validationErrors.length > 0) {
284
+ incMetric("graphoria_graphql_rejections_total", {
285
+ reason: costRejected
286
+ ? "cost"
287
+ : validationErrors.some(isDepthLimitError)
288
+ ? "depth"
289
+ : "validation",
290
+ });
267
291
  }
268
292
 
269
293
  return {
@@ -295,186 +319,243 @@ export const handleGraphQLRequestFactory = (
295
319
  const startTime = Bun.nanoseconds();
296
320
 
297
321
  // Reuse cached analysis on repeated identical queries
322
+ const analyzeSpan = startSpan("graphoria.analyze");
298
323
  const entry = isString(query) ? getCacheEntry(query) : undefined;
299
324
  let queryAnalysis: AnalysisResult;
300
325
  if (entry?.analysis) {
301
326
  queryAnalysis = entry.analysis;
327
+ analyzeSpan?.setAttribute("graphoria.analysis.cached", true);
302
328
  } else {
303
329
  queryAnalysis = isString(query) ? analyzeQuery(query, entities, gqlEntities.schema) : query;
304
330
  if (entry) entry.analysis = queryAnalysis;
331
+ analyzeSpan?.setAttribute("graphoria.analysis.cached", false);
305
332
  }
333
+ analyzeSpan?.end();
306
334
 
307
335
  if (queryAnalysis.operations.length === 0) {
308
336
  return { data: {} };
309
337
  }
310
338
 
311
339
  const operation = queryAnalysis.operations[0];
312
- log.debug(
313
- {
314
- operation: operation.operation,
315
- name: operation.name,
316
- fieldCount: operation.fields?.length,
317
- queryLength: isString(query) ? query.length : undefined,
318
- },
319
- "graphql request",
320
- );
321
-
322
- // Single pass: validate, flatten object vars, resolve field args + session vars
323
- // Returns an immutable ResolvedOperation — original operation is not mutated
324
- const resolved = resolveVariables(operation, variables, session);
325
-
326
- if (operation.operation === "query") {
327
- // Separate auth fields, remote schema fields, and table fields
328
- const authFields = resolved.fields.filter((field) => field.source === EntitySource.AUTH);
329
- const remoteFields = resolved.fields.filter(
330
- (field) => field.source === EntitySource.REMOTE_SCHEMA,
331
- );
332
- const aiFields = resolved.fields.filter((field) => field.source === EntitySource.AI);
333
- const tableFields = resolved.fields.filter(
334
- (field) =>
335
- field.source !== EntitySource.AUTH &&
336
- field.source !== EntitySource.REMOTE_SCHEMA &&
337
- field.source !== EntitySource.AI,
340
+ // An anonymous operation is named by its root fields, exactly as the slow
341
+ // query log names one: the document itself would carry inline literals,
342
+ // which are caller data.
343
+ const operationName =
344
+ operation.name ?? (operation.fields ?? []).map((field) => field.name).join(",");
345
+ const role = session?.role ?? "anonymous";
346
+ const recordOperation = (outcome: "success" | "error") => {
347
+ if (!isMetricsEnabled()) return;
348
+ const labels = {
349
+ operation: operationName,
350
+ type: operation.operation,
351
+ role,
352
+ };
353
+ incMetric("graphoria_graphql_operations_total", { ...labels, outcome });
354
+ observeMetric(
355
+ "graphoria_graphql_operation_duration_seconds",
356
+ labels,
357
+ (Bun.nanoseconds() - startTime) / 1e9,
338
358
  );
359
+ };
360
+ let outcome: "success" | "error" = "success";
361
+ const span = startSpan(`${operation.operation} ${operationName}`, {
362
+ attributes: {
363
+ "graphql.operation.name": operationName,
364
+ "graphql.operation.type": operation.operation,
365
+ "graphoria.role": role,
366
+ },
367
+ });
339
368
 
340
- // Handle auth queries (e.g. auth_me)
341
- let authData: Record<string, unknown> = {};
342
- for (const field of authFields) {
343
- Object.assign(authData, handleAuthMeQuery(field, session));
344
- }
345
-
346
- // Handle remote schema queries in parallel
347
- let remoteData: Record<string, unknown> = {};
348
- if (remoteFields.length > 0) {
349
- const remoteResults = await Promise.all(
350
- remoteFields.map(async (field) => {
351
- const entry = entities.remoteQueriesMap[field.name];
352
- if (!entry) {
353
- throw new Error(`Remote schema query not found: ${field.name}`);
354
- }
355
- const result = await proxyRemoteField(
356
- field,
357
- entry.remoteSchema,
358
- entry.originalFieldName,
359
- resolved.allVariables,
360
- "query",
361
- req,
362
- );
363
- return { [field.alias || field.name]: result };
364
- }),
365
- );
366
- remoteData = remoteResults.reduce((acc, curr) => Object.assign(acc, curr), {});
367
- }
368
-
369
- // Handle AI agent queries (admin-only `ask` field)
370
- let aiData: Record<string, unknown> = {};
371
- for (const field of aiFields) {
372
- const alias = field.alias || field.name;
373
- const prompt = String(field.arguments?.prompt ?? "");
374
- audit().emit({
375
- action: "ai.ask",
376
- actor: actorFromSession(session),
377
- target: { kind: "ai", via: "graphql" },
378
- prompt,
379
- });
380
- aiData[alias] = await getAgent()(prompt);
381
- }
382
-
383
- // Skip SQL generation if there are no table fields
384
- if (tableFields.length === 0) {
369
+ // The span is entered rather than passed: the statements this operation
370
+ // runs reach the executor through call chains that carry no span, and the
371
+ // async context is what joins them to this one.
372
+ return withActiveSpan(span, async () => {
373
+ try {
385
374
  log.debug(
386
- { durationMs: (Bun.nanoseconds() - startTime) / 1e6 },
387
- "graphql request completed (no table fields)",
388
- );
389
- return { data: { ...authData, ...remoteData, ...aiData } };
390
- }
391
-
392
- const tableQueryAnalysis = {
393
- ...queryAnalysis,
394
- operations: [
395
375
  {
396
- ...operation,
397
- fields: tableFields,
398
- variables: resolved.variables,
376
+ operation: operation.operation,
377
+ name: operation.name,
378
+ fieldCount: operation.fields?.length,
379
+ queryLength: isString(query) ? query.length : undefined,
399
380
  },
400
- ],
401
- };
381
+ "graphql request",
382
+ );
402
383
 
403
- const sqlQueries = generateSQL(
404
- entities,
405
- tableQueryAnalysis,
406
- resolved.allVariables,
407
- false,
408
- (options?.enforcePageLimits ?? true) ? pageLimits : null,
409
- options?.timeoutMs,
410
- );
384
+ // Single pass: validate, flatten object vars, resolve field args + session vars
385
+ // Returns an immutable ResolvedOperation — original operation is not mutated
386
+ const resolved = resolveVariables(operation, variables, session);
411
387
 
412
- const data = await Promise.all<object>(
413
- sqlQueries.map(([db, query]) =>
414
- executeQueryJSON(
415
- query,
416
- db,
417
- resolved.variables,
418
- resolved.allVariables as Record<string, string | number | boolean | null>,
419
- options?.timeoutMs,
420
- ),
421
- ),
422
- );
388
+ if (operation.operation === "query") {
389
+ // Separate auth fields, remote schema fields, and table fields
390
+ const authFields = resolved.fields.filter(
391
+ (field) => field.source === EntitySource.AUTH,
392
+ );
393
+ const remoteFields = resolved.fields.filter(
394
+ (field) => field.source === EntitySource.REMOTE_SCHEMA,
395
+ );
396
+ const aiFields = resolved.fields.filter((field) => field.source === EntitySource.AI);
397
+ const tableFields = resolved.fields.filter(
398
+ (field) =>
399
+ field.source !== EntitySource.AUTH &&
400
+ field.source !== EntitySource.REMOTE_SCHEMA &&
401
+ field.source !== EntitySource.AI,
402
+ );
423
403
 
424
- log.debug(
425
- { durationMs: (Bun.nanoseconds() - startTime) / 1e6, dbCount: sqlQueries.length },
426
- "graphql request completed",
427
- );
404
+ // Handle auth queries (e.g. auth_me)
405
+ let authData: Record<string, unknown> = {};
406
+ for (const field of authFields) {
407
+ Object.assign(authData, handleAuthMeQuery(field, session));
408
+ }
409
+
410
+ // Handle remote schema queries in parallel
411
+ let remoteData: Record<string, unknown> = {};
412
+ if (remoteFields.length > 0) {
413
+ const remoteResults = await Promise.all(
414
+ remoteFields.map(async (field) => {
415
+ const entry = entities.remoteQueriesMap[field.name];
416
+ if (!entry) {
417
+ throw new Error(`Remote schema query not found: ${field.name}`);
418
+ }
419
+ const result = await proxyRemoteField(
420
+ field,
421
+ entry.remoteSchema,
422
+ entry.originalFieldName,
423
+ resolved.allVariables,
424
+ "query",
425
+ req,
426
+ );
427
+ return { [field.alias || field.name]: result };
428
+ }),
429
+ );
430
+ remoteData = remoteResults.reduce((acc, curr) => Object.assign(acc, curr), {});
431
+ }
432
+
433
+ // Handle AI agent queries (admin-only `ask` field)
434
+ let aiData: Record<string, unknown> = {};
435
+ for (const field of aiFields) {
436
+ const alias = field.alias || field.name;
437
+ const prompt = String(field.arguments?.prompt ?? "");
438
+ audit().emit({
439
+ action: "ai.ask",
440
+ actor: actorFromSession(session),
441
+ target: { kind: "ai", via: "graphql" },
442
+ prompt,
443
+ });
444
+ aiData[alias] = await getAgent()(prompt);
445
+ }
446
+
447
+ // Skip SQL generation if there are no table fields
448
+ if (tableFields.length === 0) {
449
+ log.debug(
450
+ { durationMs: (Bun.nanoseconds() - startTime) / 1e6 },
451
+ "graphql request completed (no table fields)",
452
+ );
453
+ return { data: { ...authData, ...remoteData, ...aiData } };
454
+ }
455
+
456
+ const tableQueryAnalysis = {
457
+ ...queryAnalysis,
458
+ operations: [
459
+ {
460
+ ...operation,
461
+ fields: tableFields,
462
+ variables: resolved.variables,
463
+ },
464
+ ],
465
+ };
428
466
 
429
- return {
430
- ...(env.queryOnResponse
431
- ? {
432
- sqlQueries: sqlQueries.map(([db, query]) => ({
433
- db: db.name,
434
- query,
435
- })),
436
- }
437
- : {}),
438
- data: data.reduce((acc, curr) => Object.assign(acc, curr), {
439
- ...authData,
440
- ...remoteData,
441
- ...aiData,
442
- }),
443
- };
444
- } else if (operation.operation === "mutation") {
445
- // Route mutation to the appropriate handler based on field source
446
- let results: Record<string, object> = {};
467
+ const sqlQueries = generateSQL(
468
+ entities,
469
+ tableQueryAnalysis,
470
+ resolved.allVariables,
471
+ false,
472
+ (options?.enforcePageLimits ?? true) ? pageLimits : null,
473
+ options?.timeoutMs,
474
+ );
447
475
 
448
- for await (const field of resolved.fields) {
449
- const source = field.source;
476
+ const data = await Promise.all<object>(
477
+ sqlQueries.map(([db, query]) =>
478
+ executeQueryJSON(
479
+ query,
480
+ db,
481
+ resolved.variables,
482
+ resolved.allVariables as Record<string, string | number | boolean | null>,
483
+ options?.timeoutMs,
484
+ {
485
+ operation: {
486
+ type: operation.operation,
487
+ name: operation.name,
488
+ fields: tableFields.map((field) => field.name),
489
+ },
490
+ role: session?.role,
491
+ },
492
+ ),
493
+ ),
494
+ );
450
495
 
451
- if (source && mutationHandlers[source]) {
452
- const result = await mutationHandlers[source]!(
453
- field,
454
- resolved.allVariables,
455
- queryAnalysis,
456
- req,
457
- session,
496
+ log.debug(
497
+ { durationMs: (Bun.nanoseconds() - startTime) / 1e6, dbCount: sqlQueries.length },
498
+ "graphql request completed",
458
499
  );
459
500
 
460
- results = {
461
- ...results,
462
- ...result.data,
501
+ return {
502
+ ...(env.queryOnResponse
503
+ ? {
504
+ sqlQueries: sqlQueries.map(([db, query]) => ({
505
+ db: db.name,
506
+ query,
507
+ })),
508
+ }
509
+ : {}),
510
+ data: data.reduce((acc, curr) => Object.assign(acc, curr), {
511
+ ...authData,
512
+ ...remoteData,
513
+ ...aiData,
514
+ }),
463
515
  };
464
- }
465
- }
516
+ } else if (operation.operation === "mutation") {
517
+ // Route mutation to the appropriate handler based on field source
518
+ let results: Record<string, object> = {};
519
+
520
+ for await (const field of resolved.fields) {
521
+ const source = field.source;
522
+
523
+ if (source && mutationHandlers[source]) {
524
+ const result = await mutationHandlers[source]!(
525
+ field,
526
+ resolved.allVariables,
527
+ queryAnalysis,
528
+ req,
529
+ session,
530
+ );
531
+
532
+ results = {
533
+ ...results,
534
+ ...result.data,
535
+ };
536
+ }
537
+ }
466
538
 
467
- log.debug(
468
- { durationMs: (Bun.nanoseconds() - startTime) / 1e6 },
469
- "graphql request completed",
470
- );
539
+ log.debug(
540
+ { durationMs: (Bun.nanoseconds() - startTime) / 1e6 },
541
+ "graphql request completed",
542
+ );
471
543
 
472
- return {
473
- data: results,
474
- };
475
- }
544
+ return {
545
+ data: results,
546
+ };
547
+ }
476
548
 
477
- return { data: {} };
549
+ return { data: {} };
550
+ } catch (error) {
551
+ outcome = "error";
552
+ span?.recordError(error);
553
+ throw error;
554
+ } finally {
555
+ recordOperation(outcome);
556
+ span?.end();
557
+ }
558
+ });
478
559
  },
479
560
  };
480
561
 
@@ -11,7 +11,7 @@ import { createConsoleSessions } from "./session";
11
11
  import { getTags } from "../configuration/rest/generateOpenAPI";
12
12
  import { audit } from "../logging/audit";
13
13
  import { getCronJobs } from "../singletons/cron";
14
- import { databasesConnections } from "../singletons/databases";
14
+ import { databasesConnections, pingConnection } from "../singletons/databases";
15
15
  import { queueManager } from "../singletons/queues";
16
16
  import { resolveClientAddress } from "../utils/rateLimit";
17
17
  import { S200, S400, S401, S403, S404, S429 } from "../utils/responses";
@@ -35,11 +35,6 @@ type ConsoleRouteHandler = (
35
35
 
36
36
  const PING_TIMEOUT_MS = 2000;
37
37
 
38
- const pingConnection = (connection: SQL | ConnectionPool, type: string) =>
39
- type === "mssql"
40
- ? (connection as ConnectionPool).query("SELECT 1")
41
- : (connection as SQL).unsafe("SELECT 1");
42
-
43
38
  const measureLatency = async (connection: SQL | ConnectionPool, type: string) => {
44
39
  const start = performance.now();
45
40
  let timer: ReturnType<typeof setTimeout> | undefined;