Skip to content

Commit

Permalink
write the library
Browse files Browse the repository at this point in the history
  • Loading branch information
v1rtl committed Mar 20, 2021
1 parent fdd4288 commit fb3a5c2
Show file tree
Hide file tree
Showing 5 changed files with 218 additions and 109 deletions.
26 changes: 26 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: CI

# Controls when the action will run. Triggers the workflow on push or pull request
# events but only for the master branch
on:
push:
branches: [master]
pull_request:
branches: [master]

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: denolib/setup-deno@v2
with:
deno-version: v1.8
- name: Run tests
run: deno test --unstable --coverage=coverage
- name: Create coverage report
run: deno --unstable coverage ./coverage --lcov > coverage.lcov
- name: Collect coverage
uses: codecov/codecov-action@v1.0.10
with:
file: ./coverage.lcov
44 changes: 43 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,45 @@
# graphql-tag

🦕 Deno port of `graphql-tag` library.
[![GitHub release (latest by date)][releases]][releases-page] [![GitHub Workflow Status][gh-actions-img]][github-actions]
[![Codecov][codecov-badge]][codecov] [![][docs-badge]][docs]

> 🦕 Deno port of [graphql-tag](https://github.com/apollographql/graphql-tag) library.
Create a GraphQL schema AST from template literal.

## Example

```ts
import { buildASTSchema, graphql } from 'https://deno.land/x/graphql_deno@v15.0.0/mod.ts'
import { gql } from 'https://deno.land/x/graphql_tag/mod.ts'

const typeDefs = gql`
type Query {
hello: String
}
`

const query = `{ hello }`

const resolvers = { hello: () => 'world' }

const schema = buildASTSchema(typeDefs)

console.log(await graphql(schema, query, resolvers))
```

[releases]: https://img.shields.io/github/v/release/deno-libs/graphql-tag?style=flat-square
[docs-badge]: https://img.shields.io/github/v/release/deno-libs/graphql_tag?color=yellow&label=Documentation&logo=deno&style=flat-square
[docs]: https://doc.deno.land/https/deno.land/x/graphql_tag/mod.ts
[releases-page]: https://github.com/deno-libs/graphql-tag/releases
[gh-actions-img]: https://img.shields.io/github/workflow/status/deno-libs/graphql-tag/CI?style=flat-square
[codecov]: https://codecov.io/gh/deno-libs/graphql-tag
[github-actions]: https://github.com/deno-libs/graphql-tag/actions
[codecov-badge]: https://img.shields.io/codecov/c/gh/deno-libs/graphql-tag?style=flat-square

## Donate

[![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)

[![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)
](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)
22 changes: 22 additions & 0 deletions example.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { buildASTSchema, graphql } from 'https://deno.land/x/graphql_deno@v15.0.0/mod.ts'
import { gql } from './mod.ts'

const typeDefs = gql`
type Query {
hello: String
}
`

const query = `
{
hello
}
`

const resolvers = {
hello: () => 'world'
}

const schema = buildASTSchema(typeDefs)

console.log(await graphql(schema, query, resolvers))
214 changes: 106 additions & 108 deletions mod.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,39 @@
import { parse } from 'https://deno.land/x/graphql_deno@v15.0.0/mod.ts'

// Strip insignificant whitespace
// Note that this could do a lot more, such as reorder fields etc.
const normalize = (x: string) => x.replace(/[\s,]+/g, ' ').trim()
import { parse, DocumentNode, DefinitionNode, Location } from 'https://deno.land/x/graphql_deno@v15.0.0/mod.ts'

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

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

function cacheKeyFromLoc(loc: any) {
return normalize(loc.source.body.substring(loc.start, loc.end))
let printFragmentWarnings = true
let experimentalFragmentVariables = false

// Strip insignificant whitespace
// Note that this could do a lot more, such as reorder fields etc.
function normalize(string: string) {
return string.replace(/[\s,]+/g, ' ').trim()
}

// For testing.
export function resetCaches() {
docCache = {}
fragmentSourceMap = {}
function cacheKeyFromLoc(loc: Location) {
return normalize(loc.source.body.substring(loc.start, loc.end))
}

// Take a unstripped parsed document (query/mutation or even fragment), and
// check all fragment definitions, checking for name->source uniqueness.
// We also want to make sure only unique fragments exist in the document.
let printFragmentWarnings = true
function processFragments(ast: any) {
const astFragmentMap: any = {}
const definitions: any[] = []

for (let i = 0; i < ast.definitions.length; i++) {
const fragmentDefinition = ast.definitions[i]
function processFragments(ast: DocumentNode) {
const seenKeys = new Set<string>()
const definitions: DefinitionNode[] = []

ast.definitions.forEach((fragmentDefinition) => {
if (fragmentDefinition.kind === 'FragmentDefinition') {
const fragmentName = fragmentDefinition.name.value
const sourceKey = cacheKeyFromLoc(fragmentDefinition.loc)
const sourceKey = cacheKeyFromLoc(fragmentDefinition.loc!)

// We know something about this fragment
if (fragmentSourceMap.hasOwnProperty(fragmentName) && !fragmentSourceMap[fragmentName][sourceKey]) {
let sourceKeySet = fragmentSourceMap.get(fragmentName)!
if (sourceKeySet && !sourceKeySet.has(sourceKey)) {
// this is a problem because the app developer is trying to register another fragment with
// the same name as one previously registered. So, we tell them about it.
if (printFragmentWarnings) {
Expand All @@ -48,122 +45,123 @@ function processFragments(ast: any) {
'this in the docs: http://dev.apollodata.com/core/fragments.html#unique-names'
)
}

fragmentSourceMap[fragmentName][sourceKey] = true
} else if (!fragmentSourceMap.hasOwnProperty(fragmentName)) {
fragmentSourceMap[fragmentName] = {}
fragmentSourceMap[fragmentName][sourceKey] = true
} else if (!sourceKeySet) {
fragmentSourceMap.set(fragmentName, (sourceKeySet = new Set()))
}

if (!astFragmentMap[sourceKey]) {
astFragmentMap[sourceKey] = true
sourceKeySet.add(sourceKey)

if (!seenKeys.has(sourceKey)) {
seenKeys.add(sourceKey)
definitions.push(fragmentDefinition)
}
} else {
definitions.push(fragmentDefinition)
}
}

ast.definitions = definitions
return ast
}
})

export function disableFragmentWarnings() {
printFragmentWarnings = false
return {
...ast,
definitions
}
}

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

if (docType === '[object Array]') {
return doc.map(function (d: any) {
return stripLoc(d, removeLocAtThisLevel)
workSet.forEach((node) => {
if (node.loc) delete node.loc
Object.keys(node).forEach((key) => {
const value = node[key]
if (value && typeof value === 'object') {
workSet.add(value)
}
})
}

if (docType !== '[object Object]') {
throw new Error('Unexpected input.')
}

// We don't want to remove the root loc field so we can use it
// for fragment substitution (see below)
if (removeLocAtThisLevel && doc.loc) {
delete doc.loc
}
})

// https://github.com/apollographql/graphql-tag/issues/40
if (doc.loc) {
delete doc.loc.startToken
delete doc.loc.endToken
const loc = doc.loc as Record<string, any>
if (loc) {
delete loc.startToken
delete loc.endToken
}

const keys = Object.keys(doc)
let key
let value
let valueType

for (key in keys) {
if (keys.hasOwnProperty(key)) {
value = doc[keys[key]]
valueType = Object.prototype.toString.call(value)
return doc
}

if (valueType === '[object Object]' || valueType === '[object Array]') {
doc[keys[key]] = stripLoc(value, true)
}
function parseDocument(source: string) {
var cacheKey = normalize(source)
if (!docCache.has(cacheKey)) {
const parsed = parse(source, {
experimentalFragmentVariables
})
if (!parsed || parsed.kind !== 'Document') {
throw new Error('Not a valid GraphQL document.')
}
docCache.set(
cacheKey,
// check that all "new" fragments inside the documents are consistent with
// existing fragments of the same name
stripLoc(processFragments(parsed))
)
}

return doc
return docCache.get(cacheKey)!
}

let experimentalFragmentVariables = false

function parseDocument(doc: string) {
const cacheKey = normalize(doc)

if (docCache[cacheKey]) {
return docCache[cacheKey]
/**
* Create a GraphQL AST from template literal
* @param literals
* @param args
*
* @example
* ```ts
* import { buildASTSchema, graphql } from 'https://deno.land/x/graphql_deno@v15.0.0/mod.ts'
* import { gql } from 'https://deno.land/x/graphql_tag/mod.ts'
*
* const typeDefs = gql`
* type Query {
* hello: String
* }
*`
*
* const query = `{ hello }`
*
* const resolvers = { hello: () => 'world' }
*
* console.log(await graphql(buildASTSchema(typeDefs), query, resolvers))
* ```
*/
export function gql(literals: string | readonly string[], ...args: any[]) {
if (typeof literals === 'string') {
literals = [literals]
}

let parsed = parse(doc, {
experimentalFragmentVariables
let result = literals[0]

args.forEach((arg, i) => {
if (arg && arg.kind === 'Document') {
result += arg.loc.source.body
} else {
result += arg
}
result += literals[i + 1]
})
if (!parsed || parsed.kind !== 'Document') {
throw new Error('Not a valid GraphQL document.')
}

// check that all "new" fragments inside the documents are consistent with
// existing fragments of the same name
parsed = processFragments(parsed)
parsed = stripLoc(parsed, false)
docCache[cacheKey] = parsed
return parseDocument(result)
}

return parsed
export function resetCaches() {
docCache.clear()
fragmentSourceMap.clear()
}

export function enableExperimentalFragmentletiables() {
export function disableFragmentWarnings() {
printFragmentWarnings = false
}

export function enableExperimentalFragmentVariables() {
experimentalFragmentVariables = true
}

export function disableExperimentalFragmentVariables() {
experimentalFragmentVariables = false
}

// XXX This should eventually disallow arbitrary string interpolation, like Relay does
export function gql(...args: any[]) {
// We always get literals[0] and then matching post literals for each arg given
const literals = args[0]
let result = typeof literals === 'string' ? literals : literals[0]

for (let i = 1; i < args.length; i++) {
if (args[i] && args[i].kind && args[i].kind === 'Document') {
result += args[i].loc.source.body
} else {
result += args[i]
}

result += literals[i]
}

return parseDocument(result)
}
21 changes: 21 additions & 0 deletions mod_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { DocumentNode, buildASTSchema, isSchema } from 'https://deno.land/x/graphql_deno@v15.0.0/mod.ts'
import { describe, it, run, expect } from 'https://deno.land/x/wizard@0.1.3/mod.ts'
import { gql } from './mod.ts'

const typeDefs = gql`
type Query {
hello: String
}
`

it('Returns a valid document node', () => {
expect(typeDefs.kind).toBe('Document')
})

it('Creates a valid schema from AST', () => {
const schema = buildASTSchema(typeDefs)

expect(isSchema(schema)).toBe(true)
})

run()

0 comments on commit fb3a5c2

Please sign in to comment.