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