Skip to content

Commit

Permalink
feat(rest-adapter): adds adapter
Browse files Browse the repository at this point in the history
  • Loading branch information
rafamel committed Oct 24, 2019
1 parent 1e59d01 commit 9c1ffd9
Show file tree
Hide file tree
Showing 4 changed files with 205 additions and 0 deletions.
86 changes: 86 additions & 0 deletions packages/rest-adapter/src/adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import express from 'express';
import {
isServiceQuery,
isServiceSubscription,
CollectionError,
CollectionTreeImplementation,
toUnary,
GeneralError,
application,
isElementService,
filter
} from '@karmic/core';
import createDefaults from './defaults';
import mapError from './helpers/map-error';
import parseService from './parse-service';
import mergeNotFound from './helpers/merge';
import { RESTAdapterOptions, RESTAdapter } from './types';

export default function adapter(
collection: CollectionTreeImplementation,
options?: RESTAdapterOptions
): RESTAdapter {
const opts = Object.assign(createDefaults(), options);
const router = express.Router();

const { notFound, ...other } = mergeNotFound(collection, opts.notFound);
collection = other.collection;

const app = application(
opts.subscriptions
? toUnary(collection)
: filter(
collection,
(element) =>
!isElementService(element) || !isServiceSubscription(element)
)
);

const internal: GeneralError = 'ServerError';
const errors = {
server: new CollectionError(app.declaration, internal)
};

const services = app.flatten('/');
for (const [route, { declaration, resolve }] of Object.entries(services)) {
const { method, url, map } = parseService(
route.split('/'),
opts.crud,
isServiceQuery(declaration)
);

router[method](url, async (req, res) => {
try {
const context = await opts.context(req);
const data = await resolve(map(req), context);
return res.json(opts.envelope(null, data));
} catch (err) {
const { error, status } = mapError(err, errors.server);
return res.status(status).json(opts.envelope(error, null));
}
});
}

if (opts.declaration) {
router.get(
(opts.declaration[0] === '/' ? '' : '/') + opts.declaration,
(req, res) => res.json(app.declaration)
);
}

router.use(async (req, res) => {
try {
const context = await opts.context(req);
const data = await notFound(req.body, context);
return res.json(opts.envelope(null, data));
} catch (err) {
const { error, status } = mapError(err, errors.server);
return res.status(status).json(opts.envelope(error, null));
}
});

return {
router,
declaration: app.declaration
};
}
35 changes: 35 additions & 0 deletions packages/rest-adapter/src/helpers/merge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import {
CollectionTreeImplementation,
QueryServiceImplementation,
collections,
services,
normalize,
application,
UnaryApplicationResolve,
atPath,
toUnary
} from '@karmic/core';

export interface MergeNotFound {
collection: CollectionTreeImplementation;
notFound: UnaryApplicationResolve;
}

export default function mergeNotFound(
collection: CollectionTreeImplementation,
notFound: QueryServiceImplementation
): MergeNotFound {
const normal = normalize(services({ notFound }), {
skipReferences: Object.keys(collection.types)
});
collection = collections(collection, { ...normal, services: {} });

return {
collection: collection,
notFound: atPath(
application(toUnary(normal), { validate: false }).routes,
['notFound'],
(x: any): x is UnaryApplicationResolve => typeof x === 'function'
)
};
}
1 change: 1 addition & 0 deletions packages/rest-adapter/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { default } from './adapter';
export * from './types';
83 changes: 83 additions & 0 deletions packages/rest-adapter/src/parse-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { Request } from 'express';
import { CrudServices } from '@karmic/core';

export type RouterMethod = 'post' | 'get' | 'put' | 'patch' | 'delete';

export interface ParseServiceResponse {
method: RouterMethod;
url: string;
map: (req: Request) => object;
}

const parsers: {
[P in keyof Required<CrudServices>]: (
route: string[],
isQuery: boolean
) => ParseServiceResponse;
} = {
get(route, isQuery) {
if (!isQuery) throw Error(`CRUD service "get" can't be a mutation`);
return {
method: 'get',
url: '/' + route.join('/') + '/:id',
map: (req) => ({ ...req.query, id: req.params.id })
};
},
list(route, isQuery) {
if (!isQuery) throw Error(`CRUD service "list" can't be a mutation`);
return {
method: 'get',
url: '/' + route.join('/'),
map: (req) => req.query
};
},
create(route, isQuery) {
if (isQuery) throw Error(`CRUD service "create" must be a mutation`);
return {
method: 'post',
url: '/' + route.join('/'),
map: (req) => req.body
};
},
update(route, isQuery) {
if (isQuery) throw Error(`CRUD service "update" must be a mutation`);
return {
method: 'put',
url: '/' + route.join('/') + '/:id',
map: (req) => ({ ...req.body, id: req.params.id })
};
},
patch(route, isQuery) {
if (!isQuery) throw Error(`CRUD service "patch" must be a mutation`);
return {
method: 'patch',
url: '/' + route.join('/') + '/:id',
map: (req) => ({ ...req.body, id: req.params.id })
};
},
remove(route, isQuery) {
if (!isQuery) throw Error(`CRUD service "remove" must be a mutation`);
return {
method: 'delete',
url: '/' + route.join('/') + '/:id',
map: (req) => ({ ...req.body, id: req.params.id })
};
}
};

export default function parseService(
route: string[],
toCrud: boolean,
isQuery: boolean
): ParseServiceResponse {
const name = route[route.length - 1];
if (!toCrud || !Object.hasOwnProperty.call(parsers, name)) {
return {
method: isQuery ? 'get' : 'post',
url: '/' + route.join('/'),
map: isQuery ? (req) => req.query : (req) => req.body
};
}

return (parsers as any)[name](route.slice(0, -1), isQuery);
}

0 comments on commit 9c1ffd9

Please sign in to comment.