├── .dockerignore ├── .github ├── CODEOWNERS └── workflows │ ├── cd-dgraph-lambda.yml │ ├── main.yml │ └── stale.yml ├── .gitignore ├── .vscode └── launch.json ├── Dockerfile ├── Readme.md ├── acl └── hmac_secret_file ├── docker-compose.yml ├── ecosystem.config.js ├── jest.config.js ├── jest.setup.js ├── package-lock.json ├── package.json ├── script └── script.js ├── scripts └── .DS_Store ├── slash-graphql-lambda-types ├── index.js ├── package.json └── types.d.ts ├── src ├── dgraph.ts ├── evaluate-script.test.ts ├── evaluate-script.ts ├── index.ts ├── script-to-express.test.ts ├── script-to-express.ts └── test-utils.ts └── tsconfig.json /.dockerignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /dist 3 | /.git 4 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # CODEOWNERS info: https://help.github.com/en/articles/about-code-owners 2 | # Owners are automatically requested for review for PRs that changes code 3 | # that they own. 4 | * @dgraph-io/committers 5 | -------------------------------------------------------------------------------- /.github/workflows/cd-dgraph-lambda.yml: -------------------------------------------------------------------------------- 1 | name: cd-dgraph-lambda 2 | on: workflow_dispatch 3 | jobs: 4 | dgraph-lambda-build: 5 | runs-on: ubuntu-20.04 6 | steps: 7 | - uses: actions/checkout@v3 8 | - name: Set dgraph-lambda Release Version 9 | run: | 10 | #!/bin/bash 11 | GIT_BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD) 12 | if [[ "$GIT_BRANCH_NAME" == "release/v"* ]]; 13 | then 14 | echo "this is a release branch" 15 | else 16 | echo "this is NOT a release branch" 17 | exit 1 18 | fi 19 | DGRAPH_LAMBDA_RELEASE_VERSION=$(git rev-parse --abbrev-ref HEAD | sed 's/release\///') 20 | echo "making a new release for dgraph "$DGRAPH_LAMBDA_RELEASE_VERSION 21 | echo "DGRAPH_LAMBDA_RELEASE_VERSION=$DGRAPH_LAMBDA_RELEASE_VERSION" >> $GITHUB_ENV 22 | - name: Login to DockerHub 23 | uses: docker/login-action@v2 24 | with: 25 | username: ${{ secrets.DOCKERHUB_USERNAME }} 26 | password: ${{ secrets.DOCKERHUB_PASSWORD_TOKEN }} 27 | - name: Install linux dependencies 28 | run: | 29 | #!/bin/bash 30 | sudo apt-get update -y 31 | # buildx requires these base linux packages to run npm install 32 | sudo apt-get install qemu qemu-user-static binfmt-support debootstrap -y 33 | - name: Build and push dgraph-lambda images 34 | run: | 35 | docker buildx create --name builder --driver docker-container 36 | docker buildx use builder 37 | docker buildx build -t dgraph/dgraph-lambda:${{ env.DGRAPH_LAMBDA_RELEASE_VERSION }} -t dgraph/dgraph-lambda:latest --push --platform=linux/arm64,linux/amd64 . 38 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: CI 4 | 5 | # Controls when the action will run. Triggers the workflow on push or pull request 6 | # events but only for the master branch 7 | on: 8 | push: 9 | branches: [ master ] 10 | pull_request: 11 | branches: [ master ] 12 | 13 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 14 | jobs: 15 | # This workflow contains a single job called "build" 16 | build: 17 | # The type of runner that the job will run on 18 | runs-on: ubuntu-latest 19 | 20 | # Steps represent a sequence of tasks that will be executed as part of the job 21 | steps: 22 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 23 | - uses: actions/checkout@v2 24 | 25 | # Pull the latest image to build, and avoid caching pull-only images. 26 | # (docker pull is faster than caching in most cases.) 27 | - run: docker-compose pull 28 | 29 | # In this step, this action saves a list of existing images, 30 | # the cache is created without them in the post run. 31 | # It also restores the cache if it exists. 32 | - uses: satackey/action-docker-layer-caching@v0.0.8 33 | continue-on-error: true 34 | 35 | # Runs a single command using the runners shell 36 | - name: Run Tests 37 | run: docker-compose run --rm -T -e NODE_ENV=test dgraph-lambda test 38 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: 'Close stale issues and PRs' 2 | on: 3 | schedule: 4 | - cron: '30 1 * * *' 5 | 6 | permissions: 7 | issues: write 8 | pull-requests: write 9 | actions: write 10 | 11 | jobs: 12 | stale: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/stale@v9 16 | with: 17 | stale-issue-message: 'This issue has been stale for 60 days and will be closed automatically in 7 days. Comment to keep it open.' 18 | stale-pr-message: 'This PR has been stale for 60 days and will be closed automatically in 7 days. Comment to keep it open.' 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /dist 2 | /node_modules 3 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "request": "attach", 7 | "name": "Attach to Process", 8 | "port": 9230 9 | } 10 | ] 11 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20-alpine as build 2 | 3 | RUN apk add python3 make g++ 4 | WORKDIR /app 5 | COPY package.json package-lock.json ./ 6 | RUN npm install 7 | 8 | COPY . . 9 | ARG nodeEnv=production 10 | ENV NODE_ENV $nodeEnv 11 | RUN npm run build 12 | 13 | # Used just for tests 14 | ENTRYPOINT [ "npm", "run" ] 15 | 16 | FROM node:20-alpine 17 | ENV NODE_ENV production 18 | RUN adduser app -h /app -D 19 | USER app 20 | WORKDIR /app 21 | COPY --from=build --chown=app /app /app 22 | CMD ["npm", "start"] 23 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Dgraph Lambda 2 | 3 | Dgraph Lambda is a serverless platform for running JS on Slash GraphQL (or Dgraph). 4 | 5 | ## Running a script 6 | 7 | A script looks something like this. There are two ways to add a resolver 8 | * `addGraphQLResolver` which recieves `{ parent, args }` and returns a single value 9 | * `addMultiParentGraphQLResolver` which received `{ parents, args }` and should return an array of results, each result matching to one parent. This method will have much better performance if you are able to club multiple requests together 10 | 11 | If the query is a root query/mutation, parents will be set to `[null]`. 12 | 13 | ```javascript 14 | const fullName = ({ parent: { firstName, lastName } }) => `${firstName} ${lastName}` 15 | 16 | async function todoTitles({ graphql }) { 17 | const results = await graphql('{ queryTodo { title } }') 18 | return results.data.queryTodo.map(t => t.title) 19 | } 20 | 21 | self.addGraphQLResolvers({ 22 | "User.fullName": fullName, 23 | "Query.todoTitles": todoTitles, 24 | }) 25 | 26 | async function reallyComplexDql({parents, dql}) { 27 | const ids = parents.map(p => p.id); 28 | const someComplexResults = await dql(`really-complex-query-here with ${ids}`); 29 | return parents.map(parent => someComplexResults[parent.id]) 30 | } 31 | 32 | self.addMultiParentGraphQLResolvers({ 33 | "User.reallyComplexProperty": reallyComplexDql 34 | }) 35 | ``` 36 | 37 | ## Running Locally 38 | 39 | Create a "local-lambda" docker image 40 | ``` 41 | docker build -t local-lambda . 42 | ``` 43 | ### option 1 - run the lambda server alone with 44 | ``` 45 | docker run -d -p 8686:8686 local-lambda 46 | ``` 47 | You can perform a basic test using curl: 48 | 49 | ```bash 50 | curl localhost:8686/graphql-worker -H "Content-Type: application/json" -d '{"resolver":"User.fullName","parents":[{"firstName":"Dgraph","lastName":"Labs"}]}' 51 | ``` 52 | 53 | 54 | ### option 2 - use one of the scripts in dgraph/contrib/local-test 55 | 56 | - change the lambda image used in the script to use local-lambda for your tests 57 | - check the script you want to load. To run the lambda jest, load the script provided in this repo. 58 | 59 | For testing, update exosystem.config.js 60 | 61 | - add node_args: ["--inspect"], to enable debugging. 62 | - set instances to 1, to be sure that the port 9230 will be on the only instance your are testing. 63 | 64 | in VS code, attach to the docker container, and then launch the debug confugration "Attach to Process" 65 | 66 | 67 | ### Tests 68 | run 69 | ``` 70 | docker-compose up 71 | ``` 72 | to get an cluster with Dgraph zero, alpha and lambda server 73 | 74 | run 75 | 76 | ``` 77 | export DGRAPH_URL=http://localhost:8080 78 | 79 | export INTEGRATION_TEST=true 80 | 81 | npm test 82 | ``` 83 | to execute the tests. 84 | 85 | 86 | 87 | ## Environment 88 | 89 | We are trying to make the environment match the environment you'd get from ServiceWorker. 90 | 91 | * [x] fetch 92 | * [x] graphql / dql 93 | * [x] base64 94 | * [x] URL 95 | * [ ] crypto - should test this 96 | 97 | ## Adding libraries 98 | 99 | If you would like to add libraries, then use webpack --target=webworker to compile your script. We'll fill out these instructions later. 100 | 101 | ### Working with Typescript 102 | 103 | You can import `@slash-graphql/lambda-types` to get types for `addGraphQLResolver` and `addGraphQLMultiParentResolver`. 104 | 105 | ## Security 106 | 107 | Currently, this uses node context to try and make sure that users aren't up to any fishy business. However, contexts aren't true security, and we should eventually switch to isolates. In the meanwhile, we will basically have kube kill this if it takes a lot of CPU for say 5 secs 108 | 109 | ## Publishing 110 | 111 | Currently, the publishing of this isn't automated. In order to publish: 112 | * Publish the types in slash-graphql-lambda-types if needed with (npm version minor; npm publish) 113 | * The docker-image auto publishes, but pushing a tag will create a tagged version that is more stable 114 | -------------------------------------------------------------------------------- /acl/hmac_secret_file: -------------------------------------------------------------------------------- 1 | f77dca9a74915c339bff019ffefefb36 2 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | services: 3 | lambda: 4 | build: 5 | context: . 6 | args: 7 | nodeEnv: development 8 | volumes: 9 | - ./script/script.js:/app/script/script.js:ro 10 | environment: 11 | - DGRAPH_URL=http://host.docker.internal:8080 12 | - MAX_MEMORY_LIMIT=256M 13 | ports: 14 | - 8686:8686 15 | depends_on: 16 | - alpha 17 | # Dgraph Zero controls the cluster 18 | zero: 19 | image: dgraph/dgraph:latest 20 | container_name: lambda_dgraph_zero 21 | ports: 22 | - 5080:5080 23 | - 6080:6080 24 | command: dgraph zero --my=zero:5080 --logtostderr -v=2 --telemetry sentry=false 25 | restart: unless-stopped 26 | # Dgraph Alpha hosts the graph and indexes 27 | alpha: 28 | image: dgraph/dgraph:latest 29 | container_name: lambda_dgraph_alpha 30 | volumes: 31 | - ./acl:/config/acl 32 | ports: 33 | - 8080:8080 34 | - 9080:9080 35 | command: > 36 | dgraph alpha --my=alpha:7080 --zero=zero:5080 37 | --security whitelist=0.0.0.0/0 38 | --logtostderr -v=2 39 | --graphql lambda-url=http://host.docker.internal:8686/graphql-worker 40 | --telemetry sentry=false 41 | environment: 42 | DGRAPH_ALPHA_ACL: secret-file=/config/acl/hmac_secret_file 43 | restart: unless-stopped 44 | 45 | 46 | -------------------------------------------------------------------------------- /ecosystem.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | apps: [ 3 | { 4 | name: "dgraph-lambda", 5 | script: "./dist/index.js", 6 | instances: Number(process.env.INSTANCES || 4), 7 | exp_backoff_restart_delay: 100, 8 | max_memory_restart: process.env.MAX_MEMORY_LIMIT || "64M", 9 | watch: ["./script/script.js"], 10 | watch_options: { 11 | followSymlinks: false, 12 | }, 13 | exec_mode: "cluster", 14 | env: { 15 | NODE_ENV: "development", 16 | }, 17 | env_production: { 18 | NODE_ENV: "production", 19 | }, 20 | }, 21 | ], 22 | }; 23 | // add the option node_args: ["--inspect"], to enable debugging. -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "roots": [ 3 | "/src" 4 | ], 5 | "testMatch": [ 6 | "**/__tests__/**/*.+(ts|tsx|js)", 7 | "**/?(*.)+(spec|test).+(ts|tsx|js)" 8 | ], 9 | "transform": { 10 | "^.+\\.(ts|tsx)$": "ts-jest" 11 | }, 12 | "setupFilesAfterEnv": ['./jest.setup.js'] 13 | } 14 | -------------------------------------------------------------------------------- /jest.setup.js: -------------------------------------------------------------------------------- 1 | jest.setTimeout(30000) 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dgraph-lambda", 3 | "version": "1.4.0", 4 | "description": "Serverless Framework for Dgraph", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "start": "pm2-runtime start ecosystem.config.js", 8 | "build": "tsc", 9 | "test": "jest --verbose" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/dgraph-io/dgraph-lambda.git" 14 | }, 15 | "keywords": [ 16 | "dgraph" 17 | ], 18 | "author": "Tejas Dinkar ", 19 | "license": "Apache-2.0", 20 | "bugs": { 21 | "url": "https://github.com/dgraph-io/dgraph-lambda/issues" 22 | }, 23 | "homepage": "https://github.com/dgraph-io/dgraph-lambda#readme", 24 | "dependencies": { 25 | "@slash-graphql/lambda-types": "file:slash-graphql-lambda-types", 26 | "atob": "^2.1.2", 27 | "btoa": "^1.2.1", 28 | "event-target-shim": "^5.0.1", 29 | "express": "^4.17.1", 30 | "node-fetch": "^2.6.1", 31 | "pm2": "^5.3.0", 32 | "sleep-promise": "^8.0.1" 33 | }, 34 | "devDependencies": { 35 | "@types/atob": "^2.1.2", 36 | "@types/btoa": "^1.2.3", 37 | "@types/express": "^4.17.8", 38 | "@types/jest": "^29.5.2", 39 | "@types/node-fetch": "^2.5.7", 40 | "@types/supertest": "^2.0.10", 41 | "jest": "^29.5.0", 42 | "supertest": "^4.0.2", 43 | "ts-jest": "^29.1.1", 44 | "typescript": "^4.9.5" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /script/script.js: -------------------------------------------------------------------------------- 1 | const fullName = ({ parent: { firstName, lastName } }) => 2 | `${firstName} ${lastName}`; 3 | 4 | async function todoTitles({ graphql }) { 5 | const results = await graphql("{ queryTodo { title } }"); 6 | return results.data.queryTodo.map((t) => t.title); 7 | } 8 | 9 | self.addGraphQLResolvers({ 10 | "User.fullName": fullName, 11 | "Query.todoTitles": todoTitles, 12 | }); 13 | 14 | async function reallyComplexDql({ parents, dql }) { 15 | const ids = parents.map((p) => p.id); 16 | const someComplexResults = await dql.query( 17 | `really-complex-query-here with ${ids}` 18 | ); 19 | return parents.map((parent) => someComplexResults[parent.id]); 20 | } 21 | 22 | self.addMultiParentGraphQLResolvers({ 23 | "User.reallyComplexProperty": reallyComplexDql, 24 | }); 25 | 26 | /* 27 | Test functions to use with following Schema 28 | type Query { 29 | dqlquery(query: String): String @lambda 30 | gqlquery(query: String): String @lambda 31 | dqlmutate(query: String): String @lambda 32 | echo(query: String): String @lambda 33 | } 34 | 35 | */ 36 | const echo = async ({args, authHeader, graphql, dql,accessJWT}) => { 37 | let payload=JSON.parse(atob(accessJWT.split('.')[1])); 38 | return `args: ${JSON.stringify(args)} 39 | accesJWT: ${accessJWT} 40 | authHeader: ${JSON.stringify(authHeader)} 41 | namespace: ${payload.namespace}` 42 | } 43 | const dqlquery = async ({args, authHeader, graphql, dql}) => { 44 | 45 | const dqlQ = await dql.query(`${args.query}`) 46 | return JSON.stringify(dqlQ) 47 | } 48 | const gqlquery = async ({args, authHeader, graphql, dql}) => { 49 | const gqlQ = await graphql(`${args.query}`) 50 | return JSON.stringify(gqlQ) 51 | } 52 | const dqlmutation = async ({args, authHeader, graphql, dql}) => { 53 | // Mutate User with DQL 54 | const dqlM = await dql.mutate(`${args.query}`) 55 | return JSON.stringify(dqlM) 56 | } 57 | 58 | self.addGraphQLResolvers({ 59 | "Query.dqlquery": dqlquery, 60 | "Query.gqlquery": gqlquery, 61 | "Query.dqlmutate": dqlmutation, 62 | "Query.echo": echo, 63 | }); 64 | -------------------------------------------------------------------------------- /scripts/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dgraph-io/dgraph-lambda/708ba04cb1b4527d0a59324bcfd7e0d4a803e4f2/scripts/.DS_Store -------------------------------------------------------------------------------- /slash-graphql-lambda-types/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | addGraphQLResolvers: global.addGraphQLResolvers, 3 | addMultiParentGraphQLResolvers: global.addMultiParentGraphQLResolvers, 4 | } 5 | -------------------------------------------------------------------------------- /slash-graphql-lambda-types/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@slash-graphql/lambda-types", 3 | "version": "1.4.0", 4 | "description": "Types for building out a Dgraph Lambda", 5 | "main": "index.js", 6 | "types": "types.d.ts", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "github.com/dgraph-io/dgraph-lambda" 13 | }, 14 | "author": "Tejas Dinkar ", 15 | "license": "Apache-2.0" 16 | } 17 | -------------------------------------------------------------------------------- /slash-graphql-lambda-types/types.d.ts: -------------------------------------------------------------------------------- 1 | declare module "@slash-graphql/lambda-types" { 2 | type GraphQLResponse = { 3 | data?: Record, 4 | errors?: { message: string }[] 5 | } 6 | 7 | type AuthHeaderField = { 8 | key: string | undefined, 9 | value: string | undefined 10 | } 11 | 12 | type InfoField = { 13 | field: selectionField 14 | } 15 | 16 | type selectionField = { 17 | alias: string, 18 | name: string, 19 | arguments: Record, 20 | directives: fldDirectiveList, 21 | selectionSet: selectionSet 22 | } 23 | 24 | type selectionSet = Array 25 | 26 | type fldDirectiveList = Array 27 | 28 | type fldDirective = { 29 | name: string, 30 | arguments: Record 31 | } 32 | 33 | type eventPayload = { 34 | __typename: string, 35 | operation: string, 36 | commitTs: number, 37 | add: addEvent | undefined, 38 | update: updateEvent | undefined, 39 | delete: deleteEvent | undefined 40 | } 41 | 42 | 43 | type addEvent = { 44 | add: { 45 | rootUIDs: Array, 46 | input: Array 47 | } 48 | } 49 | 50 | type updateEvent = { 51 | update: { 52 | rootUIDs: Array, 53 | SetPatch: Object, 54 | RemovePatch: Object 55 | } 56 | } 57 | 58 | type deleteEvent = { 59 | delete: { 60 | rootUIDs: Array 61 | } 62 | } 63 | // body structure of Alpha request 64 | // autHeader contains the key and value of the header used in GraphQL authorization 65 | // the auth key is sepcified in Dgraph.Authorization in the graphql schema 66 | // accessJWT is the optional user token, obtain after login to a tenant 67 | // accessJWT is used by lambda to call back the /graphq /query or /mutate endpoints 68 | // accessJWT can also be decoded by the lambda code to get user id and namespace 69 | // let payload=JSON.parse(atob(accessJWT.split('.')[1])) 70 | // type is $webhook for webhook calls. 71 | 72 | type GraphQLEventFields = { 73 | type: string, 74 | parents: (Record)[] | null, 75 | args: Record, 76 | authHeader?: AuthHeaderField, 77 | event?: eventPayload, 78 | info?: InfoField, 79 | accessJWT?: string 80 | } 81 | 82 | type ResolverResponse = any[] | Promise[] | Promise; 83 | 84 | type GraphQLEventCommonFields = { 85 | type: string; 86 | respondWith: (r: ResolverResponse) => void; 87 | graphql: (s: string, vars: Record | undefined, ah?: AuthHeaderField) => Promise; 88 | dql: { 89 | query: (s: string, vars: Record | undefined) => Promise; 90 | mutate: (s: string) => Promise; 91 | }; 92 | authHeader?: AuthHeaderField; 93 | }; 94 | 95 | type GraphQLEvent = GraphQLEventCommonFields & { 96 | parents: Record[] | null; 97 | args: Record; 98 | info: InfoField; 99 | }; 100 | 101 | type WebHookGraphQLEvent = GraphQLEventCommonFields & { 102 | event?: eventPayload; 103 | }; 104 | 105 | type GraphQLEventWithParent = GraphQLEvent & { 106 | parent: Record | null 107 | } 108 | 109 | function addGraphQLResolvers(resolvers: { 110 | [key: string]: (e: GraphQLEventWithParent) => any; 111 | }): void 112 | 113 | function addWebHookResolvers(resolvers: { 114 | [key: string]: (e: WebHookGraphQLEvent) => any; 115 | }): void 116 | 117 | function addMultiParentGraphQLResolvers(resolvers: { 118 | [key: string]: (e: GraphQLEvent) => ResolverResponse; 119 | }): void 120 | } 121 | -------------------------------------------------------------------------------- /src/dgraph.ts: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch'; 2 | import { GraphQLResponse, AuthHeaderField } from '@slash-graphql/lambda-types'; 3 | 4 | export async function graphql(query: string, variables: Record = {}, authHeader?: AuthHeaderField, accessJWT?:string): Promise { 5 | const headers: Record = { "Content-Type": "application/json" }; 6 | // add graphql auth header is defined 7 | if (authHeader && authHeader.key && authHeader.value) { 8 | headers[authHeader.key] = authHeader.value; 9 | } 10 | // add user access token (login to tenant) if defined 11 | if (accessJWT && accessJWT!='') { 12 | headers["X-Dgraph-AccessToken"] = accessJWT; 13 | } 14 | const response = await fetch(`${process.env.DGRAPH_URL}/graphql`, { 15 | method: "POST", 16 | headers, 17 | body: JSON.stringify({ query, variables }) 18 | }) 19 | if (response.status !== 200) { 20 | throw new Error("Failed to execute GraphQL Query") 21 | } 22 | return response.json(); 23 | } 24 | 25 | 26 | export class dql { 27 | accessJWT: string | undefined; 28 | 29 | constructor(accessJWT?:string) { 30 | // keep access token (login to tenant) if defined 31 | // it will be used by dql.query and dql.mutate methods 32 | this.accessJWT = accessJWT 33 | } 34 | async query(query: string, variables: Record = {}): Promise { 35 | const headers: Record = { "Content-Type": "application/json" }; 36 | headers["X-Auth-Token"] = process.env.DGRAPH_TOKEN || ""; 37 | // add user access token (login to tenant) if defined 38 | if (this.accessJWT && this.accessJWT!='') { 39 | headers["X-Dgraph-AccessToken"] = this.accessJWT; 40 | } 41 | const response = await fetch(`${process.env.DGRAPH_URL}/query`, { 42 | method: "POST", 43 | headers: headers, 44 | body: JSON.stringify({ query, variables }) 45 | }) 46 | if (response.status !== 200) { 47 | throw new Error("Failed to execute DQL Query") 48 | } 49 | return response.json(); 50 | } 51 | 52 | async mutate(mutate: string | Object, commitNow: boolean = true): Promise { 53 | const headers: Record = { }; 54 | headers["Content-Type"] = typeof mutate === 'string' ? "application/rdf" : "application/json" 55 | headers["X-Auth-Token"] = process.env.DGRAPH_TOKEN || ""; 56 | // add user access token (login to tenant) if defined 57 | if (this.accessJWT && this.accessJWT!='') { 58 | headers["X-Dgraph-AccessToken"] = this.accessJWT; 59 | } 60 | const response = await fetch(`${process.env.DGRAPH_URL}/mutate?commitNow=${commitNow}`, { 61 | method: "POST", 62 | headers: headers, 63 | body: typeof mutate === 'string' ? mutate : JSON.stringify(mutate) 64 | }) 65 | if (response.status !== 200) { 66 | throw new Error("Failed to execute DQL Mutate") 67 | } 68 | return response.json(); 69 | } 70 | 71 | async commit(txn: { 72 | start_ts: number; 73 | hash: string; 74 | preds: string[]; 75 | keys: string[]; 76 | }): Promise { 77 | const headers: Record = {}; 78 | headers["Content-Type"] = "application/json"; 79 | headers["X-Auth-Token"] = process.env.DGRAPH_TOKEN || ""; 80 | // add user access token (login to tenant) if defined 81 | if (this.accessJWT && this.accessJWT != "") { 82 | headers["X-Dgraph-AccessToken"] = this.accessJWT; 83 | } 84 | const { start_ts, hash, ...rest } = txn; 85 | const response = await fetch( 86 | `${process.env.DGRAPH_URL}/commit?startTs=${start_ts}&hash=${hash}`, 87 | { 88 | method: "POST", 89 | headers: headers, 90 | body: JSON.stringify(rest), 91 | } 92 | ); 93 | if (response.status !== 200) { 94 | throw new Error("Failed to commit transaction"); 95 | } 96 | return response.json(); 97 | } 98 | 99 | } 100 | 101 | -------------------------------------------------------------------------------- /src/evaluate-script.test.ts: -------------------------------------------------------------------------------- 1 | import { evaluateScript } from './evaluate-script'; 2 | import { waitForDgraph, loadSchema, runQuery , login, addNamespace} from './test-utils' 3 | import sleep from 'sleep-promise'; 4 | 5 | const integrationTest = process.env.INTEGRATION_TEST === "true" ? describe : describe.skip; 6 | 7 | describe(evaluateScript, () => { 8 | it("returns undefined if there was no event", async () => { 9 | const runScript = evaluateScript("") 10 | expect(await runScript({type: "Query.unknown", args: {}, parents: null})).toBeUndefined() 11 | }) 12 | 13 | it("returns the value if there is a resolver registered", async () => { 14 | const runScript = evaluateScript(`addGraphQLResolvers({ 15 | "Query.fortyTwo": ({parent}) => 42 16 | })`) 17 | expect(await runScript({ type: "Query.fortyTwo", args: {}, parents: null })).toEqual(42) 18 | }) 19 | 20 | it("passes the args and parents over", async () => { 21 | const runScript = evaluateScript(`addMultiParentGraphQLResolvers({ 22 | "User.fortyTwo": ({parents, args}) => parents.map(({n}) => n + args.foo) 23 | })`) 24 | expect(await runScript({ type: "User.fortyTwo", args: {foo: 1}, parents: [{n: 41}] })).toEqual([42]) 25 | }) 26 | 27 | it("returns undefined if the number of parents doesn't match the number of return types", async () => { 28 | const runScript = evaluateScript(`addMultiParentGraphQLResolvers({ 29 | "Query.fortyTwo": () => [41, 42] 30 | })`) 31 | expect(await runScript({ type: "Query.fortyTwo", args: {}, parents: null })).toBeUndefined() 32 | }) 33 | 34 | it("returns undefined somehow the script doesn't return an array", async () => { 35 | const runScript = evaluateScript(`addMultiParentGraphQLResolvers({ 36 | "User.fortyTwo": () => ({}) 37 | })`) 38 | expect(await runScript({ type: "User.fortyTwo", args: {}, parents: [{n: 42}] })).toBeUndefined() 39 | }) 40 | 41 | integrationTest("dgraph integration", () => { 42 | var accessJWT0: string 43 | var accessJWT: string 44 | var namespaceInfo: any 45 | beforeAll(async () => { 46 | await waitForDgraph(); 47 | accessJWT0 = await login("groot","password",0) 48 | namespaceInfo = await addNamespace("tenant1",accessJWT0) 49 | 50 | accessJWT = await login("groot","tenant1",namespaceInfo["namespaceId"]) 51 | await loadSchema(` 52 | type Todo { id: ID!, title: String! } 53 | type Query { 54 | dqlquery(query: String): String @lambda 55 | gqlquery(query: String): String @lambda 56 | dqlmutate(query: String): String @lambda 57 | echo(query: String): String @lambda 58 | }`,accessJWT) 59 | await sleep(250) 60 | await runQuery(`mutation { addTodo(input: [{title: "Kick Ass"}, {title: "Chew Bubblegum"}]) { numUids } }`,accessJWT) 61 | 62 | }) 63 | it("obtain accessJWT", async () => { 64 | expect(accessJWT != undefined) 65 | }) 66 | it("add namespace ", async () => { 67 | expect(namespaceInfo["message"] == "Created namespace successfully") 68 | }) 69 | it("works with dgraph graphql", async () => { 70 | const runScript = evaluateScript(` 71 | async function todoTitles({graphql}) { 72 | const results = await graphql('{ queryTodo { title } }') 73 | return results.data.queryTodo.map(t => t.title) 74 | } 75 | addGraphQLResolvers({ "Query.todoTitles": todoTitles })`) 76 | const results = await runScript({ type: "Query.todoTitles", args: {}, parents: null , accessJWT: accessJWT}); 77 | expect(new Set(results)).toEqual(new Set(["Kick Ass", "Chew Bubblegum"])) 78 | }) 79 | 80 | it("works with dgraph dql", async () => { 81 | const runScript = evaluateScript(` 82 | async function todoTitles({dql}) { 83 | const results = await dql.query('{ queryTitles(func: type(Todo)){ Todo.title } }') 84 | return results.data.queryTitles.map(t => t["Todo.title"]) 85 | } 86 | addGraphQLResolvers({ "Query.todoTitles": todoTitles })`) 87 | const results = await runScript({ type: "Query.todoTitles", args: {}, parents: null, accessJWT: accessJWT}); 88 | expect(new Set(results)).toEqual(new Set(["Kick Ass", "Chew Bubblegum"])) 89 | }) 90 | }) 91 | }) 92 | -------------------------------------------------------------------------------- /src/evaluate-script.ts: -------------------------------------------------------------------------------- 1 | import { EventTarget } from 'event-target-shim'; 2 | import vm from 'vm'; 3 | import { GraphQLEvent, GraphQLEventWithParent, GraphQLEventFields, ResolverResponse, AuthHeaderField, WebHookGraphQLEvent } from '@slash-graphql/lambda-types' 4 | import * as crypto from "crypto"; 5 | import fetch, { Request, Response, Headers } from "node-fetch"; 6 | import { URL } from "url"; 7 | import atob from "atob"; 8 | import btoa from "btoa"; 9 | import { TextDecoder, TextEncoder } from "util"; 10 | 11 | import { graphql, dql } from './dgraph'; 12 | 13 | function getParents(e: GraphQLEventFields): (Record|null)[] { 14 | return e.parents || [null] 15 | } 16 | 17 | class GraphQLResolverEventTarget extends EventTarget { 18 | addMultiParentGraphQLResolvers(resolvers: {[key: string]: (e: GraphQLEvent) => ResolverResponse}) { 19 | for (const [name, resolver] of Object.entries(resolvers)) { 20 | this.addEventListener(name, e => { 21 | const event = e as unknown as GraphQLEvent; 22 | event.respondWith(resolver(event)) 23 | }) 24 | } 25 | } 26 | 27 | addGraphQLResolvers(resolvers: { [key: string]: (e: GraphQLEventWithParent) => (any | Promise) }) { 28 | for (const [name, resolver] of Object.entries(resolvers)) { 29 | this.addEventListener(name, e => { 30 | const event = e as unknown as GraphQLEvent; 31 | event.respondWith(getParents(event).map(parent => resolver({...event, parent}))) 32 | }) 33 | } 34 | } 35 | 36 | addWebHookResolvers(resolvers: { [key: string]: (e: WebHookGraphQLEvent) => (any | Promise) }) { 37 | for (const [name, resolver] of Object.entries(resolvers)) { 38 | this.addEventListener(name, e => { 39 | const event = e as unknown as WebHookGraphQLEvent; 40 | event.respondWith(resolver(event)) 41 | }) 42 | } 43 | } 44 | } 45 | 46 | function newContext(eventTarget: GraphQLResolverEventTarget) { 47 | return vm.createContext({ 48 | // From fetch 49 | fetch, 50 | Request, 51 | Response, 52 | Headers, 53 | 54 | // URL Standards 55 | URL, 56 | URLSearchParams, 57 | 58 | // bas64 59 | atob, 60 | btoa, 61 | 62 | // Crypto 63 | crypto, 64 | TextDecoder, 65 | TextEncoder, 66 | 67 | // Debugging 68 | console, 69 | 70 | // Async 71 | setTimeout, 72 | setInterval, 73 | clearTimeout, 74 | clearInterval, 75 | 76 | // EventTarget 77 | self: eventTarget, 78 | addEventListener: eventTarget.addEventListener.bind(eventTarget), 79 | removeEventListener: eventTarget.removeEventListener.bind(eventTarget), 80 | addMultiParentGraphQLResolvers: eventTarget.addMultiParentGraphQLResolvers.bind(eventTarget), 81 | addGraphQLResolvers: eventTarget.addGraphQLResolvers.bind(eventTarget), 82 | addWebHookResolvers: eventTarget.addWebHookResolvers.bind(eventTarget), 83 | }); 84 | } 85 | 86 | export function evaluateScript(source: string) { 87 | const script = new vm.Script(source) 88 | const target = new GraphQLResolverEventTarget(); 89 | const context = newContext(target) 90 | script.runInContext(context); 91 | 92 | return async function(e: GraphQLEventFields): Promise { 93 | let retPromise: ResolverResponse | undefined = undefined; 94 | const event = { 95 | ...e, 96 | respondWith: (x: ResolverResponse) => { retPromise = x }, 97 | graphql: (query: string, variables: Record, ah?: AuthHeaderField) => graphql(query, variables, ah || e.authHeader,e.accessJWT), 98 | dql: new dql(e.accessJWT), 99 | } 100 | if (e.type === '$webhook' && e.event) { 101 | event.type = `${e.event?.__typename}.${e.event?.operation}` 102 | } 103 | target.dispatchEvent(event) 104 | 105 | if(retPromise === undefined) { 106 | return undefined 107 | } 108 | 109 | const resolvedArray = await (retPromise as ResolverResponse); 110 | if(!Array.isArray(resolvedArray) || resolvedArray.length !== getParents(e).length) { 111 | process.env.NODE_ENV != "test" && e.type !== '$webhook' && console.error(`Value returned from ${e.type} was not an array or of incorrect length`) 112 | return undefined 113 | } 114 | 115 | const response = await Promise.all(resolvedArray); 116 | return e.parents === null ? response[0] : response; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import cluster from "cluster"; 2 | import fs from "fs"; 3 | import { scriptToExpress } from "./script-to-express"; 4 | import atob from "atob"; 5 | import btoa from "btoa"; 6 | 7 | function base64Decode(str: string) { 8 | try { 9 | const original = str.trim(); 10 | const decoded = atob(original); 11 | return btoa(decoded) === original ? decoded : ""; 12 | } catch (err) { 13 | console.error(err); 14 | return ""; 15 | } 16 | } 17 | 18 | async function startServer() { 19 | const source = ( 20 | await fs.promises.readFile(process.env.SCRIPT_PATH || "./script/script.js") 21 | ).toString(); 22 | const script = base64Decode(source) || source; 23 | 24 | const app = scriptToExpress(script); 25 | const port = process.env.PORT || "8686"; 26 | const server = app.listen(port, () => 27 | console.log("Server Listening on port " + port + "!") 28 | ); 29 | cluster.on("disconnect", () => server.close()); 30 | 31 | process.on("SIGINT", () => { 32 | server.close(); 33 | process.exit(0); 34 | }); 35 | } 36 | 37 | startServer(); 38 | -------------------------------------------------------------------------------- /src/script-to-express.test.ts: -------------------------------------------------------------------------------- 1 | import { scriptToExpress } from "./script-to-express"; 2 | import supertest from 'supertest' 3 | 4 | describe(scriptToExpress, () => { 5 | it("calls the appropriate function, passing the resolver, parent and args", async () => { 6 | const app = scriptToExpress(`addMultiParentGraphQLResolvers({ 7 | "Query.fortyTwo": ({parents, args}) => parents.map(({n}) => n + args.foo) 8 | })`) 9 | const response = await supertest(app) 10 | .post('/graphql-worker') 11 | .send({ resolver: "Query.fortyTwo", parents: [{ n: 41 }], args: {foo: 1} }) 12 | .set('Accept', 'application/json') 13 | .expect('Content-Type', /json/) 14 | .expect(200); 15 | expect(response.body).toEqual([42]); 16 | }) 17 | 18 | it("returns a single item if the parents is null", async () => { 19 | const app = scriptToExpress(`addGraphQLResolvers({ 20 | "Query.fortyTwo": () => 42 21 | })`) 22 | const response = await supertest(app) 23 | .post('/graphql-worker') 24 | .send( 25 | { resolver: "Query.fortyTwo" }, 26 | ) 27 | .set('Accept', 'application/json') 28 | .expect('Content-Type', /json/) 29 | .expect(200); 30 | expect(response.body).toEqual(42); 31 | }) 32 | 33 | it("returns a 400 if the resolver is not registered or invalid", async () => { 34 | const app = scriptToExpress(``) 35 | const response = await supertest(app) 36 | .post('/graphql-worker') 37 | .send( 38 | { resolver: "Query.notFound" }, 39 | ) 40 | .set('Accept', 'application/json') 41 | .expect('Content-Type', /json/) 42 | .expect(400); 43 | expect(response.body).toEqual(""); 44 | }) 45 | 46 | it("gets the auth header as a key", async () => { 47 | const app = scriptToExpress(`addGraphQLResolvers({ 48 | "Query.authHeader": ({authHeader}) => authHeader.key + authHeader.value 49 | })`) 50 | const response = await supertest(app) 51 | .post('/graphql-worker') 52 | .send({ resolver: "Query.authHeader", parents: [{ n: 41 }], authHeader: {key: "foo", value: "bar"} }) 53 | .set('Accept', 'application/json') 54 | .expect('Content-Type', /json/) 55 | .expect(200); 56 | expect(response.body).toEqual(["foobar"]); 57 | }) 58 | }) 59 | -------------------------------------------------------------------------------- /src/script-to-express.ts: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import { evaluateScript } from './evaluate-script' 3 | import { GraphQLEventFields } from '@slash-graphql/lambda-types' 4 | 5 | function bodyToEvent(b: any): GraphQLEventFields { 6 | return { 7 | type: b.resolver, 8 | parents: b.parents || null, 9 | args: b.args || {}, 10 | authHeader: b.authHeader, 11 | event: b.event || {}, 12 | info: b.info || null, 13 | accessJWT: b["X-Dgraph-AccessToken"] 14 | } 15 | } 16 | 17 | export function scriptToExpress(source: string) { 18 | const runner = evaluateScript(source) 19 | const app = express() 20 | app.use(express.json({limit: '32mb'})) 21 | app.post("/graphql-worker", async (req, res, next) => { 22 | try { 23 | const result = await runner(bodyToEvent(req.body)); 24 | if(result === undefined && req.body.resolver !== '$webhook') { 25 | res.status(400) 26 | } 27 | res.json(result) 28 | } catch(e) { 29 | next(e) 30 | } 31 | }) 32 | return app; 33 | } 34 | -------------------------------------------------------------------------------- /src/test-utils.ts: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch'; 2 | import sleep from 'sleep-promise'; 3 | 4 | export async function waitForDgraph() { 5 | const startTime = new Date().getTime(); 6 | while(true) { 7 | try { 8 | const response = await fetch(`${process.env.DGRAPH_URL}/probe/graphql`) 9 | if(response.status === 200) { 10 | return 11 | } 12 | } catch(e) { } 13 | await sleep(100); 14 | if(new Date().getTime() - startTime > 20000) { 15 | throw new Error("Failed while waiting for dgraph to come up") 16 | } 17 | } 18 | } 19 | 20 | export async function loadSchema(schema: string, accessJWT:string = "") { 21 | const response = await fetch(`${process.env.DGRAPH_URL}/admin/schema`, { 22 | method: "POST", 23 | headers: { "Content-Type": "application/graphql", "X-Dgraph-AccessToken":accessJWT }, 24 | body: schema 25 | }) 26 | if(response.status !== 200) { 27 | throw new Error("Could Not Load Schema") 28 | } 29 | } 30 | 31 | export async function runAdmin(body: string, accessJWT:string = ""):Promise{ 32 | return fetch(`${process.env.DGRAPH_URL}/admin`, { 33 | method: "POST", 34 | headers: { "Content-Type": "application/graphql", "X-Dgraph-AccessToken":accessJWT }, 35 | body: body 36 | }).then((response) => { 37 | if (response.status !== 200) { 38 | throw new Error("Could Not Fire GraphQL Query") 39 | } else { 40 | return response.json() 41 | } 42 | }) 43 | } 44 | export async function login(user: string, password: string, tenant: number) { 45 | const response = await fetch(`${process.env.DGRAPH_URL}/admin`, { 46 | method: "POST", 47 | headers: { "Content-Type": "application/graphql" }, 48 | body: `mutation { 49 | login(userId: "${user}", password: "${password}", namespace: ${tenant}) { 50 | response { 51 | accessJWT 52 | refreshJWT 53 | } 54 | } 55 | }` 56 | }) 57 | if(response.status !== 200) { 58 | throw new Error(`Could Not Login in tenant ${tenant} using ${user} and ${password}`) 59 | } 60 | let body = await response.json() 61 | console.log(body) 62 | let token = body["data"]["login"]["response"]["accessJWT"]; 63 | return token 64 | } 65 | 66 | export async function runQuery(query: string, accessJWT:string = "") { 67 | const response = await fetch(`${process.env.DGRAPH_URL}/graphql`, { 68 | method: "POST", 69 | headers: { "Content-Type": "application/graphql", "X-Dgraph-AccessToken":accessJWT }, 70 | body: query 71 | }) 72 | if (response.status !== 200) { 73 | throw new Error("Could Not Fire GraphQL Query") 74 | } 75 | } 76 | 77 | 78 | export async function addNamespace(password:string, accessJWT:string = ""):Promise { 79 | const body = `mutation { 80 | addNamespace(input: {password: "${password}"}) 81 | { 82 | namespaceId 83 | message 84 | } 85 | }` 86 | const response = await runAdmin(body,accessJWT) 87 | console.log(response) 88 | return response["data"]["addNamespace"] 89 | } 90 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | "target": "es2017", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 8 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 9 | // "lib": [], /* Specify library files to be included in the compilation. */ 10 | // "allowJs": true, /* Allow javascript files to be compiled. */ 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 13 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 15 | "sourceMap": true, /* Generates corresponding '.map' file. */ 16 | // "outFile": "./", /* Concatenate and emit output to single file. */ 17 | "outDir": "dist", /* Redirect output structure to the directory. */ 18 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 19 | // "composite": true, /* Enable project compilation */ 20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 21 | // "removeComments": true, /* Do not emit comments to output. */ 22 | // "noEmit": true, /* Do not emit outputs. */ 23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 26 | 27 | /* Strict Type-Checking Options */ 28 | "strict": true, /* Enable all strict type-checking options. */ 29 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 30 | // "strictNullChecks": true, /* Enable strict null checks. */ 31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 36 | 37 | /* Additional Checks */ 38 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 39 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 40 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 41 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 42 | 43 | /* Module Resolution Options */ 44 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 45 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 46 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 47 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 48 | // "typeRoots": [], /* List of folders to include type definitions from. */ 49 | // "types": [], /* Type declaration files to be included in compilation. */ 50 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 51 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 52 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 53 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 54 | 55 | /* Source Map Options */ 56 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 58 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 59 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 60 | 61 | /* Experimental Options */ 62 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 63 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 64 | 65 | /* Advanced Options */ 66 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 67 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 68 | } 69 | } 70 | --------------------------------------------------------------------------------