Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Resolves #5 #6

Merged
merged 4 commits into from
Oct 15, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ version: 2.1
executors:
build-test-and-publish:
docker:
- image: circleci/node:10.14.1
- image: circleci/node:10.16.3
environment:
TEST_DB_HOST: localhost
TEST_DB_TYPE: postgres
Expand Down
112 changes: 94 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,20 @@

Create a [GraphQL.js](https://github.com/graphql/graphql-js) schema from your [TypeORM](https://github.com/typeorm/typeorm) Entities with integrated [dataloader](https://github.com/graphql/dataloader) support.

## Usage

Here's example [Apollo Server](https://github.com/apollographql/apollo-server) with a `posts` resolver to paginate over the `Post` entity.

```typescript
import 'reflect-metadata'

import * as path from 'path'
import dotenv from 'dotenv'
import { ApolloServer } from 'apollo-server'
import { Column, ManyToOne, OneToMany, PrimaryGeneratedColumn, createConnection } from 'typeorm'
import { NexusEntity, nexusTypeORMPlugin, entityType } from 'nexus-typeorm-plugin'
import { queryType, makeSchema } from 'nexus'

dotenv.config()

// First we define our entities
@NexusEntity()
export class User {
@PrimaryGeneratedColumn()
Expand Down Expand Up @@ -52,20 +54,19 @@ class Post {
}
}

const { DB_HOST, DB_TYPE, DB_NAME, DB_USERNAME, DB_PASSWORD, DB_PORT } = process.env

async function main() {
await createConnection({
entities: [User, Post],
host: DB_HOST,
type: DB_TYPE as 'mysql',
database: DB_NAME,
username: DB_USERNAME,
password: DB_PASSWORD,
port: DB_PORT ? parseInt(DB_PORT as any, 10) : undefined,
host: 'localhost',
type: 'mysql',
database: 'nexus-typeorm',
username: 'root',
password: '',
port: 3306,
synchronize: true,
})

// Define the Query type for our schema
const query = queryType({
definition: t => {
t.paginationField('posts', {
Expand All @@ -75,6 +76,7 @@ async function main() {
})

const schema = makeSchema({
// It's important to notice that even though we didn't create an resolver for User in Query. We have to define it in our schema since it's related to Post entity
types: [nexusTypeORMPlugin(), query, entityType(User), entityType(Post)],
outputs: {
schema: path.resolve('schema.graphql'),
Expand All @@ -87,15 +89,93 @@ async function main() {
}

main().catch(error => {
// eslint-disable-next-line no-console
console.error(error)
process.exit(1)
})
```

## To run tests
## Features

### entityType

Helps you create an `objectType` for an entity faster and simpler.

```typescript
export const User = entityType<User>(User, {
definition: t => {
t.entityField('id')
t.entityField('name')
t.paginationField('followers', {
type: 'User',
entity: 'UserFollows',
resolve: async (source: User, args, ctx, info, next) => {
const follows = await next(source, args, ctx, info)

return getConnection()
.getRepository(User)
.createQueryBuilder()
.where('id IN (:...ids)', {
ids: follows.map((follow: UserFollows) => follow.followerId),
})
.getMany()
},
})
},
})
```

### paginationField

Creates a field that resolves into a list of instances of the choosen entity. It includes the `first`, `last`, `after`, `before`, `skip`, `where`, and the `orderBy` arguments.

```typescript
export const Query = queryType({
definition(t) {
t.paginationField('posts', {
entity: 'Post',
})
},
})
```

### uniqueField

Create `.env` file at the project root and fill it with database information.
Creates a field that resolves into one entity instance. It includes the `where` and the `orderBy` arguments.

```typescript
export const Query = queryType({
definition(t) {
t.uniqueField('user', {
entity: 'User',
})
},
})
```

### Auto join

In order to speed up requests and decrease the number of queries made to the database, this plugin analyzes each graphql query and makes the necessary joins automatically.

```gql
{
user {
id
posts {
id
}
}
}
```

Generates a SQL query that left joins Post.

```SQL
SELECT * from User ... LEFT JOIN POST
```

## Contributing

To run tests create `.env` file at the project root and fill it with database information.

```bash
TEST_DB_HOST=localhost
Expand All @@ -117,7 +197,3 @@ Now you can run tests.
```bash
yarn test
```

## Notes

Implementation is now at experimental stage. It's currently tested on the simplest cases.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "nexus-typeorm-plugin",
"description": "Create schemas from TypeORM models easily with this nexus plugin. Comes with dataloader!",
"version": "0.0.1-beta.4",
"version": "0.0.1-beta.5",
"main": "./dist/index.js",
"private": false,
"typings": "./dist/index.d.ts",
Expand Down
8 changes: 4 additions & 4 deletions src/args/arg-where.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ const singleOperandOperations = ['contains']
const numberOperandOperations = ['lt', 'lte', 'gt', 'gte']
const multipleOperandOperations = ['in']

export type ArgWhere = {
AND: ArgWhere[]
OR: ArgWhere[]
NOT: ArgWhere[]
export type ArgWhereType = {
AND: ArgWhereType[]
OR: ArgWhereType[]
NOT: ArgWhereType[]
} & {
[key: string]: string | Array<string | boolean | number>
}
Expand Down
63 changes: 44 additions & 19 deletions src/dataloader/entity-dataloader.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import * as DataLoader from 'dataloader'
import { getConnection } from 'typeorm'
import { ArgWhere, translateWhereClause } from '../args/arg-where'
import { ArgWhereType, translateWhereClause } from '../args/arg-where'
import { ArgOrder, orderNamesToOrderInfos } from '../args/arg-order-by'
import { createQueryBuilder } from '../query-builder'
import { createQueryBuilder, EntityJoin } from '../query-builder'
import { getEntityTypeName, getEntityPrimaryColumn } from '../util'
import { SchemaBuilder } from '../schema-builder'

export const generateCacheKeyFromORMDataLoaderRequest = (req: QueryDataLoaderRequest<any>) => {
let key = `${JSON.stringify(req.where)}${
Expand All @@ -19,19 +20,21 @@ export const generateCacheKeyFromORMDataLoaderRequest = (req: QueryDataLoaderReq
interface QueryListDataLoaderRequest<Model> {
entity: Model
type: 'list'
where?: ArgWhere
where?: ArgWhereType
orderBy?: ArgOrder
first?: number
last?: number
join?: string[]
schemaBuilder: SchemaBuilder
join?: EntityJoin[]
}

interface QueryOneDataLoaderRequest<Model> {
entity: Model
type: 'one'
where?: ArgWhere
where?: ArgWhereType
orderBy?: ArgOrder
join?: string[]
schemaBuilder: SchemaBuilder
join?: EntityJoin[]
}

export type QueryDataLoaderRequest<Model> =
Expand All @@ -41,11 +44,11 @@ export type QueryDataLoaderRequest<Model> =
export type QueryDataLoader = DataLoader<QueryDataLoaderRequest<any>, any>
export function createQueryDataLoader(entitiesDataLoader?: EntityDataLoader<any>): QueryDataLoader {
return new DataLoader<QueryDataLoaderRequest<any>, any>(
requests =>
Promise.all(
requests => {
return Promise.all(
requests.map(async req => {
if (req.type === 'one') {
const queryBuilder = createQueryBuilder<any>({
const queryBuilder = createQueryBuilder<any>(req.schemaBuilder, {
entity: req.entity,
where: req.where && translateWhereClause(getEntityTypeName(req.entity), req.where),
orders: req.orderBy && orderNamesToOrderInfos(req.orderBy),
Expand All @@ -66,7 +69,7 @@ export function createQueryDataLoader(entitiesDataLoader?: EntityDataLoader<any>
return node
}

const queryBuilder = createQueryBuilder<any>({
const queryBuilder = createQueryBuilder<any>(req.schemaBuilder, {
entity: req.entity,
where: req.where && translateWhereClause(getEntityTypeName(req.entity), req.where),
orders: req.orderBy && orderNamesToOrderInfos(req.orderBy),
Expand All @@ -77,7 +80,8 @@ export function createQueryDataLoader(entitiesDataLoader?: EntityDataLoader<any>

return queryBuilder.getMany()
}),
),
)
},
{
cacheKeyFn: generateCacheKeyFromORMDataLoaderRequest,
},
Expand All @@ -95,14 +99,35 @@ export const generateEntityDataLoaderCacheKey = (req: EntityDataLoaderRequest<an
export type EntityDataLoader<Model> = DataLoader<EntityDataLoaderRequest<Model>, Model>
export const createEntityDataLoader = (): EntityDataLoader<any> => {
return new DataLoader(
(requests: EntityDataLoaderRequest<any>[]) =>
Promise.all(
requests.map(req =>
getConnection()
.getRepository(req.entity)
.findOne(req.value),
),
),
async (requests: EntityDataLoaderRequest<any>[]) => {
const entityMap: { [entityName: string]: any } = {}
const pksByEntityName: { [entityName: string]: string[] } = {}
requests.forEach(req => {
entityMap[req.entity.name] = req.entity

if (!pksByEntityName[req.entity.name]) {
pksByEntityName[req.entity.name] = []
}

pksByEntityName[req.entity.name].push(req.value)
})

const resultMap: { [key: string]: any } = {}
await Promise.all(
Object.keys(pksByEntityName).map(async entityName => {
const entity = entityMap[entityName]
const primaryColumn = getEntityPrimaryColumn(entity)
const nodes = await getConnection()
.getRepository<any>(entity)
.findByIds(pksByEntityName[entityName])
nodes.forEach(node => {
resultMap[`${entityName}:${node[primaryColumn.propertyName]}`] = node
})
}),
)

return requests.map(req => resultMap[`${req.entity.name}:${req.value}`])
},
{
cacheKeyFn: generateEntityDataLoaderCacheKey,
},
Expand Down
21 changes: 5 additions & 16 deletions src/nexus/entity-field-output-method.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { findEntityByTypeName, getEntityPrimaryColumn, getEntityTypeName } from
import { getConnection } from 'typeorm'
import { columnToGraphQLTypeDef } from '../type'
import { ORMResolverContext } from '../dataloader/entity-dataloader'
import { ArgWhere } from '../args/arg-where'
import { ArgWhereType } from '../args/arg-where'
import { PaginationFieldResolveFn } from './pagination-output-method'

declare global {
Expand Down Expand Up @@ -64,12 +64,6 @@ export function createEntityFieldOutputMethod(schemaBuilder: SchemaBuilder) {
const inverseForeignKeyName = relation.inverseRelation.foreignKeys[0].columnNames[0]
const resolve: PaginationFieldResolveFn = (source, args, ctx, info, next) => {
if (!args.where && source[relation.propertyName]) {
if (args.join && !(ctx && ctx.ignoreErrors)) {
throw new Error(
'Join argument is ignored here because a this field was already joined',
)
}

return source[relation.propertyName]
}

Expand Down Expand Up @@ -99,18 +93,12 @@ export function createEntityFieldOutputMethod(schemaBuilder: SchemaBuilder) {

const entityPrimaryKey = getEntityPrimaryColumn(entity)
const isRelationOwner = relation.isOneToOneOwner || relation.isManyToOne
const resolve: GraphQLFieldResolver<any, any, { join: string[] }> = (
const resolve: GraphQLFieldResolver<any, any, {}> = (
source: any,
args: { join: string[] },
_,
ctx: ORMResolverContext,
) => {
if (source[relation.propertyName]) {
if (args.join && !(ctx && ctx.ignoreErrors)) {
throw new Error(
'Join argument is ignored here because a this field was already joined',
)
}

return source[relation.propertyName]
}

Expand All @@ -136,9 +124,10 @@ export function createEntityFieldOutputMethod(schemaBuilder: SchemaBuilder) {
: ctx.orm.queryDataLoader.load({
entity,
type: 'one',
schemaBuilder,
where: {
[sourceForeignKey]: source[entityPrimaryKey.propertyName],
} as ArgWhere,
} as ArgWhereType,
})
}

Expand Down
4 changes: 2 additions & 2 deletions src/nexus/nexus-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ interface EntityObjectDefinitionBlock<TEntity, TTypeName extends string>
}

interface EntityObjectTypeConfig<TEntity, TTypeName extends string>
extends Omit<NexusObjectTypeConfig<TTypeName>, 'definition'> {
extends Omit<NexusObjectTypeConfig<TTypeName>, 'definition' | 'name'> {
definition(t: EntityObjectDefinitionBlock<TEntity, TTypeName>): void
}

export function entityType<TEntity>(
entity: new (...args: any[]) => TEntity,
config?: Omit<EntityObjectTypeConfig<TEntity, any>, 'name'>,
config?: EntityObjectTypeConfig<TEntity, any>,
) {
const metadata = getDatabaseObjectMetadata(entity)
return objectType<any>(
Expand Down
Loading