Skip to content

Commit 8ecea93

Browse files
committed
Implement graphql queries structuring.
1 parent f7c5fab commit 8ecea93

3 files changed

Lines changed: 438 additions & 3 deletions

File tree

lib/index.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,8 @@ export declare function stringify(
8181
): string
8282

8383
export declare function structure(
84-
object: object,
84+
object: object | string,
85+
format?: string
8586
): object
8687

8788
export declare function validate(

lib/structure.js

Lines changed: 286 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,291 @@
11
/**
2-
* @param {object} object
2+
* @param {object|string} object
3+
* @param {string} format
34
* @return {object}
45
* */
5-
export function structure (object) {
6+
export function structure (object, format = '') {
7+
if (isGraphqlSource(object, format)) {
8+
return parseGraphql(object)
9+
}
610
return JSON.parse(JSON.stringify(object))
711
}
12+
13+
function isGraphqlSource (object, format) {
14+
if (typeof object !== 'string') {
15+
return false
16+
}
17+
if (['graphql', 'gql'].includes(format)) {
18+
return true
19+
}
20+
21+
return !format && /^\s*(query|mutation|subscription)\b/.test(object)
22+
}
23+
24+
function parseGraphql (source) {
25+
const parser = createParser(source)
26+
const document = {}
27+
28+
while (parser.hasMore()) {
29+
const { key, value } = parser.parseDefinition()
30+
document[key] = value
31+
}
32+
33+
return document
34+
}
35+
36+
function createParser (source) {
37+
let index = 0
38+
39+
return {
40+
hasMore,
41+
parseDefinition
42+
}
43+
44+
function hasMore () {
45+
skipIgnored()
46+
return index < source.length
47+
}
48+
49+
function parseDefinition () {
50+
const key = parseName()
51+
const value = {}
52+
53+
if (isOperationType(key) && isNameStart(peek())) {
54+
value.__name = parseName()
55+
}
56+
57+
const arguments_ = parseArguments()
58+
if (arguments_) {
59+
value.__arguments = arguments_
60+
}
61+
62+
Object.assign(value, parseSelectionSet())
63+
return { key, value }
64+
}
65+
66+
function parseSelectionSet () {
67+
eat('{')
68+
const fields = {}
69+
70+
while (!eat('}')) {
71+
const { key, value } = parseField()
72+
fields[key] = value
73+
}
74+
75+
return fields
76+
}
77+
78+
function parseField () {
79+
const first = parseName()
80+
let key = first
81+
82+
if (eat(':')) {
83+
key = `${first}: ${parseName()}`
84+
}
85+
86+
const arguments_ = parseArguments()
87+
88+
if (peek() === '{') {
89+
const value = parseSelectionSet()
90+
if (arguments_) {
91+
return { key, value: { __arguments: arguments_, ...value } }
92+
}
93+
return { key, value }
94+
}
95+
96+
if (arguments_) {
97+
return { key, value: { __arguments: arguments_ } }
98+
}
99+
100+
return { key, value: true }
101+
}
102+
103+
function parseArguments () {
104+
if (!eat('(')) {
105+
return null
106+
}
107+
108+
const arguments_ = {}
109+
while (!eat(')')) {
110+
const key = parseName()
111+
eat(':')
112+
arguments_[key] = parseValue()
113+
}
114+
115+
return Object.keys(arguments_).length ? arguments_ : null
116+
}
117+
118+
function parseValue () {
119+
const char = peek()
120+
121+
if (char === '"') {
122+
return parseString()
123+
}
124+
125+
if (char === '[') {
126+
return parseList()
127+
}
128+
129+
if (char === '{') {
130+
return parseObject()
131+
}
132+
133+
if (char === '-' || isDigit(char)) {
134+
return parseNumber()
135+
}
136+
137+
const value = parseName()
138+
if (value === 'true') {
139+
return true
140+
}
141+
if (value === 'false') {
142+
return false
143+
}
144+
if (value === 'null') {
145+
return null
146+
}
147+
return value
148+
}
149+
150+
function parseString () {
151+
eat('"')
152+
let value = ''
153+
154+
while (source[index] !== '"') {
155+
if (source[index] === '\\') {
156+
value += decodeEscape(source[index + 1])
157+
index += 2
158+
} else {
159+
value += source[index]
160+
index += 1
161+
}
162+
}
163+
164+
eat('"')
165+
return value
166+
}
167+
168+
function parseList () {
169+
eat('[')
170+
const value = []
171+
172+
while (!eat(']')) {
173+
value.push(parseValue())
174+
}
175+
176+
return value
177+
}
178+
179+
function parseObject () {
180+
eat('{')
181+
const value = {}
182+
183+
while (!eat('}')) {
184+
const key = parseName()
185+
eat(':')
186+
value[key] = parseValue()
187+
}
188+
189+
return value
190+
}
191+
192+
function parseNumber () {
193+
let value = ''
194+
195+
if (peek() === '-') {
196+
value += source[index]
197+
index += 1
198+
}
199+
200+
while (isDigit(source[index])) {
201+
value += source[index]
202+
index += 1
203+
}
204+
205+
if (source[index] === '.') {
206+
value += source[index]
207+
index += 1
208+
while (isDigit(source[index])) {
209+
value += source[index]
210+
index += 1
211+
}
212+
}
213+
214+
return Number(value)
215+
}
216+
217+
function parseName () {
218+
skipIgnored()
219+
220+
let value = ''
221+
while (isNamePart(source[index])) {
222+
value += source[index]
223+
index += 1
224+
}
225+
226+
return value
227+
}
228+
229+
function eat (char) {
230+
skipIgnored()
231+
if (source[index] === char) {
232+
index += 1
233+
return true
234+
}
235+
236+
return false
237+
}
238+
239+
function peek () {
240+
skipIgnored()
241+
return source[index]
242+
}
243+
244+
function skipIgnored () {
245+
while (index < source.length) {
246+
const char = source[index]
247+
if (char === ',' || /\s/.test(char)) {
248+
index += 1
249+
continue
250+
}
251+
252+
if (char === '#') {
253+
while (index < source.length && source[index] !== '\n') {
254+
index += 1
255+
}
256+
continue
257+
}
258+
259+
break
260+
}
261+
}
262+
}
263+
264+
function decodeEscape (char) {
265+
return {
266+
'\\': '\\',
267+
'"': '"',
268+
'/': '/',
269+
b: '\b',
270+
f: '\f',
271+
n: '\n',
272+
r: '\r',
273+
t: '\t'
274+
}[char] ?? char
275+
}
276+
277+
function isOperationType (value) {
278+
return ['query', 'mutation', 'subscription'].includes(value)
279+
}
280+
281+
function isDigit (value) {
282+
return /[0-9]/.test(value ?? '')
283+
}
284+
285+
function isNameStart (value) {
286+
return /[A-Za-z_$]/.test(value ?? '')
287+
}
288+
289+
function isNamePart (value) {
290+
return /[A-Za-z0-9_$]/.test(value ?? '')
291+
}

0 commit comments

Comments
 (0)