Skip to content

Commit

Permalink
Implement DataSource mapper
Browse files Browse the repository at this point in the history
  • Loading branch information
lorefnon committed Nov 9, 2018
0 parents commit bb60157
Show file tree
Hide file tree
Showing 13 changed files with 7,745 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .gitignore
@@ -0,0 +1,5 @@
/node_modules
/yarn-error.log
/lib
/src/docs/.next
/public
4 changes: 4 additions & 0 deletions .prettierrc.yml
@@ -0,0 +1,4 @@
tabWidth: 4
arrowParens: avoid
trailingComma: all
printWidth: 120
3 changes: 3 additions & 0 deletions .projectile
@@ -0,0 +1,3 @@
-/node_modules
-/examples/*/node_modules
-/lib
109 changes: 109 additions & 0 deletions package.json
@@ -0,0 +1,109 @@
{
"name": "greldal",
"version": "0.3.0",
"description": "A simple micro-framework to expose your relational datastore as a GraphQL API (powered by Node.js).",
"homepage": "https://lorefnon.gitlab.io/greldal",
"bugs": "https://gitlab.com/lorefnon/greldal/issues",
"main": "lib/index.js",
"umd:main": "lib/index.umd.js",
"module": "lib/index.mjs",
"source": "src/index.ts",
"repository": "https://gitlab.com/lorefnon/greldal",
"author": "lorefnon <lorefnon@gmail.com> (https://lorefnon.tech)",
"license": "MIT",
"private": false,
"files": [
"lib"
],
"scripts": {
"prebuild": "rimraf lib",
"format": "prettier --write \"src/**/*.ts\"",
"build:tsc": "tsc",
"build:docs": "yarn run build:docs:site && yarn run build:docs:api",
"build:docs:api": "typedoc --out ./public/api --ignoreCompilerErrors --exclude ./src/__specs__ ./src",
"build:docs:site": "node scripts/generate-docs.js",
"build": "yarn run build:tsc && yarn run build:docs",
"test": "jest",
"test:watch": "jest --watch",
"test:prod": "yarn run lint && yarn run test -- --no-cache",
"docs:dev-server": "next src/docs"
},
"jest": {
"transform": {
".(ts|tsx)": "ts-jest"
},
"testEnvironment": "node",
"testRegex": "(/__tests__/.*|\\.(test|spec))\\.(ts|tsx|js)$",
"moduleFileExtensions": [
"ts",
"tsx",
"js"
],
"coveragePathIgnorePatterns": [
"/node_modules/",
"/test/"
],
"coverageThreshold": {
"global": {
"branches": 90,
"functions": 95,
"lines": 95,
"statements": 95
}
},
"collectCoverage": false
},
"devDependencies": {
"@react-pdf/renderer": "^1.0.0-alpha.25",
"@types/debug": "^0.0.31",
"@types/graphql": "^14.0.3",
"@types/graphql-iso-date": "^3.3.1",
"@types/inflection": "^1.5.28",
"@types/jest": "^23.3.10",
"@types/knex": "^0.15.0",
"@types/lodash": "^4.14.118",
"@types/rimraf": "^2.0.2",
"@zeit/next-css": "^1.0.1",
"@zeit/next-mdx": "^1.2.0",
"cli-glob": "^0.1.0",
"cross-env": "^5.2.0",
"file-loader": "^2.0.0",
"graphql": "^14.0.2",
"jest": "^23.6.0",
"knex": "^0.15.2",
"mdx-table-of-contents": "^0.1.0",
"next": "^7.0.2",
"normalize.css": "^8.0.1",
"prettier": "^1.15.2",
"react": "^16.6.3",
"react-dom": "^16.6.3",
"reflect-metadata": "^0.1.12",
"remark-autolink-headings": "^5.0.0",
"remark-emoji": "^2.0.2",
"remark-highlight.js": "^5.0.0",
"remark-html": "^9.0.0",
"remark-html-emoji-image": "^1.0.0",
"remark-mermaid": "^0.2.0",
"remark-slug": "^5.1.1",
"rimraf": "^2.6.2",
"shelljs": "^0.8.3",
"sqlite3": "^4.0.3",
"ts-jest": "^23.10.5",
"typedoc": "^0.13.0",
"typescript": "^3.2.2"
},
"dependencies": {
"core-decorators": "^0.20.0",
"debug": "^4.1.0",
"graphql-iso-date": "^3.6.1",
"graphql-parse-resolve-info": "^4.0.0",
"graphql-type-uuid": "^0.2.0",
"inflection": "^1.12.0",
"io-ts": "^1.5.0",
"lodash": "^4.17.11",
"lodash-decorators": "^6.0.0"
},
"peerDependencies": {
"knex": "^0.15.2"
}
}
13 changes: 13 additions & 0 deletions src/MappedAssociation.ts
@@ -0,0 +1,13 @@
import { DataSourceMapping, MappedDataSource } from "./MappedDataSource";

export interface AssociationMapping<T extends DataSourceMapping> {
from: () => MappedDataSource<T>;
join: any;
preFetch: any;
postFetch: any;
reverseAssociateWithParents: any;
}

export class MappedAssociation<T extends AssociationMapping<any>> {
constructor(private mapping: T) {}
}
14 changes: 14 additions & 0 deletions src/MappedDataSource.spec.ts
@@ -0,0 +1,14 @@
import {dataSource} from "./MappedDataSource";

test("name mapping", () => {
const user = dataSource({
name: "User"
});
expect(user.storedName).toEqual("users");
expect(user.mappedName).toEqual("User");
const productDetails = dataSource({
name: "productDetails"
});
expect(productDetails.storedName).toEqual("product_details");
expect(productDetails.mappedName).toEqual("ProductDetail");
});
62 changes: 62 additions & 0 deletions src/MappedDataSource.ts
@@ -0,0 +1,62 @@
import { getTypeAccessorError } from "./errors";
import { Memoize } from "lodash-decorators";
import { Mapped, TypeGuard, Dict, MaybeMapped, MaybeArray, NNil } from "./util-types";
import { isString, transform, camelCase, upperFirst, snakeCase } from "lodash";
import * as t from "io-ts";
import { GraphQLInputType, GraphQLOutputType } from "graphql";
import { FieldMapping, MappedField } from "./MappedField";
import { AssociationMapping, MappedAssociation } from "./MappedAssociation";
import { singularize, pluralize } from "inflection";

export interface DataSourceMapping {
name: MaybeMapped<string>;
description?: string;
fields?: Dict<FieldMapping<any, any>>
associations?: Dict<AssociationMapping<any>>
}

type ShallowRecordType<T extends DataSourceMapping> = {
[K in keyof T["fields"]]: t.TypeOf<NNil<T["fields"]>[K]["type"]>
}

type NestedRecordType<T extends DataSourceMapping> = ShallowRecordType<T> & {
[K in keyof T["associations"]]: ReturnType<NNil<T["associations"]>[K]["from"]>["NestedRecordType"]
}

export class MappedDataSource<T extends DataSourceMapping> {
fields: {[K in keyof T["fields"]]: MappedField<NNil<T["fields"]>[K]>};
associations: {[K in keyof T["associations"]]: MappedAssociation<NNil<T["associations"]>[K]>};

constructor(private mapping: T) {
this.fields = transform(mapping.fields!, (result, fieldMapping, name) => {
result[name] = new MappedField(fieldMapping);
}, {}) as any;
this.associations = transform(mapping.associations!, (result, associationMapping, name) => {
result[name] = new MappedAssociation(associationMapping);
}, {}) as any;
}

@Memoize
get mappedName() {
return (isString as TypeGuard<string>)(this.mapping.name)
? upperFirst(camelCase(singularize(this.mapping.name)))
: this.mapping.name.mapped;
}

@Memoize
get storedName() {
return (isString as TypeGuard<string>)(this.mapping.name)
? snakeCase(pluralize(this.mapping.name))
: this.mapping.name.stored;
}

get ShallowRecordType(): ShallowRecordType<T> {
throw getTypeAccessorError('ShallowRecordType', 'MappedDataSource');
}

get NestedRecordType(): NestedRecordType<T> {
throw getTypeAccessorError('NestedRecordType', 'MappedDataSource');
}
}

export const dataSource = <T extends DataSourceMapping> (mapping: T) => new MappedDataSource<T>(mapping);
23 changes: 23 additions & 0 deletions src/MappedField.ts
@@ -0,0 +1,23 @@
import * as t from "io-ts";
import { MaybeArray } from "./util-types";
import { GraphQLInputType, GraphQLOutputType } from "graphql";
import { getTypeAccessorError } from "./errors";

export interface FieldMapping<TMapped extends t.Type<any>, TArgs extends {}> {
type: TMapped;
from?: MaybeArray<keyof TArgs>;
to?: {
input: GraphQLInputType,
output: GraphQLOutputType
};
description?: string;
derive?: (args: TArgs) => t.TypeOf<TMapped>;
}

export class MappedField<T extends FieldMapping<any, any>> {
constructor(private mapping: T) {
}
get Type(): t.TypeOf<T["type"]> {
throw getTypeAccessorError('Type', 'MappedField');
}
}
4 changes: 4 additions & 0 deletions src/errors.ts
@@ -0,0 +1,4 @@
export const getTypeAccessorError = (name: string, parent: string) => new Error(
`Property ${name} must not be accessed directly. ` +
`Use typeof ${parent}Instance.${name} to get the ${name} type for this ${parent}`
);
6 changes: 6 additions & 0 deletions src/knex-types.ts
@@ -0,0 +1,6 @@
import * as Knex from "knex";
import { Maybe } from "./util-types";

export interface QBFactory {
(alias: Maybe<string>): Knex.QueryBuilder;
}
50 changes: 50 additions & 0 deletions src/util-types.ts
@@ -0,0 +1,50 @@
import { Dictionary, Omit } from "lodash";

/** Convenience utility types */

export type Maybe<T> = null | undefined | T;

export type NNil<T> = Exclude<T, undefined | null>;

export interface Dict<T = any> extends Dictionary<T> {}

export interface Lazy<T> {
(): T;
}

export type MaybeLazy<T> = T | Lazy<T>;
export type MaybeArray<T> = T | T[];

export interface Factory<T> {
(...args: any[]): T;
}

export interface Newable<T> {
new (...args: any[]): T;
}

export type StrKey<T> = keyof T & string;
export type IdxKey<T> = keyof T & number;

export type Normalizer<PreNormalizedT, NormalizedT> = (v: PreNormalizedT) => NormalizedT;

export type MandateProps<T, P extends keyof T> = Omit<T, P> & { [K in keyof Pick<T, P>]-?: T[K] };

export type ReturnType<T extends (...args: any[]) => any> = T extends (...args: any[]) => infer R ? R : any;

export type Fn<A extends any[] = any[], R = any> = (...args: A) => R;

export type TypeGuard<S> = (v: any) => v is S;

export type Omit<T, K> = Pick<T, Exclude<keyof T, K>>;

export type ReplaceWith<TSource, TKey, TSub = never> = {
[K in keyof TSource]: K extends TKey ? TSub : TSource[K];
}

export interface Mapped<TMapped, TStored = TMapped> {
mapped: TMapped,
stored: TStored
}

export type MaybeMapped<T> = T | Mapped<T>;
27 changes: 27 additions & 0 deletions tsconfig.json
@@ -0,0 +1,27 @@
{
"compilerOptions": {
"moduleResolution": "node",
"target": "es6",
"module": "commonjs",
"lib": ["es2015", "es2016", "es2017", "dom", "esnext.asynciterable"],
"strict": true,
"sourceMap": true,
"declaration": true,
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"declarationDir": "lib",
"outDir": "lib",
"esModuleInterop": true,
"typeRoots": [
"node_modules/@types"
]
},
"include": [
"src"
],
"exclude": [
"src/__fixtures__/**/*",
"src/__snapshots__/**/*"
]
}

0 comments on commit bb60157

Please sign in to comment.