Skip to content

Commit

Permalink
feat(core): moves types to service object root; inline types must be …
Browse files Browse the repository at this point in the history
…schemas
  • Loading branch information
rafamel committed Nov 10, 2019
1 parent 5c562f8 commit 89b1c4f
Show file tree
Hide file tree
Showing 18 changed files with 126 additions and 264 deletions.
6 changes: 3 additions & 3 deletions packages/core/src/application/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,19 @@ import {
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, defaultMap } from './defaults';
import { mergeFallback } from './helpers/merge-fallback';
import { removeChildren } from './helpers/remove-children';

// TODO: routes from camel/pascal case to _ or - separated + implement for REST
/**
* 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,
Expand All @@ -31,7 +31,7 @@ export function application<T extends CollectionTreeImplementation>(

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

Expand Down
6 changes: 2 additions & 4 deletions packages/core/src/application/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,8 @@ export function createDefaults(): Required<ApplicationCreateOptions> {
validate: true,
children: true,
fallback: query({
types: {
errors: {
NotFoundError: error({ label: 'ClientNotFound' })
}
errors: {
NotFoundError: error({ label: 'ClientNotFound' })
},
async resolve() {
throw new PublicError(
Expand Down
36 changes: 0 additions & 36 deletions packages/core/src/application/helpers/handle-children.ts

This file was deleted.

41 changes: 18 additions & 23 deletions packages/core/src/application/helpers/merge-intercepts.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import {
ServiceImplementation,
Type,
CollectionTree,
CollectionTreeImplementation
} from '~/types';
Expand Down Expand Up @@ -35,34 +34,33 @@ export function serviceIntercepts(
delete service.intercepts;
if (!intercepts || !intercepts.length) return service;

const request: Type =
typeof service.types.request === 'string'
? collection.types[service.types.request]
: service.types.request;
const response: Type =
typeof service.types.response === 'string'
? collection.types[service.types.response]
: service.types.response;
if (!isTypeRequest(request) || !isTypeResponse(response)) {
throw Error(`Invalid type kind for service`);
let request = service.request;
let response = service.response;
if (typeof request === 'string') {
const type = collection.types[request];
if (!isTypeRequest(type)) {
throw Error(`Invalid type kind for service request`);
}
request = type.schema;
}
if (typeof response === 'string') {
const type = collection.types[response];
if (!isTypeResponse(type)) {
throw Error(`Invalid type kind for service response`);
}
response = type.schema;
}

const intercept = allof(intercepts);
const interceptFn = intercept.factory({
request: request.schema,
response: response.schema
});
const interceptFn = intercept.factory({ request, response });

switch (service.kind) {
case 'query':
case 'mutation': {
const resolve = service.resolve;
return {
...service,
types: {
...service.types,
errors: mergeServiceErrors(service.types.errors, intercept.errors)
},
errors: mergeServiceErrors(service.errors, intercept.errors),
resolve(data: any, context, info): Promise<any> {
return subscribe(
interceptFn(
Expand All @@ -81,10 +79,7 @@ export function serviceIntercepts(
const resolve = service.resolve;
return {
...service,
types: {
...service.types,
errors: mergeServiceErrors(service.types.errors, intercept.errors)
},
errors: mergeServiceErrors(service.errors, intercept.errors),
resolve(data: any, context, info): Observable<any> {
return interceptFn(
data,
Expand Down
16 changes: 16 additions & 0 deletions packages/core/src/application/helpers/remove-children.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { replace } from '~/transform';
import { isTypeResponse, isElementType } from '~/inspect';
import { CollectionTreeImplementation } from '~/types';

export function removeChildren(
collection: CollectionTreeImplementation
): CollectionTreeImplementation {
return replace(collection, (element, info, next) => {
element = next(element);
if (isElementType(element) && isTypeResponse(element) && element.children) {
const { children, ...other } = element;
return other;
}
return element;
}) as CollectionTreeImplementation;
}
43 changes: 17 additions & 26 deletions packages/core/src/create/implement/services/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,15 @@ import {
SubscriptionServiceImplementation,
TreeServicesImplementation,
CollectionTreeImplementation,
ServiceTypesImplementation
ServiceImplementation
} from '~/types';
import { Observable, from } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import {
UnaryServiceImplementationInput,
StreamServiceImplementationInput,
ServiceInputTypes
QueryServiceImplementationInput,
MutationServiceImplementationInput,
SubscriptionServiceImplementationInput
} from './types';
import { isElement } from '~/inspect';
import { request, response } from '../types';

/**
* Returns a new `collection` with services `services`.
Expand All @@ -36,13 +34,12 @@ export function services<T extends TreeServicesImplementation>(
* Creates a `QueryServiceImplementation`.
*/
export function query<I = any, O = any, C = any>(
query: UnaryServiceImplementationInput<I, O, C>
query: QueryServiceImplementationInput<I, O, C>
): QueryServiceImplementation<I, O, C> {
return {
...getDefaults(),
...query,
kind: 'query',
types: parseTypes(query.types),
intercepts: query.intercepts || [],
async resolve(...args: any) {
return query.resolve.apply(this, args);
}
Expand All @@ -53,15 +50,15 @@ export function query<I = any, O = any, C = any>(
* Creates a `MutationServiceImplementation`.
*/
export function mutation<I = any, O = any, C = any>(
mutation: UnaryServiceImplementationInput<I, O, C>
mutation: MutationServiceImplementationInput<I, O, C>
): MutationServiceImplementation<I, O, C> {
const resolve = mutation.resolve || ((() => null) as any);
return {
...getDefaults(),
...mutation,
kind: 'mutation',
types: parseTypes(mutation.types),
intercepts: mutation.intercepts || [],
async resolve(...args: any) {
return mutation.resolve.apply(this, args);
return resolve.apply(this, args);
}
};
}
Expand All @@ -70,13 +67,12 @@ export function mutation<I = any, O = any, C = any>(
* Creates a `SubscriptionServiceImplementation`.
*/
export function subscription<I = any, O = any, C = any>(
subscription: StreamServiceImplementationInput<I, O, C>
subscription: SubscriptionServiceImplementationInput<I, O, C>
): SubscriptionServiceImplementation<I, O, C> {
return {
...getDefaults(),
...subscription,
kind: 'subscription',
types: parseTypes(subscription.types),
intercepts: subscription.intercepts || [],
resolve(...args: any) {
const get = async (): Promise<Observable<any>> => {
return subscription.resolve.apply(this, args);
Expand All @@ -86,16 +82,11 @@ export function subscription<I = any, O = any, C = any>(
};
}

function parseTypes(types: ServiceInputTypes = {}): ServiceTypesImplementation {
function getDefaults(): Omit<ServiceImplementation, 'kind' | 'resolve'> {
return {
errors: types.errors || {},
request:
isElement(types.request) || typeof types.request === 'string'
? types.request
: request({ schema: types.request || { type: 'object' } }),
response:
isElement(types.response) || typeof types.response === 'string'
? types.response
: response({ schema: types.response || { type: 'null' } })
errors: {},
request: { type: 'object' },
response: { type: 'null' },
intercepts: []
};
}
34 changes: 16 additions & 18 deletions packages/core/src/create/implement/services/types.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,29 @@
import {
InterceptImplementation,
Schema,
ResponseTypeImplementation,
ServiceErrors,
RequestType,
ElementInfo
ElementInfo,
QueryServiceImplementation,
MutationServiceImplementation,
SubscriptionServiceImplementation
} from '~/types';
import { Observable } from 'rxjs';

export interface UnaryServiceImplementationInput<I = any, O = any, C = any> {
types?: ServiceInputTypes;
export interface QueryServiceImplementationInput<I = any, O = any, C = any>
extends Partial<Omit<QueryServiceImplementation, 'kind' | 'resolve'>> {
resolve: (data: I, context: C, info: ElementInfo) => Promise<O> | O;
intercepts?: Array<InterceptImplementation<I, O, C>>;
}

export interface StreamServiceImplementationInput<I = any, O = any, C = any> {
types?: ServiceInputTypes;
export interface MutationServiceImplementationInput<I = any, O = any, C = any>
extends Partial<Omit<MutationServiceImplementation, 'kind' | 'resolve'>> {
resolve: (data: I, context: C, info: ElementInfo) => Promise<O> | O;
}

export interface SubscriptionServiceImplementationInput<
I = any,
O = any,
C = any
> extends Partial<Omit<SubscriptionServiceImplementation, 'kind' | 'resolve'>> {
resolve: (
data: I,
context: C,
info: ElementInfo
) => Observable<O> | Promise<Observable<O>>;
intercepts?: Array<InterceptImplementation<I, O, C>>;
}

export interface ServiceInputTypes {
errors?: ServiceErrors;
request?: string | Schema | RequestType;
response?: string | Schema | ResponseTypeImplementation;
}
8 changes: 4 additions & 4 deletions packages/core/src/generate/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,22 +116,22 @@ export async function client(
children: true,
map(service): any {
const requestType = normal.types[
service.types.request as string
service.request as string
] as RequestType;
const emptyObjectValid = ajv.compile(requestType.schema)({});

let resolve = '';
resolve += start + `function resolve(data`;
resolve += opts.typescript ? `: ${service.types.request}` : ``;
resolve += opts.typescript ? `: ${service.request}` : ``;
resolve += emptyObjectValid ? ` = {}` : ``;

resolve += `)`;
if (opts.typescript) {
const isSubscription = isServiceSubscription(service);
if (isSubscription) importObservable = true;
resolve += isSubscription
? `: Observable<${service.types.response}>`
: `: Promise<${service.types.response}>`;
? `: Observable<${service.response}>`
: `: Promise<${service.response}>`;
}
resolve += ` { `;
resolve += service.resolve;
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/inspect/at.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,9 @@ export function atRoute<
);
}
if (isElementService(element)) {
if (Object.hasOwnProperty.call(element.types, next)) {
if (Object.hasOwnProperty.call(element, next)) {
return trunk(
(element.types as any)[next],
(element as any)[next],
after.slice(1),
before.concat(next)
);
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/inspect/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { isTreeImplementation } from './is';
// TODO: validate collection object (ajv) + check schemas are valid
// TODO: children services must have a request schema equal or as a subset of the type they belong to
// TODO: all request types must be of type object
// TODO: verify lowercase/uppercase don't collide
export interface ValidateInspectOptions {
/**
* If specified, it will also throw if the collection is not a full implementation or solely a declaration, in each case. Default: `null`.
Expand Down

0 comments on commit 89b1c4f

Please sign in to comment.