Skip to content

Commit fb3a5c2

Browse files
author
v1rtl
committed
write the library
1 parent fdd4288 commit fb3a5c2

File tree

5 files changed

+218
-109
lines changed

5 files changed

+218
-109
lines changed

.github/workflows/main.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
name: CI
2+
3+
# Controls when the action will run. Triggers the workflow on push or pull request
4+
# events but only for the master branch
5+
on:
6+
push:
7+
branches: [master]
8+
pull_request:
9+
branches: [master]
10+
11+
jobs:
12+
test:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- uses: actions/checkout@v2
16+
- uses: denolib/setup-deno@v2
17+
with:
18+
deno-version: v1.8
19+
- name: Run tests
20+
run: deno test --unstable --coverage=coverage
21+
- name: Create coverage report
22+
run: deno --unstable coverage ./coverage --lcov > coverage.lcov
23+
- name: Collect coverage
24+
uses: codecov/codecov-action@v1.0.10
25+
with:
26+
file: ./coverage.lcov

README.md

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,45 @@
11
# graphql-tag
22

3-
🦕 Deno port of `graphql-tag` library.
3+
[![GitHub release (latest by date)][releases]][releases-page] [![GitHub Workflow Status][gh-actions-img]][github-actions]
4+
[![Codecov][codecov-badge]][codecov] [![][docs-badge]][docs]
5+
6+
> 🦕 Deno port of [graphql-tag](https://github.com/apollographql/graphql-tag) library.
7+
8+
Create a GraphQL schema AST from template literal.
9+
10+
## Example
11+
12+
```ts
13+
import { buildASTSchema, graphql } from 'https://deno.land/x/graphql_deno@v15.0.0/mod.ts'
14+
import { gql } from 'https://deno.land/x/graphql_tag/mod.ts'
15+
16+
const typeDefs = gql`
17+
type Query {
18+
hello: String
19+
}
20+
`
21+
22+
const query = `{ hello }`
23+
24+
const resolvers = { hello: () => 'world' }
25+
26+
const schema = buildASTSchema(typeDefs)
27+
28+
console.log(await graphql(schema, query, resolvers))
29+
```
30+
31+
[releases]: https://img.shields.io/github/v/release/deno-libs/graphql-tag?style=flat-square
32+
[docs-badge]: https://img.shields.io/github/v/release/deno-libs/graphql_tag?color=yellow&label=Documentation&logo=deno&style=flat-square
33+
[docs]: https://doc.deno.land/https/deno.land/x/graphql_tag/mod.ts
34+
[releases-page]: https://github.com/deno-libs/graphql-tag/releases
35+
[gh-actions-img]: https://img.shields.io/github/workflow/status/deno-libs/graphql-tag/CI?style=flat-square
36+
[codecov]: https://codecov.io/gh/deno-libs/graphql-tag
37+
[github-actions]: https://github.com/deno-libs/graphql-tag/actions
38+
[codecov-badge]: https://img.shields.io/codecov/c/gh/deno-libs/graphql-tag?style=flat-square
39+
40+
## Donate
41+
42+
[![PayPal](https://img.shields.io/badge/PayPal-cyan?style=flat-square&logo=paypal)](https://paypal.me/v1rtl) [![ko-fi](https://img.shields.io/badge/kofi-pink?style=flat-square&logo=ko-fi)](https://ko-fi.com/v1rtl) [![Qiwi](https://img.shields.io/badge/qiwi-white?style=flat-square&logo=qiwi)](https://qiwi.com/n/V1RTL) [![Yandex Money](https://img.shields.io/badge/Yandex_Money-yellow?style=flat-square&logo=yandex)](https://money.yandex.ru/to/410014774355272)
43+
44+
[![Bitcoin](https://badge-crypto.vercel.app/api/badge?coin=btc&address=3PxedDftWBXujWtr7TbWQSiYTsZJoMD8K5)](https://badge-crypto.vercel.app/btc/3PxedDftWBXujWtr7TbWQSiYTsZJoMD8K5) [![Ethereum](https://badge-crypto.vercel.app/api/badge?coin=eth&address=0x9d9236DC024958D7fB73Ad9B178BD5D372D82288)
45+
](https://badge-crypto.vercel.app/eth/0x9d9236DC024958D7fB73Ad9B178BD5D372D82288) [![ChainLink](https://badge-crypto.vercel.app/api/badge?coin=link&address=0x9d9236DC024958D7fB73Ad9B178BD5D372D82288)](https://badge-crypto.vercel.app/link/0xcd0da1c9b0DA7D2b862bbF813cB50f76F2fB4F5d)

example.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { buildASTSchema, graphql } from 'https://deno.land/x/graphql_deno@v15.0.0/mod.ts'
2+
import { gql } from './mod.ts'
3+
4+
const typeDefs = gql`
5+
type Query {
6+
hello: String
7+
}
8+
`
9+
10+
const query = `
11+
{
12+
hello
13+
}
14+
`
15+
16+
const resolvers = {
17+
hello: () => 'world'
18+
}
19+
20+
const schema = buildASTSchema(typeDefs)
21+
22+
console.log(await graphql(schema, query, resolvers))

mod.ts

Lines changed: 106 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,39 @@
1-
import { parse } from 'https://deno.land/x/graphql_deno@v15.0.0/mod.ts'
2-
3-
// Strip insignificant whitespace
4-
// Note that this could do a lot more, such as reorder fields etc.
5-
const normalize = (x: string) => x.replace(/[\s,]+/g, ' ').trim()
1+
import { parse, DocumentNode, DefinitionNode, Location } from 'https://deno.land/x/graphql_deno@v15.0.0/mod.ts'
62

73
// A map docString -> graphql document
8-
let docCache: any = {}
4+
const docCache = new Map<string, DocumentNode>()
95

106
// A map fragmentName -> [normalized source]
11-
let fragmentSourceMap: any = {}
7+
const fragmentSourceMap = new Map<string, Set<string>>()
128

13-
function cacheKeyFromLoc(loc: any) {
14-
return normalize(loc.source.body.substring(loc.start, loc.end))
9+
let printFragmentWarnings = true
10+
let experimentalFragmentVariables = false
11+
12+
// Strip insignificant whitespace
13+
// Note that this could do a lot more, such as reorder fields etc.
14+
function normalize(string: string) {
15+
return string.replace(/[\s,]+/g, ' ').trim()
1516
}
1617

17-
// For testing.
18-
export function resetCaches() {
19-
docCache = {}
20-
fragmentSourceMap = {}
18+
function cacheKeyFromLoc(loc: Location) {
19+
return normalize(loc.source.body.substring(loc.start, loc.end))
2120
}
2221

2322
// Take a unstripped parsed document (query/mutation or even fragment), and
2423
// check all fragment definitions, checking for name->source uniqueness.
2524
// We also want to make sure only unique fragments exist in the document.
26-
let printFragmentWarnings = true
27-
function processFragments(ast: any) {
28-
const astFragmentMap: any = {}
29-
const definitions: any[] = []
30-
31-
for (let i = 0; i < ast.definitions.length; i++) {
32-
const fragmentDefinition = ast.definitions[i]
25+
function processFragments(ast: DocumentNode) {
26+
const seenKeys = new Set<string>()
27+
const definitions: DefinitionNode[] = []
3328

29+
ast.definitions.forEach((fragmentDefinition) => {
3430
if (fragmentDefinition.kind === 'FragmentDefinition') {
3531
const fragmentName = fragmentDefinition.name.value
36-
const sourceKey = cacheKeyFromLoc(fragmentDefinition.loc)
32+
const sourceKey = cacheKeyFromLoc(fragmentDefinition.loc!)
3733

3834
// We know something about this fragment
39-
if (fragmentSourceMap.hasOwnProperty(fragmentName) && !fragmentSourceMap[fragmentName][sourceKey]) {
35+
let sourceKeySet = fragmentSourceMap.get(fragmentName)!
36+
if (sourceKeySet && !sourceKeySet.has(sourceKey)) {
4037
// this is a problem because the app developer is trying to register another fragment with
4138
// the same name as one previously registered. So, we tell them about it.
4239
if (printFragmentWarnings) {
@@ -48,122 +45,123 @@ function processFragments(ast: any) {
4845
'this in the docs: http://dev.apollodata.com/core/fragments.html#unique-names'
4946
)
5047
}
51-
52-
fragmentSourceMap[fragmentName][sourceKey] = true
53-
} else if (!fragmentSourceMap.hasOwnProperty(fragmentName)) {
54-
fragmentSourceMap[fragmentName] = {}
55-
fragmentSourceMap[fragmentName][sourceKey] = true
48+
} else if (!sourceKeySet) {
49+
fragmentSourceMap.set(fragmentName, (sourceKeySet = new Set()))
5650
}
5751

58-
if (!astFragmentMap[sourceKey]) {
59-
astFragmentMap[sourceKey] = true
52+
sourceKeySet.add(sourceKey)
53+
54+
if (!seenKeys.has(sourceKey)) {
55+
seenKeys.add(sourceKey)
6056
definitions.push(fragmentDefinition)
6157
}
6258
} else {
6359
definitions.push(fragmentDefinition)
6460
}
65-
}
66-
67-
ast.definitions = definitions
68-
return ast
69-
}
61+
})
7062

71-
export function disableFragmentWarnings() {
72-
printFragmentWarnings = false
63+
return {
64+
...ast,
65+
definitions
66+
}
7367
}
7468

75-
function stripLoc(doc: any, removeLocAtThisLevel: any) {
76-
let docType = Object.prototype.toString.call(doc)
69+
function stripLoc(doc: DocumentNode) {
70+
const workSet = new Set<Record<string, any>>(doc.definitions)
7771

78-
if (docType === '[object Array]') {
79-
return doc.map(function (d: any) {
80-
return stripLoc(d, removeLocAtThisLevel)
72+
workSet.forEach((node) => {
73+
if (node.loc) delete node.loc
74+
Object.keys(node).forEach((key) => {
75+
const value = node[key]
76+
if (value && typeof value === 'object') {
77+
workSet.add(value)
78+
}
8179
})
82-
}
83-
84-
if (docType !== '[object Object]') {
85-
throw new Error('Unexpected input.')
86-
}
87-
88-
// We don't want to remove the root loc field so we can use it
89-
// for fragment substitution (see below)
90-
if (removeLocAtThisLevel && doc.loc) {
91-
delete doc.loc
92-
}
80+
})
9381

94-
// https://github.com/apollographql/graphql-tag/issues/40
95-
if (doc.loc) {
96-
delete doc.loc.startToken
97-
delete doc.loc.endToken
82+
const loc = doc.loc as Record<string, any>
83+
if (loc) {
84+
delete loc.startToken
85+
delete loc.endToken
9886
}
9987

100-
const keys = Object.keys(doc)
101-
let key
102-
let value
103-
let valueType
104-
105-
for (key in keys) {
106-
if (keys.hasOwnProperty(key)) {
107-
value = doc[keys[key]]
108-
valueType = Object.prototype.toString.call(value)
88+
return doc
89+
}
10990

110-
if (valueType === '[object Object]' || valueType === '[object Array]') {
111-
doc[keys[key]] = stripLoc(value, true)
112-
}
91+
function parseDocument(source: string) {
92+
var cacheKey = normalize(source)
93+
if (!docCache.has(cacheKey)) {
94+
const parsed = parse(source, {
95+
experimentalFragmentVariables
96+
})
97+
if (!parsed || parsed.kind !== 'Document') {
98+
throw new Error('Not a valid GraphQL document.')
11399
}
100+
docCache.set(
101+
cacheKey,
102+
// check that all "new" fragments inside the documents are consistent with
103+
// existing fragments of the same name
104+
stripLoc(processFragments(parsed))
105+
)
114106
}
115-
116-
return doc
107+
return docCache.get(cacheKey)!
117108
}
118109

119-
let experimentalFragmentVariables = false
120-
121-
function parseDocument(doc: string) {
122-
const cacheKey = normalize(doc)
123-
124-
if (docCache[cacheKey]) {
125-
return docCache[cacheKey]
110+
/**
111+
* Create a GraphQL AST from template literal
112+
* @param literals
113+
* @param args
114+
*
115+
* @example
116+
* ```ts
117+
* import { buildASTSchema, graphql } from 'https://deno.land/x/graphql_deno@v15.0.0/mod.ts'
118+
* import { gql } from 'https://deno.land/x/graphql_tag/mod.ts'
119+
*
120+
* const typeDefs = gql`
121+
* type Query {
122+
* hello: String
123+
* }
124+
*`
125+
*
126+
* const query = `{ hello }`
127+
*
128+
* const resolvers = { hello: () => 'world' }
129+
*
130+
* console.log(await graphql(buildASTSchema(typeDefs), query, resolvers))
131+
* ```
132+
*/
133+
export function gql(literals: string | readonly string[], ...args: any[]) {
134+
if (typeof literals === 'string') {
135+
literals = [literals]
126136
}
127137

128-
let parsed = parse(doc, {
129-
experimentalFragmentVariables
138+
let result = literals[0]
139+
140+
args.forEach((arg, i) => {
141+
if (arg && arg.kind === 'Document') {
142+
result += arg.loc.source.body
143+
} else {
144+
result += arg
145+
}
146+
result += literals[i + 1]
130147
})
131-
if (!parsed || parsed.kind !== 'Document') {
132-
throw new Error('Not a valid GraphQL document.')
133-
}
134148

135-
// check that all "new" fragments inside the documents are consistent with
136-
// existing fragments of the same name
137-
parsed = processFragments(parsed)
138-
parsed = stripLoc(parsed, false)
139-
docCache[cacheKey] = parsed
149+
return parseDocument(result)
150+
}
140151

141-
return parsed
152+
export function resetCaches() {
153+
docCache.clear()
154+
fragmentSourceMap.clear()
142155
}
143156

144-
export function enableExperimentalFragmentletiables() {
157+
export function disableFragmentWarnings() {
158+
printFragmentWarnings = false
159+
}
160+
161+
export function enableExperimentalFragmentVariables() {
145162
experimentalFragmentVariables = true
146163
}
147164

148165
export function disableExperimentalFragmentVariables() {
149166
experimentalFragmentVariables = false
150167
}
151-
152-
// XXX This should eventually disallow arbitrary string interpolation, like Relay does
153-
export function gql(...args: any[]) {
154-
// We always get literals[0] and then matching post literals for each arg given
155-
const literals = args[0]
156-
let result = typeof literals === 'string' ? literals : literals[0]
157-
158-
for (let i = 1; i < args.length; i++) {
159-
if (args[i] && args[i].kind && args[i].kind === 'Document') {
160-
result += args[i].loc.source.body
161-
} else {
162-
result += args[i]
163-
}
164-
165-
result += literals[i]
166-
}
167-
168-
return parseDocument(result)
169-
}

mod_test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { DocumentNode, buildASTSchema, isSchema } from 'https://deno.land/x/graphql_deno@v15.0.0/mod.ts'
2+
import { describe, it, run, expect } from 'https://deno.land/x/wizard@0.1.3/mod.ts'
3+
import { gql } from './mod.ts'
4+
5+
const typeDefs = gql`
6+
type Query {
7+
hello: String
8+
}
9+
`
10+
11+
it('Returns a valid document node', () => {
12+
expect(typeDefs.kind).toBe('Document')
13+
})
14+
15+
it('Creates a valid schema from AST', () => {
16+
const schema = buildASTSchema(typeDefs)
17+
18+
expect(isSchema(schema)).toBe(true)
19+
})
20+
21+
run()

0 commit comments

Comments
 (0)