Skip to content

Commit

Permalink
feat(core): adds default service to Application
Browse files Browse the repository at this point in the history
  • Loading branch information
rafamel committed Oct 26, 2019
1 parent 75c8098 commit 5ec0d59
Show file tree
Hide file tree
Showing 15 changed files with 204 additions and 162 deletions.
70 changes: 70 additions & 0 deletions packages/core/src/application/application.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import {
CollectionTreeImplementation,
ApplicationResolve,
Application,
ApplicationServices
} from '~/types';
import { validate, traverse, isElementService, atPath } from '~/inspect';
import { addInterceptResponse } from './helpers/intercept-response';
import { mergeIntercepts } from './helpers/merge-intercepts';
import { handleChildren } from './helpers/handle-children';
import { getRoutes } from './helpers/get-routes';
import { toDeclaration } from '~/transform';
import { ApplicationCreateOptions } from './types';
import { createDefaults } from './defaults';
import { mergeDefault } from './helpers/merge-default';

/**
* Validates and prepares a collection to be used:
* - Merges service intercepts into each route resolver.
* - Ensures services fail with a `PublicError` and resolve with `null` for empty responses.
* - Ensures `ServerError` and `ClientError` error types exist on the collection declaration.
* - Names and lifts inline types to the collection root if they have children services.
*/
export function application<T extends CollectionTreeImplementation>(
collection: T,
options?: ApplicationCreateOptions
): Application {
const opts = Object.assign(createDefaults(), options);

const merge = mergeDefault(collection, opts.default, opts.map);

let tree: CollectionTreeImplementation = merge.collection;
if (opts.validate) validate(tree, { as: 'implementation' });
tree = handleChildren(tree, opts.children ? 'lift' : 'remove');
tree = addInterceptResponse(tree);
tree = mergeIntercepts(tree);

const declaration = toDeclaration(tree);
const routes = getRoutes(tree, opts.map);

return {
declaration,
default: merge.default,
routes,
flatten(delimiter: string): ApplicationServices {
if (!/[^\w]/.exec(delimiter)) {
throw Error(
`Delimiter must include at least a non wod character: ${delimiter}`
);
}

const services: ApplicationServices = {};
traverse(declaration, (element, info, next) => {
next();
if (isElementService(element)) {
services[info.route.join(delimiter)] = {
declaration: element,
resolve: atPath(
routes,
info.route,
(x: any): x is ApplicationResolve => typeof x === 'function'
)
};
}
});

return services;
}
};
}
37 changes: 37 additions & 0 deletions packages/core/src/application/defaults.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { ApplicationCreateOptions } from './types';
import { query, error } from '~/create';
import { PublicError } from '~/errors';
import {
ServiceImplementation,
ElementInfo,
ApplicationResolve
} from '~/types';
import { Observable } from 'rxjs';

export function createDefaults(): Required<ApplicationCreateOptions> {
return {
validate: true,
children: true,
default: query({
types: {
errors: {
NotFoundError: error({ label: 'ClientNotFound' })
}
},
async resolve() {
throw new PublicError(
'NotFoundError',
'ClientNotFound',
null,
null,
true
);
}
}),
map(service: ServiceImplementation, info: ElementInfo): ApplicationResolve {
return (data: any, context: any): Promise<any> | Observable<any> => {
return service.resolve(data, context, info);
};
}
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
isTypeResponse,
isElementTree
} from '~/inspect';
import { ApplicationCreateMapFn } from './index';
import { ApplicationCreateMapFn } from '../types';

export function getRoutes(
collection: CollectionTreeImplementation,
Expand Down
41 changes: 41 additions & 0 deletions packages/core/src/application/helpers/merge-default.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import {
CollectionTreeImplementation,
UnaryApplicationResolve,
QueryServiceImplementation
} from '~/types';
import { services, collections } from '~/create';
import { normalize } from '~/transform';
import { ApplicationCreateMapFn } from '../types';
import { getRoutes } from './get-routes';
import { atPath } from '~/inspect';

export interface MergeDefault {
collection: CollectionTreeImplementation;
default: UnaryApplicationResolve;
}

export function mergeDefault(
collection: CollectionTreeImplementation,
service: QueryServiceImplementation,
map: ApplicationCreateMapFn
): MergeDefault {
if (service.kind !== 'query') {
throw Error(`Default service must be a query service`);
}

const normal = normalize(services({ default: service }), {
skipReferences: Object.keys(collection.types)
});

return {
collection: collections(collection, {
...normal,
services: {}
}),
default: atPath(
getRoutes(normal, map),
['default'],
(x: any): x is UnaryApplicationResolve => typeof x === 'function'
)
};
}
99 changes: 2 additions & 97 deletions packages/core/src/application/index.ts
Original file line number Diff line number Diff line change
@@ -1,97 +1,2 @@
import {
CollectionTreeImplementation,
ApplicationResolve,
ServiceImplementation,
ElementInfo,
Application,
ApplicationServices
} from '~/types';
import { validate, traverse, isElementService, atPath } from '~/inspect';
import { addInterceptResponse } from './intercept-response';
import { mergeIntercepts } from './merge-intercepts';
import { handleChildren } from './handle-children';
import { getRoutes } from './get-routes';
import { toDeclaration } from '~/transform';

export interface ApplicationCreateOptions {
/**
* Whether the collection should be validated - see `validate`. Default: `true`.
*/
validate?: boolean;
/**
* Whether to include response types with children as routes. Default: `true`.
*/
children?: boolean;
/**
* Maps a service to its route resolver.
*/
map?: ApplicationCreateMapFn;
}

export type ApplicationCreateMapFn<I = any, O = any, C = any> = (
service: ServiceImplementation<I, O, C>,
info: ElementInfo
) => ApplicationResolve<I, O, C>;

/**
* Validates and prepares a collection to be used:
* - Merges service intercepts into each route resolver.
* - Ensures services fail with a `PublicError` and resolve with `null` for empty responses.
* - Ensures `ServerError` and `ClientError` error types exist on the collection declaration.
* - Names and lifts inline types to the collection root if they have children services.
*/
export function application<T extends CollectionTreeImplementation>(
collection: T,
options?: ApplicationCreateOptions
): Application {
const opts = Object.assign(
{
validate: true,
children: true,
map(service: ServiceImplementation, info: ElementInfo) {
return (data: any, context: any) => {
return service.resolve(data, context, info);
};
}
},
options
);

let tree: CollectionTreeImplementation = collection;
if (opts.validate) validate(tree, { as: 'implementation' });
tree = handleChildren(tree, opts.children ? 'lift' : 'remove');
tree = addInterceptResponse(tree);
tree = mergeIntercepts(tree);

const declaration = toDeclaration(tree);
const routes = getRoutes(tree, opts.map);

return {
declaration,
routes,
flatten(delimiter: string): ApplicationServices {
if (!/[^\w]/.exec(delimiter)) {
throw Error(
`Delimiter must include at least a non wod character: ${delimiter}`
);
}

const services: ApplicationServices = {};
traverse(declaration, (element, info, next) => {
next();
if (isElementService(element)) {
services[info.route.join(delimiter)] = {
declaration: element,
resolve: atPath(
routes,
info.route,
(x: any): x is ApplicationResolve => typeof x === 'function'
)
};
}
});

return services;
}
};
}
export * from './application';
export * from './types';
31 changes: 31 additions & 0 deletions packages/core/src/application/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {
ServiceImplementation,
ElementInfo,
ApplicationResolve,
QueryServiceImplementation
} from '~/types';

export interface ApplicationCreateOptions {
/**
* Whether the collection should be validated - see `validate`. Default: `true`.
*/
validate?: boolean;
/**
* Whether to include response types with children as routes. Default: `true`.
*/
children?: boolean;
/**
* Maps a service to its route resolver.
*/
map?: ApplicationCreateMapFn;
/**
* A default service for adapters to use when the route is non existent.
* Defaults to a `ClientNotFound` error throwing service.
*/
default?: QueryServiceImplementation;
}

export type ApplicationCreateMapFn<I = any, O = any, C = any> = (
service: ServiceImplementation<I, O, C>,
info: ElementInfo
) => ApplicationResolve<I, O, C>;
4 changes: 2 additions & 2 deletions packages/core/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ export class PublicError extends Error {
id: string,
label: ErrorLabel,
source?: Error | null,
message?: string,
message?: string | null,
clear?: boolean
) {
super(message);
super(message || '');
this.id = id;
this.label = label;
this.source = source || undefined;
Expand Down
14 changes: 9 additions & 5 deletions packages/core/src/types/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Observable } from 'rxjs';

export interface Application {
declaration: CollectionTreeDeclaration;
default: UnaryApplicationResolve;
routes: ApplicationRoutes;
flatten(delimiter: string): ApplicationServices;
}
Expand All @@ -15,14 +16,17 @@ export interface ApplicationServices {
[key: string]: ApplicationService;
}

export interface ApplicationService {
export interface ApplicationService<
R extends ApplicationResolve = ApplicationResolve
> {
declaration: ServiceDeclaration;
resolve: ApplicationResolve;
resolve: R;
}

export type ApplicationResolve<I = any, O = any, C = any> =
| UnaryApplicationResolve<I, O, C>
| StreamApplicationResolve<I, O, C>;
export type ApplicationResolve<I = any, O = any, C = any> = (
data: I,
context: C
) => Promise<O> | Observable<O>;

export type UnaryApplicationResolve<I = any, O = any, C = any> = (
data: I,
Expand Down
11 changes: 4 additions & 7 deletions packages/rest-adapter/src/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,9 @@ import {
isElementService,
filter
} from '@karmic/core';
import createDefaults from './defaults';
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(
Expand All @@ -23,17 +22,15 @@ export default function adapter(
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)
)
),
opts.default ? { default: opts.default } : {}
);

const internal: GeneralError = 'ServerError';
Expand Down Expand Up @@ -71,7 +68,7 @@ export default function adapter(
router.use(async (req, res) => {
try {
const context = await opts.context(req);
const data = await notFound(req.body, context);
const data = await app.default(req.body, context);
return res.json(opts.envelope(null, data));
} catch (err) {
const { error, status } = mapError(err, errors.server);
Expand Down

0 comments on commit 5ec0d59

Please sign in to comment.