pgGraphqlSchemaComment.test.ts 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. import { describe, expect, it } from 'vitest'
  2. import {
  3. buildSchemaCommentWith,
  4. isIntrospectionEnabled,
  5. isPgGraphqlIntrospectionOptIn,
  6. parseSchemaComment,
  7. } from './pgGraphqlSchemaComment'
  8. describe('parseSchemaComment', () => {
  9. it('returns no directive for null', () => {
  10. expect(parseSchemaComment(null)).toEqual({
  11. options: {},
  12. hasDirective: false,
  13. isMalformed: false,
  14. prefix: '',
  15. suffix: '',
  16. })
  17. })
  18. it('returns no directive for undefined', () => {
  19. expect(parseSchemaComment(undefined)).toEqual({
  20. options: {},
  21. hasDirective: false,
  22. isMalformed: false,
  23. prefix: '',
  24. suffix: '',
  25. })
  26. })
  27. it('returns no directive for empty string', () => {
  28. expect(parseSchemaComment('')).toEqual({
  29. options: {},
  30. hasDirective: false,
  31. isMalformed: false,
  32. prefix: '',
  33. suffix: '',
  34. })
  35. })
  36. it('parses a directive with no options', () => {
  37. expect(parseSchemaComment('@graphql({})')).toEqual({
  38. options: {},
  39. hasDirective: true,
  40. isMalformed: false,
  41. prefix: '',
  42. suffix: '',
  43. })
  44. })
  45. it('parses a directive with introspection: true', () => {
  46. expect(parseSchemaComment('@graphql({"introspection": true})')).toMatchObject({
  47. options: { introspection: true },
  48. hasDirective: true,
  49. isMalformed: false,
  50. })
  51. })
  52. it('parses a directive with introspection: false', () => {
  53. expect(parseSchemaComment('@graphql({"introspection": false})')).toMatchObject({
  54. options: { introspection: false },
  55. hasDirective: true,
  56. isMalformed: false,
  57. })
  58. })
  59. it('parses multiple option keys', () => {
  60. expect(
  61. parseSchemaComment(
  62. '@graphql({"introspection": true, "inflect_names": true, "max_rows": 100})'
  63. )
  64. ).toMatchObject({
  65. options: { introspection: true, inflect_names: true, max_rows: 100 },
  66. hasDirective: true,
  67. })
  68. })
  69. it('preserves surrounding text', () => {
  70. const result = parseSchemaComment(
  71. 'a user-written prefix @graphql({"introspection": true}) and a suffix'
  72. )
  73. expect(result).toMatchObject({
  74. options: { introspection: true },
  75. hasDirective: true,
  76. prefix: 'a user-written prefix ',
  77. suffix: ' and a suffix',
  78. })
  79. })
  80. it('treats a comment without a directive as prefix-only', () => {
  81. expect(parseSchemaComment('Just a plain comment.')).toEqual({
  82. options: {},
  83. hasDirective: false,
  84. isMalformed: false,
  85. prefix: 'Just a plain comment.',
  86. suffix: '',
  87. })
  88. })
  89. it('handles whitespace between @graphql and the opening paren', () => {
  90. expect(parseSchemaComment('@graphql ({"introspection": true})')).toMatchObject({
  91. options: { introspection: true },
  92. hasDirective: true,
  93. })
  94. })
  95. it('handles whitespace inside the directive parens', () => {
  96. expect(parseSchemaComment('@graphql( {"introspection": true} )')).toMatchObject({
  97. options: { introspection: true },
  98. hasDirective: true,
  99. })
  100. })
  101. it('handles JSON with nested objects and arrays', () => {
  102. const comment =
  103. '@graphql({"introspection": true, "schema": {"nested": {"deep": [1, 2, 3]}, "list": []}})'
  104. expect(parseSchemaComment(comment)).toMatchObject({
  105. options: {
  106. introspection: true,
  107. schema: { nested: { deep: [1, 2, 3] }, list: [] },
  108. },
  109. hasDirective: true,
  110. })
  111. })
  112. it('handles string values containing braces and parens', () => {
  113. const comment = '@graphql({"label": "value with } { ( ) braces"})'
  114. expect(parseSchemaComment(comment)).toMatchObject({
  115. options: { label: 'value with } { ( ) braces' },
  116. hasDirective: true,
  117. })
  118. })
  119. it('handles escaped quotes inside string values', () => {
  120. const comment = '@graphql({"label": "she said \\"hi\\""})'
  121. expect(parseSchemaComment(comment)).toMatchObject({
  122. options: { label: 'she said "hi"' },
  123. hasDirective: true,
  124. })
  125. })
  126. it('treats invalid JSON as malformed', () => {
  127. expect(parseSchemaComment('@graphql({not valid json})')).toMatchObject({
  128. options: {},
  129. hasDirective: true,
  130. isMalformed: true,
  131. })
  132. })
  133. it('treats trailing-comma JSON as malformed', () => {
  134. expect(parseSchemaComment('@graphql({"introspection": true,})')).toMatchObject({
  135. options: {},
  136. hasDirective: true,
  137. isMalformed: true,
  138. })
  139. })
  140. it('ignores incomplete @graphql with no closing paren', () => {
  141. expect(parseSchemaComment('@graphql({"introspection": true}')).toEqual({
  142. options: {},
  143. hasDirective: false,
  144. isMalformed: false,
  145. prefix: '@graphql({"introspection": true}',
  146. suffix: '',
  147. })
  148. })
  149. it('ignores @graphql followed by something other than {', () => {
  150. expect(parseSchemaComment('@graphql(true)')).toEqual({
  151. options: {},
  152. hasDirective: false,
  153. isMalformed: false,
  154. prefix: '@graphql(true)',
  155. suffix: '',
  156. })
  157. })
  158. it('only matches the first @graphql directive', () => {
  159. const comment = '@graphql({"introspection": true}) tail @graphql({"max_rows": 5})'
  160. const result = parseSchemaComment(comment)
  161. expect(result.options).toEqual({ introspection: true })
  162. expect(result.hasDirective).toBe(true)
  163. expect(result.prefix).toBe('')
  164. expect(result.suffix).toBe(' tail @graphql({"max_rows": 5})')
  165. })
  166. })
  167. describe('buildSchemaCommentWith', () => {
  168. it('produces a directive when comment is null', () => {
  169. expect(buildSchemaCommentWith(null, { introspection: true })).toBe(
  170. '@graphql({"introspection":true})'
  171. )
  172. })
  173. it('produces a directive when comment is undefined', () => {
  174. expect(buildSchemaCommentWith(undefined, { introspection: true })).toBe(
  175. '@graphql({"introspection":true})'
  176. )
  177. })
  178. it('produces a directive when comment is empty', () => {
  179. expect(buildSchemaCommentWith('', { introspection: true })).toBe(
  180. '@graphql({"introspection":true})'
  181. )
  182. })
  183. it('appends a directive after existing non-directive text', () => {
  184. expect(buildSchemaCommentWith('User notes about this schema', { introspection: true })).toBe(
  185. 'User notes about this schema @graphql({"introspection":true})'
  186. )
  187. })
  188. it('avoids double-spacing when existing text already ends in a space', () => {
  189. expect(buildSchemaCommentWith('User notes ', { introspection: true })).toBe(
  190. 'User notes @graphql({"introspection":true})'
  191. )
  192. })
  193. it('replaces only the directive when one exists, preserving surrounding text', () => {
  194. expect(
  195. buildSchemaCommentWith('prefix @graphql({"inflect_names": true}) suffix', {
  196. introspection: true,
  197. })
  198. ).toBe('prefix @graphql({"inflect_names":true,"introspection":true}) suffix')
  199. })
  200. it('merges new keys with existing keys', () => {
  201. expect(
  202. buildSchemaCommentWith('@graphql({"inflect_names": true, "max_rows": 100})', {
  203. introspection: true,
  204. })
  205. ).toBe('@graphql({"inflect_names":true,"max_rows":100,"introspection":true})')
  206. })
  207. it('overrides existing keys with new values', () => {
  208. expect(
  209. buildSchemaCommentWith('@graphql({"introspection": false, "max_rows": 100})', {
  210. introspection: true,
  211. })
  212. ).toBe('@graphql({"introspection":true,"max_rows":100})')
  213. })
  214. it('preserves nested object values when merging', () => {
  215. expect(
  216. buildSchemaCommentWith('@graphql({"schema": {"foo": "bar"}, "max_rows": 100})', {
  217. introspection: true,
  218. })
  219. ).toBe('@graphql({"schema":{"foo":"bar"},"max_rows":100,"introspection":true})')
  220. })
  221. it('discards malformed directive content and writes a clean directive', () => {
  222. expect(
  223. buildSchemaCommentWith('prefix @graphql({not valid json}) suffix', { introspection: true })
  224. ).toBe('prefix @graphql({"introspection":true}) suffix')
  225. })
  226. it('supports disabling introspection', () => {
  227. expect(
  228. buildSchemaCommentWith('@graphql({"introspection": true, "max_rows": 100})', {
  229. introspection: false,
  230. })
  231. ).toBe('@graphql({"introspection":false,"max_rows":100})')
  232. })
  233. })
  234. describe('isIntrospectionEnabled', () => {
  235. it('returns true only when introspection is the boolean true', () => {
  236. expect(isIntrospectionEnabled({ introspection: true })).toBe(true)
  237. })
  238. it.each([
  239. [{}],
  240. [{ introspection: false }],
  241. [{ introspection: 'true' }],
  242. [{ introspection: 1 }],
  243. [{ introspection: null }],
  244. [{ other: true }],
  245. ])('returns false for %j', (options) => {
  246. expect(isIntrospectionEnabled(options as Record<string, unknown>)).toBe(false)
  247. })
  248. })
  249. describe('isPgGraphqlIntrospectionOptIn', () => {
  250. it.each([
  251. ['1.6.0', true],
  252. ['1.6.1', true],
  253. ['1.7.0', true],
  254. ['2.0.0', true],
  255. ['1.5.9', false],
  256. ['1.5.0', false],
  257. ['1.0.0', false],
  258. ['0.9.0', false],
  259. ])('version %s returns %s', (version, expected) => {
  260. expect(isPgGraphqlIntrospectionOptIn(version)).toBe(expected)
  261. })
  262. it('handles versions without patch', () => {
  263. expect(isPgGraphqlIntrospectionOptIn('1.6')).toBe(true)
  264. expect(isPgGraphqlIntrospectionOptIn('1.5')).toBe(false)
  265. })
  266. it('ignores pre-release / build suffixes', () => {
  267. expect(isPgGraphqlIntrospectionOptIn('1.6.0-rc.1')).toBe(true)
  268. expect(isPgGraphqlIntrospectionOptIn('1.5.9-rc.1')).toBe(false)
  269. })
  270. it.each([[null], [undefined], [''], ['not-a-version'], ['x.y.z']])(
  271. 'returns false for unparseable input %j',
  272. (version) => {
  273. expect(isPgGraphqlIntrospectionOptIn(version)).toBe(false)
  274. }
  275. )
  276. })