Skip to content

Commit

Permalink
feat(core/create): application doesn't normalize collection; refactor…
Browse files Browse the repository at this point in the history
…s application
  • Loading branch information
rafamel committed Oct 16, 2019
1 parent 1015d0b commit 8dcdd4e
Show file tree
Hide file tree
Showing 10 changed files with 70 additions and 403 deletions.
121 changes: 25 additions & 96 deletions packages/core/src/create/application/index.ts
Original file line number Diff line number Diff line change
@@ -1,74 +1,40 @@
import {
CollectionTreeApplication,
CollectionTree,
TreeTypes,
GenericError,
ErrorType
} from '~/types';
import clone from 'lodash.clonedeep';
import camelcase from 'camelcase';
import { CollectionTree, ApplicationCollection } from '~/types';
import serviceIntercepts from './service-intercepts';
import { mergeServiceTypes } from './merge';
import { intercepts, intercept } from '../intercepts';
import { catchError } from 'rxjs/operators';
import { throwError } from 'rxjs';
import { PublicError, CollectionError } from '~/errors';
import { error } from '../types';
import {
traverse,
isElementType,
isElementService,
isElementTree
} from '~/inspect';

// TODO: validate collection object (ajv) + check schemas are valid
// TODO: adapters rely on resolve() existing on all services. Separate normalization from application?
// TODO: check no type has empty name

export interface ApplicationCreateOptions {
prefixScope?: boolean;
prefixInlineError?: boolean;
transform?: (str: string, explicit: boolean) => string;
}
import { isElementService, validate, isServiceImplementation } from '~/inspect';
import { replace } from '~/transform';

/**
* Returns a new object instance of a collection; prepares a collection to be used by an adapter:
* - Pipes all services intercepts with their resolve function and merges their error types.
* - Ensures all services throw with a `PublicError`.
* - Checks for non-existent type references and references of the wrong kind.
* - Moves all inline types to `CollectionTree.types`.
* Validates and prepares a collection to be used by an adapter. Returns a new collection with:
* - `ServerError` and `ClientError` error types, if non existent, for internal usage.
* - All of its services errors rethrown as a `PublicError`s, if they're not already one, for `CollectionImplementation`s.
* - Intercepts merged into their services, for `CollectionImplementation`s.
*/
export function application(
collection: CollectionTree,
options?: ApplicationCreateOptions
): CollectionTreeApplication {
const opts = Object.assign(
{
prefixScope: true,
prefixInlineError: false,
transform: (str: string) => camelcase(str, { pascalCase: true })
},
options
);

// add global errors
const errors: {
[P in GenericError]: ErrorType;
} = {
export function application<T extends CollectionTree>(
collection: T
): ApplicationCollection<T> {
// adds global errors
const errors = {
ServerError: error({ code: 'ServerError' }),
ClientError: error({ code: 'ClientError' })
};
collection = {
let application: ApplicationCollection<T> = {
...collection,
types: {
...errors,
...collection.types
}
};

const internal: GenericError = 'ServerError';
collection = intercepts(
collection,
// if not an implementation, return as is
if (!validate(application)) return application;

application = intercepts(
application,
[
intercept({
errors: Object.keys(errors).reduce(
Expand All @@ -81,7 +47,7 @@ export function application(
throwError(
err instanceof PublicError
? err
: new CollectionError(collection, internal)
: new CollectionError(application, 'ServerError')
)
)
);
Expand All @@ -91,47 +57,10 @@ export function application(
{ prepend: true }
);

const types = {
source: collection.types,
application: Object.entries(collection.types).reduce(
(acc: TreeTypes, [name, type]) => {
const pascal = opts.transform(name, true);
if (Object.hasOwnProperty.call(acc, pascal)) {
throw Error(`Type name collision: ${pascal}`);
}
acc[pascal] = type;
return acc;
},
{}
)
};

collection = clone(collection);
traverse(collection, (element, next, { path }) => {
if (isElementTree(element)) return next();

const name = opts.transform(path.slice(-1)[0], true);

if (isElementType(element)) {
if (element.kind !== 'response' || !element.children) return;
for (const [key, service] of Object.entries(element.children)) {
const fullName = name + opts.transform(key, false);
serviceIntercepts(fullName, service, types.source);
mergeServiceTypes(fullName, service, types, opts);
}
} else if (isElementService(element)) {
const fullName =
opts.prefixScope && path[path.length - 3]
? opts.transform(path[path.length - 3], false) + name
: name;

serviceIntercepts(fullName, element, types.source);
mergeServiceTypes(fullName, element, types, opts);
}
});

return {
...collection,
types: types.application
} as CollectionTreeApplication;
return replace(application, (element, next) => {
element = next(element);
return isElementService(element) && isServiceImplementation(element)
? serviceIntercepts(element, application)
: element;
}) as ApplicationCollection<T>;
}
108 changes: 0 additions & 108 deletions packages/core/src/create/application/merge.ts

This file was deleted.

48 changes: 18 additions & 30 deletions packages/core/src/create/application/service-intercepts.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,27 @@
import { Service, ServiceImplementation, TreeTypes, Type } from '~/types';
import { ServiceImplementation, Type, CollectionTree } from '~/types';
import { mergeServiceErrors } from '~/utils';
import { Observable, from } from 'rxjs';
import { allof } from '../intercepts';
import {
isServiceImplementation,
isTypeRequest,
isTypeResponse
} from '~/inspect';
import { isTypeRequest, isTypeResponse } from '~/inspect';

export default function serviceIntercepts(
name: string,
service: Service | ServiceImplementation,
types: TreeTypes
): void {
if (!isServiceImplementation(service)) return;

service: ServiceImplementation,
collection: CollectionTree
): ServiceImplementation {
const intercepts = service.intercepts;
delete service.intercepts;
if (!intercepts || !intercepts.length) return;
if (!intercepts || !intercepts.length) return service;

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

const intercept = allof(intercepts);
Expand All @@ -46,7 +34,8 @@ export default function serviceIntercepts(
case 'query':
case 'mutation': {
const resolve = service.resolve;
Object.assign(service, {
return {
...service,
types: {
...service.types,
errors: mergeServiceErrors(service.types.errors, intercept.errors)
Expand All @@ -56,12 +45,12 @@ export default function serviceIntercepts(
return from(resolve.call(this, data, context));
}).toPromise();
}
});
return;
};
}
case 'subscription': {
const resolve = service.resolve;
Object.assign(service, {
return {
...service,
types: {
...service.types,
errors: mergeServiceErrors(service.types.errors, intercept.errors)
Expand All @@ -71,8 +60,7 @@ export default function serviceIntercepts(
return resolve.call(this, data, context);
});
}
});
return;
};
}
default: {
throw Error(`Invalid kind for type`);
Expand Down
14 changes: 7 additions & 7 deletions packages/core/src/transform/routes.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { CollectionTree, Routes } from '~/types';
import { CollectionTree, CollectionRoutes } from '~/types';
import { traverse } from '~/inspect/traverse';
import {
traverse,
isElementService,
isElementTree,
isTreeCollection,
isElementType
} from '~/inspect';
} from '~/inspect/is';

export interface RoutesTransformOptions {
/**
* A non word character containing string that separates
* A non word character containing string. Default: `':'`.
*/
separator: string;
}
Expand All @@ -18,14 +18,14 @@ export interface RoutesTransformOptions {
* Given a collection, returns an object with *values* of all services, and *keys* of their full route. It will throw if a collection:
* - Contains conflicting routes.
* - Has a scope name equal to a service of its parent.
* - Contains services with an empty name or with non word characters.
* - Contains services or scopes with an empty name or with non word characters.
*/
export function routes<T extends CollectionTree>(
collection: T,
options?: RoutesTransformOptions
): Routes<T> {
): CollectionRoutes<T> {
const opts = Object.assign({ separator: ':' }, options);
const routes: Routes<any> = {};
const routes: CollectionRoutes<any> = {};

if (!/[^\w]/.exec(opts.separator)) {
throw Error(`Separator must contain a non word character`);
Expand Down

0 comments on commit 8dcdd4e

Please sign in to comment.