GraphQL 2.0 (#4625)

* Start on GraphQL "2.0", add methodnotallowed exceptoin

* Fix relative file pointer in peer dep

* [WIP] Add pre-filtered schema to SchemaOverview

* Use root schema as is, add reduce-schema util

* Use reduceSchema in the wild

* Base schema on local reduced schema

* Remove todo

* Use graphql-compose to build out schema

* Start restructuring resolvers

* Add create mutation

* Return boolean true for empty create mutation selections

* Add update mutation

* Add delete mutation

* Add system/items scoping

* Fix merge conflicts for real now

* Use system services, rename ids->keys

* Start on docs on mutations

* Updates to match main

* Add fetch-by-id

* Add one/many resolvers for mutations

* Check system collection rows for singleton

* Fix resolver extraction for single read

* Share delete return type

* Add comments

* Use collection root name for readable type

* Add specs endpoint for GraphQL SDL

* Update docs

* Add note on SDL spec

* Fix delete single example

* Remove package-lock

* Fix collection read scoping in non-read
This commit is contained in:
Rijk van Zanten
2021-03-30 17:06:35 -04:00
committed by GitHub
parent fb91fd57e0
commit f90c31b798
37 changed files with 1637 additions and 769 deletions

View File

@@ -8,6 +8,7 @@ import { types, Field } from '../types';
import useCollection from '../middleware/use-collection';
import { respond } from '../middleware/respond';
import { ALIAS_TYPES } from '../constants';
import { reduceSchema } from '../utils/reduce-schema';
const router = Router();
@@ -53,7 +54,13 @@ router.get(
schema: req.schema,
});
if (req.params.field in req.schema.tables[req.params.collection].columns === false) throw new ForbiddenException();
if (req.accountability?.admin !== true) {
const schema = reduceSchema(req.schema, ['read']);
if (req.params.field in schema.collections[req.params.collection].fields === false) {
throw new ForbiddenException();
}
}
const field = await service.readOne(req.params.collection, req.params.field);

View File

@@ -1,45 +1,41 @@
import { Router } from 'express';
import { graphqlHTTP } from 'express-graphql';
import { GraphQLService } from '../services';
import { respond } from '../middleware/respond';
import asyncHandler from '../utils/async-handler';
import { cloneDeep } from 'lodash';
import { parseGraphQL } from '../middleware/graphql';
const router = Router();
router.use(
'/system',
parseGraphQL,
asyncHandler(async (req, res, next) => {
const service = new GraphQLService({
accountability: req.accountability,
schema: req.schema,
scope: 'system',
});
const schema = await service.getSchema();
res.locals.payload = await service.execute(res.locals.graphqlParams);
/**
* @NOTE express-graphql will attempt to respond directly on the `res` object
* We don't want that, as that will skip our regular `respond` middleware
* and therefore skip the cache. This custom response object overwrites
* express' regular `json` function in order to trick express-graphql to
* use the next middleware instead of respond with data directly
*/
const customResponse = cloneDeep(res);
return next();
}),
respond
);
customResponse.json = customResponse.end = function (payload: Record<string, any>) {
res.locals.payload = payload;
router.use(
'/',
parseGraphQL,
asyncHandler(async (req, res, next) => {
const service = new GraphQLService({
accountability: req.accountability,
schema: req.schema,
scope: 'items',
});
if (customResponse.getHeader('content-type')) {
res.setHeader('Content-Type', customResponse.getHeader('content-type')!);
}
res.locals.payload = await service.execute(res.locals.graphqlParams);
if (customResponse.getHeader('content-length')) {
res.setHeader('content-length', customResponse.getHeader('content-length')!);
}
return next();
} as any;
graphqlHTTP({ schema, graphiql: true })(req, customResponse);
return next();
}),
respond
);

View File

@@ -3,6 +3,8 @@ import { ServerService } from '../services';
import { SpecificationService } from '../services';
import asyncHandler from '../utils/async-handler';
import { respond } from '../middleware/respond';
import { format } from 'date-fns';
import { RouteNotFoundException } from '../exceptions';
const router = Router();
@@ -13,12 +15,39 @@ router.get(
accountability: req.accountability,
schema: req.schema,
});
res.locals.payload = await service.oas.generate();
return next();
}),
respond
);
router.get(
'/specs/graphql/:scope?',
asyncHandler(async (req, res) => {
const service = new SpecificationService({
accountability: req.accountability,
schema: req.schema,
});
const serverService = new ServerService({
accountability: req.accountability,
schema: req.schema,
});
const scope = req.params.scope || 'items';
if (['items', 'system'].includes(scope) === false) throw new RouteNotFoundException(req.path);
const info = await serverService.serverInfo();
const result = await service.graphql.generate(scope as 'items' | 'system');
const filename = info.project.project_name + '_' + format(new Date(), 'yyyy-MM-dd') + '.graphql';
res.attachment(filename);
res.send(result);
})
);
router.get('/ping', (req, res) => res.send('pong'));
router.get(