Skip to content

Commit

Permalink
Merge f81e965 into 46897b5
Browse files Browse the repository at this point in the history
  • Loading branch information
robak86 committed Jan 12, 2018
2 parents 46897b5 + f81e965 commit 56ca294
Show file tree
Hide file tree
Showing 8 changed files with 285 additions and 35 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Expand Up @@ -12,4 +12,7 @@
## 0.5.1
- add @defaultValue decorator


## 0.5.5
- add support for inheritance

49 changes: 49 additions & 0 deletions README.md
Expand Up @@ -414,3 +414,52 @@ type AudioAsset implements Asset {
length: Int
}
```

### Inheritance

```typescript
import {field, id, interfaceType, nonNull, type} from "gql-schema";
import {GraphQLInt, GraphQLString} from "graphql";

@type()
class PersistedObject {
@id() @nonNull()
id:string;

@field(GraphQLInt)
createdAt:number; //for simplification store as integer timestamp

@field(GraphQLInt)
updatedAt:number; //for simplification store as integer timestamp
}

@type()
class User extends PersistedObject {
@field(GraphQLString)
email:string
}

@type()
class Product extends PersistedObject {
@field(GraphQLString)
productName:string
}
```

Given annotated classes will produce

```graphql
type User {
id: ID!
createdAt: Int
updatedAt: Int
email: String
}

type Product {
id: ID!
createdAt: Int
updatedAt: Int
productName: String
}
```
30 changes: 1 addition & 29 deletions ROADMAP.md
@@ -1,34 +1,5 @@
# ROADMAP

### Add support for inheritance
- investigate possible pitfalls

```typescript
class PersistedObject {
@id()
id:string

@field(CustomGraphQLDateScalar)
createdAt:Date

@field(CustomGraphQLDateScalar)
updatedAt:Date
}

@type()
class User extends PersistedObject {}
```

should generate:

```graphql
type User {
id: ID
createdAt: CustomGraphQLDateScalar
updateAt: CustomGraphQLDateScalar
}
```

### Reorganize, rewrite specs

### Infer basic types from ts metadata
Expand All @@ -39,6 +10,7 @@ type User {
- add validation of field configuration
- add more descriptive logs for all errors thrown from Metadata classes during native object creation
- assert one metadata object is attached to class
- print warnings if some @decarotors are skipped (for example @defaultValue for types other than input)

### Refactoring
- use one convention for private fields
Expand Down
42 changes: 36 additions & 6 deletions lib/fields-metadata/FieldsMetadata.ts
@@ -1,21 +1,51 @@
import 'reflect-metadata';
import {metadataGet, metadataGetOrSet} from "../utils/metadataFactories";
import {metadataGet} from "../utils/metadataFactories";
import {FieldConfig} from "./FieldConfig";
import {getSuperClass} from "../utils/core";

const FIELDS_METADATA_KEY = '__FIELDS_METADATA_KEY';

export class FieldsMetadata {
static getForClass = metadataGet<FieldsMetadata>(FIELDS_METADATA_KEY);
static getOrCreateForClass = metadataGetOrSet(FIELDS_METADATA_KEY, FieldsMetadata);
static getOrCreateForClass(klass):FieldsMetadata {
let attributesMetadata = Reflect.getOwnMetadata(FIELDS_METADATA_KEY, klass);

if (!attributesMetadata) {
attributesMetadata = new FieldsMetadata();
Reflect.defineMetadata(FIELDS_METADATA_KEY, attributesMetadata, klass);

protected _fields:{ [fieldName:string]:FieldConfig } = {};

let superClass = getSuperClass(klass);
if (superClass) {
attributesMetadata.setParent(FieldsMetadata.getOrCreateForClass(superClass));
}
}

return attributesMetadata;
}


private parent:FieldsMetadata;
protected _ownFields:{ [fieldName:string]:FieldConfig } = {};

getField(fieldName:string):FieldConfig {
return this._fields[fieldName] || (this._fields[fieldName] = new FieldConfig())
return this._ownFields[fieldName] || (this._ownFields[fieldName] = new FieldConfig())
}

getFields():{ [fieldName:string]:FieldConfig } {
return this._fields;
let parentFields = this.parent ? this.parent.getFields() : {};

//TODO: consider deep merge of field configuration. In current implementation ownField configuration overrides completely field config from parent
return {
...parentFields,
...this._ownFields
}
}
}

private setParent(fieldsMetadata:FieldsMetadata) {
this.parent = fieldsMetadata;
}
}



2 changes: 2 additions & 0 deletions lib/fields-metadata/TypeValue.ts
Expand Up @@ -5,6 +5,8 @@ export class TypeValue<T> {
private _type:T;
private _typeThunk:Thunk<T>;

//TODO: validate if both _type and _typeThunk are being set

setType(type:T) {
this._type = type;
}
Expand Down
5 changes: 5 additions & 0 deletions lib/utils/core.ts
Expand Up @@ -25,6 +25,11 @@ export function invariant(condition:boolean, message:string) {
}
}

export function getSuperClass(klass) {
let superClass = Object.getPrototypeOf(klass);
return superClass.name ? superClass : null;
}

export function propagateErrorWithContext<T>(ctx:string, fn:() => T):T {
try {
return fn();
Expand Down
25 changes: 25 additions & 0 deletions spec/fields-metadata/FieldsMetadata.spec.ts
@@ -0,0 +1,25 @@
import {FieldsMetadata} from "../../lib/fields-metadata/FieldsMetadata";
import {GraphQLString} from "graphql";
import {expect} from 'chai'

describe(`FieldsMetadata`, () => {
describe('inheritance', () => {
class ParentClass {}

let fieldsMetadata = FieldsMetadata.getOrCreateForClass(ParentClass);
let parentField = fieldsMetadata.getField('parentField');
parentField.setType(GraphQLString);
parentField.setDescription('Parent Field Description');

it('inherits all parent class properties', () => {
class ChildClass extends ParentClass {}

let fieldsMetadata = FieldsMetadata.getOrCreateForClass(ChildClass);
let fields = fieldsMetadata.getFields();

expect(fields).to.have.keys(['parentField']);
});

//TODO: add more test cases
})
});
164 changes: 164 additions & 0 deletions spec/integration/inheritance.spec.ts
@@ -0,0 +1,164 @@
import {buildASTSchema, GraphQLInt, GraphQLString, parse, printType} from "graphql";
import {expect} from 'chai';
import {GraphQLSchema} from "graphql/type/schema";
import {createSchema, field, id, list, nonNull, nonNullItems, type} from "../../lib";
import {FieldsMetadata} from "../../lib/fields-metadata/FieldsMetadata";
import {ClassType} from "../../lib/utils/types";


@type()
class GenericQueryListResult<T> {
items:T;

@field(GraphQLInt) @nonNull()
totalCount:number;
}

//class factory
function ListQueryResult<T>(itemClass:ClassType<T>) {
let listResultsClass = class extends GenericQueryListResult<T> {};
let fields = FieldsMetadata.getOrCreateForClass(listResultsClass);
let itemsField = fields.getField('items');
itemsField.setListType(itemClass);
itemsField.setNonNullConstraint();
itemsField.setNonNullItemsConstraint();
return listResultsClass;
}

@type()
class Mutation {
@field(GraphQLString)
unused:string;
}


@type()
class ListResult {
@field(GraphQLInt) @nonNull()
totalCount:number;
}

@type()
class PersistedObject {
@id() @nonNull()
id:string;

@field(GraphQLInt)
createdAt:number; //for simplification store as integer timestamp instead of custom scalar

@field(GraphQLInt)
updatedAt:number; //for simplification store as integer timestamp instead of custom scalar
}

@type()
class User extends PersistedObject {
@field(GraphQLString)
email:string
}

@type()
class UsersList extends ListQueryResult(User) {}

@type()
class Product extends PersistedObject {
@field(GraphQLString)
productName:string
}

@type()
class ProductsList extends ListResult {
@list(Product) @nonNull() @ nonNullItems()
products:Product[]
}


@type()
class Query {
@field(UsersList) @nonNull()
users:UsersList;

@field(ProductsList) @nonNull()
products:ProductsList;
}


function createdSchemaFromDecoratedClasses():GraphQLSchema {
return createSchema(Query, Mutation);
}

function createSchemaFromDefinition():GraphQLSchema {
const definition = `
type User {
id: ID!
createdAt: Int
updatedAt: Int
email: String
}
type Product {
id: ID!
createdAt: Int
updatedAt: Int
productName: String
}
type UsersList {
totalCount: Int!
items: [User!]!
}
type ProductsList {
totalCount: Int!
products: [Product!]!
}
type Query {
users: UsersList!
products: ProductsList!
}
type Mutation {
unused:String
}
`;
return buildASTSchema(parse(definition));
}


function expectTypesEqual(typeName:string) {
expect(printType(createdSchemaFromDecoratedClasses().getType(typeName)))
.to.eql(printType(createSchemaFromDefinition().getType(typeName)));
}

describe("building schema", () => {

describe("type Query", () => {
it("generates proper type", () => {
expectTypesEqual('Query');
});
});

describe("type User", () => {
it("generates proper type", () => {
expectTypesEqual('User');
});
});

describe("type Product", () => {
it("generates proper type", () => {
expectTypesEqual('Product');
});
});

describe("type UsersList", () => {
it("generates proper type", () => {
expectTypesEqual('UsersList');
});
});

describe("input ProductsList", () => {
it("generates proper type", () => {
expectTypesEqual('ProductsList');
});
});
});

0 comments on commit 56ca294

Please sign in to comment.