Skip to content

Commit

Permalink
Merge pull request #2 from skitsanos/graphql_support
Browse files Browse the repository at this point in the history
feat: GraphQL support [beta]
  • Loading branch information
skitsanos authored Apr 14, 2023
2 parents e6a3bc8 + c38abb3 commit 1f09ce0
Show file tree
Hide file tree
Showing 10 changed files with 239 additions and 73 deletions.
16 changes: 16 additions & 0 deletions .api-test/graphql.hurl
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
POST {{URL}}/examples/graphql_demo

```graphql
{
hello

getArticle(_key: "3704"){
title,
author {name}
},

getAllArticles {
title
}
}
```
2 changes: 0 additions & 2 deletions .api-test/users.hurl
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ GET {{URL}}/users

HTTP/* *

[Asserts]
status == 403

#
# Create a demo user (ignore an error that user exists, just for the sake of test)
Expand Down
1 change: 1 addition & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ jobs:
- run:
name: "Install API services"
command: |
npm i
foxx install /api . --server dev --database dev
- run:
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
/.idea/
/node_modules/
/node_modules/
*.lock
2 changes: 1 addition & 1 deletion manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "http://json.schemastore.org/foxx-manifest",
"name": "foxx-service",
"version": "2.1.20230102",
"version": "2.3.0",
"author": "Skitsanos",
"main": "src/index.js",
"scripts": {
Expand Down
12 changes: 7 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "foxx-api",
"version": "2.2.20230112",
"version": "2.3.0",
"description": "Foxx API Services",
"main": "src/index.js",
"author": {
Expand All @@ -14,10 +14,12 @@
"docker:setup-db": "docker exec -it arangodb-dev arangosh --server.username root --server.password openSesame --javascript.execute-string \"db._createDatabase('dev',{},[{username: 'dev', passwd: 'sandbox', active: true}]);\"",
"docker:backup": "docker exec -it arangodb-dev arangodump --overwrite true --server.database dev --server.username root --server.password openSesame --output-directory /backup",
"docker:restore": "docker exec -it arangodb-dev arangorestore --server.database dev --server.username root --server.password openSesame --input-directory /backup",
"register-foxx-dev-server": "foxx server set dev http://dev:sandbox@localhost:8529",
"install-foxx-dev": "foxx install /api . --server dev --database dev",
"replace-foxx-dev": "foxx upgrade /api . --server dev --database dev",
"register-foxx-dev-server": "foxx server set docker-dev http://dev:sandbox@localhost:8529",
"install-foxx-dev": "foxx install /api . --server docker-dev --database dev",
"replace-foxx-dev": "foxx upgrade /api . --server docker-dev --database dev",
"test": "docker run --network host --rm -it -v \"$(pwd)/.api-test\":/app \"orangeopensource/hurl:latest\" --test --variables-file /app/.vars /app/*.hurl"
},
"dependencies": {}
"dependencies": {
"graphql": "15.8.0"
}
}
78 changes: 61 additions & 17 deletions src/builder/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@
* @author Skitsanos, [email protected], https://github.com/skitsanos
*/
const createRouter = require('@arangodb/foxx/router');
const createGraphQLRouter = require('@arangodb/foxx/graphql');
const graphql = require('graphql');
const fs = require('fs');
const path = require('path');
const {graphqlSync} = require('graphql');

const getServicesBase = () =>
{
Expand All @@ -26,9 +29,10 @@ const getServicesBase = () =>
process.exit(1);
};

const supportedMethods = ['all', 'get', 'post', 'put', 'delete', 'patch', 'head', 'options', 'trace', 'graphql'];

const index = {
foxxServicesLocation: getServicesBase(),
supportedMethods: ['all', 'get', 'post', 'put', 'delete', 'patch', 'head'],

assignParams(type, params, endpoint)
{
Expand Down Expand Up @@ -91,47 +95,87 @@ const index = {
if (fs.isFile(fullPath))
{
const method = path.basename(fullPath, '.js');
if (this.supportedMethods.includes(method))

if (supportedMethods.includes(method))
{
const temp = fullPath.split(this.foxxServicesLocation)[1].split(`${method}.js`)[0];
const pathToHandle = temp.substring(0, temp.length - 1).replace(/\\/gi, '/');
const m = require(fullPath);
const routeHandlerModule = require(fullPath);

//parse path params
const pathParsed = pathToHandle.replace(/\$/gi, ':');

//create endpoint handler
//
// create endpoint handler for the GraphQL
//
if (method === 'graphql')
{
if (!('schema' in routeHandlerModule))
{
const err = `GraphQL schema is not defined for ${pathParsed}`;
console.error(err);
throw new Error(err);
}

module.context.use(pathParsed, createGraphQLRouter({
graphql,
formatError: (error) =>
{
return {
meta: {
platform: 'foxx-builder'
},
message: error.message,
locations: error.locations,
path: error.path
};
},
executor: ({context, document, variables}) =>
graphqlSync({
schema: routeHandlerModule.schema,
contextValue: context,
source: document,
variableValues: variables
}),
...routeHandlerModule
}));
return;
}

//
// create endpoint handler for the HTTP verbs
//
const r = createRouter();
const endpoint = r[method](pathParsed, m.handler);
const endpoint = r[method](pathParsed, routeHandlerModule.handler);

//check if params were defined
if (Object.prototype.hasOwnProperty.call(m, 'params'))
if (Object.prototype.hasOwnProperty.call(routeHandlerModule, 'params'))
{
//check for path params
if (Object.prototype.hasOwnProperty.call(m.params, 'path'))
if (Object.prototype.hasOwnProperty.call(routeHandlerModule.params, 'path'))
{
this.assignParams('path', m.params.path, endpoint);
this.assignParams('path', routeHandlerModule.params.path, endpoint);
}

//check for query params
if (Object.prototype.hasOwnProperty.call(m.params, 'query'))
if (Object.prototype.hasOwnProperty.call(routeHandlerModule.params, 'query'))
{
this.assignParams('query', m.params.query, endpoint);
this.assignParams('query', routeHandlerModule.params.query, endpoint);
}
}

//check headers
if (Object.prototype.hasOwnProperty.call(m, 'headers'))
if (Object.prototype.hasOwnProperty.call(routeHandlerModule, 'headers'))
{
this.assignParams('headers', m.headers, endpoint);
this.assignParams('headers', routeHandlerModule.headers, endpoint);
}

//check for body defs
if (Object.prototype.hasOwnProperty.call(m, 'body'))
if (Object.prototype.hasOwnProperty.call(routeHandlerModule, 'body'))
{
if (Boolean(m.body))
if (Boolean(routeHandlerModule.body))
{
const {model, mimes, description} = m.body;
const {model, mimes, description} = routeHandlerModule.body;
endpoint.body(model, mimes, description);
}
else
Expand All @@ -140,9 +184,9 @@ const index = {
}
}

if (Object.prototype.hasOwnProperty.call(m, 'error') && Array.isArray(m.error))
if (Object.prototype.hasOwnProperty.call(routeHandlerModule, 'error') && Array.isArray(routeHandlerModule.error))
{
for (const rule of m.error)
for (const rule of routeHandlerModule.error)
{
const [status, message] = Object.entries(rule)[0];
endpoint.error(Number(status), message);
Expand Down
68 changes: 34 additions & 34 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,40 +5,40 @@ builder.init();
// Example on how to use JWT token authorization.
// Only /login and /signup requests will be allowed without authentication
//
module.context.use((req, res, next) =>
{
if (req.path === '/' || req.path.match(/\/(login|signup)/igu))
{
next();
}
else
{
const {authorization} = req.headers;

if (!authorization)
{
res.throw(403, 'Missing authorization header');
}

const token = authorization && authorization.split(' ')[1];

try
{
const {auth} = module.context;

if (auth.isExpired(token))
{
res.throw(403, 'The token is expired');
}

next();
}
catch (e)
{
res.throw(400, e.message);
}
}
});
// module.context.use((req, res, next) =>
// {
// if (req.path === '/' || req.path.match(/\/(login|signup)/igu))
// {
// next();
// }
// else
// {
// const {authorization} = req.headers;
//
// if (!authorization)
// {
// res.throw(403, 'Missing authorization header');
// }
//
// const token = authorization && authorization.split(' ')[1];
//
// try
// {
// const {auth} = module.context;
//
// if (auth.isExpired(token))
// {
// res.throw(403, 'The token is expired');
// }
//
// next();
// }
// catch (e)
// {
// res.throw(400, e.message);
// }
// }
// });

/*module.context.use((req, res, next) =>
{
Expand Down
97 changes: 97 additions & 0 deletions src/routes/examples/graphql_demo/graphql.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
const {GraphQLString, GraphQLSchema, GraphQLObjectType, GraphQLList} = require('graphql');
const {query} = require('@arangodb');

const AuthorType = new GraphQLObjectType({
name: 'Author',
fields: {
_key: {type: GraphQLString},
name: {type: GraphQLString}
},
resolve: (_, author) =>
{
const result = query`
FOR author IN Authors
FILTER author._key == ${author._key}
RETURN author
`;
return result.next();
}
});

const ArticleType = new GraphQLObjectType({
name: 'Article',
fields: {
_key: {type: GraphQLString},
title: {type: GraphQLString},
authorKey: {type: GraphQLString},
author: {
type: AuthorType,
resolve: (parent) =>
{
const authorKey = parent.authorKey;
const result = query`
FOR author IN Authors
FILTER author._key == ${authorKey}
RETURN author
`;
return result.next();
}
}
},
resolve: (_, article) =>
{
const result = query`
FOR author IN Authors
FILTER author._key == ${article.authorKey}}
RETURN author
`;
return result.next();
}
});

module.exports = {
schema: new GraphQLSchema({
query: new GraphQLObjectType({
name: 'RootQueryType',
fields: {
hello: {
type: GraphQLString,
resolve()
{
return 'world';
}
},

getArticle: {
type: ArticleType,

args: {
_key: {type: GraphQLString}
},

resolve(_, {_key})
{
const result = query`FOR article IN Articles
FILTER article._key == ${_key}
RETURN article`;
return result.next();
}

},

getAllArticles: {
type: new GraphQLList(ArticleType),
resolve()
{
const result = query`
FOR article IN Articles
RETURN article
`;
return result.toArray();
}
}
}
})
}),
graphiql: true
};
Loading

0 comments on commit 1f09ce0

Please sign in to comment.