├── .gitignore ├── .vscode └── launch.json ├── README.md ├── lib ├── cache │ ├── cache.ts │ ├── index.ts │ └── simple-cache.ts ├── codegen.ts ├── entity-manager.ts ├── entity.ts ├── index.ts ├── repository.ts ├── type-helpers.ts ├── types.ts └── utils.ts ├── package.json ├── prisma ├── client.ts ├── datamodel.prisma ├── docker-compose.yml ├── generated │ └── prisma-client │ │ ├── index.ts │ │ └── prisma-schema.ts ├── prisma.yml └── read-datamodel.ts ├── src ├── entities │ ├── User.entity.ts │ └── index.ts ├── generated.ts ├── nexus.ts ├── repositories │ └── User.ts ├── test1.ts └── test2.ts ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | out 3 | .idea -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch n°1", 11 | "program": "${workspaceFolder}/1/src/index.ts", 12 | "preLaunchTask": "tsc: build - tsconfig.json", 13 | "outFiles": ["${workspaceFolder}/out/**/*.js"] 14 | }, 15 | { 16 | "type": "node", 17 | "request": "launch", 18 | "name": "Launch n°2", 19 | "program": "${workspaceFolder}/2/src/index.ts", 20 | "preLaunchTask": "tsc: build - tsconfig.json", 21 | "outFiles": ["${workspaceFolder}/out/**/*.js"] 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ent 2 | 3 | An entity layer for Prisma that uses the DataMapper pattern. 4 | 5 | > **Warning**: This is more than WIP and cannot be used at all yet. 6 | 7 | 8 | 9 | ## Motivation 10 | 11 | So far, the only data-access layer possible with Prisma has been the `prisma-client`. While this is certainly the most flexible/performant layer (which can be seen as a query-builder in most other ORMs), we thought there was one missing piece for Prisma to compete even better with other ORMs in the landscape (such as TypeORM, Doctrine, ActiveRecord etc..): **Entities**. 12 | 13 | While you can certainly already build your own entity layer with the `prisma-client` today, we think there's room for another abstraction that would help you build that layer through code-generation. 14 | 15 | 16 | 17 | ## Benefits 18 | 19 | 20 | 21 | - Consistent & predictable data across your application 22 | - Structure to horizontally scale your project 23 | - Reusability 24 | - ... 25 | 26 | 27 | 28 | ## Entities ? 29 | 30 | Entities are domain models which you can use across your application to do business logic using consistent and predictable "data objects". 31 | 32 | 33 | 34 | ## What does the architecture look like? 35 | 36 | Datamodel -> Entity -> Repository -> Service (optional) -> API Layer 37 | 38 | - **Datamodel**: This is the traditional prisma datamodel defined in the `datamodel.prisma` 39 | 40 | - **Entity**: Classes that maps to your datamodel types 41 | 42 | - **Repository**: Layer reponsible for fetching data and hydrating your entities 43 | 44 | - **Service: (optional)**: Layer that aggregate several repositories and handle business logic 45 | 46 | - **API Layer (optional)**: Your traditional api layer, whether it's rest, graphql etc.. 47 | 48 | 49 | 50 | ## What does an entity look like ? 51 | 52 | Given the following prisma datamodel: 53 | 54 | ```graphql 55 | type User { 56 | id: ID! @id 57 | firstName: String! 58 | lastName: String! 59 | posts: [Post] 60 | } 61 | 62 | type Post { 63 | id: ID! @id 64 | body: String! 65 | } 66 | ``` 67 | 68 | The following entities are derived/generated: 69 | 70 | ```ts 71 | //generated.ts 72 | 73 | export class User extends BaseEntity<"User"> { 74 | static modelName = "User"; 75 | 76 | id: string; 77 | firstName: string; 78 | lastName: string; 79 | posts: () => Promise; 80 | 81 | constructor(protected input: UserInput) { 82 | super(input); 83 | } 84 | } 85 | 86 | export class Post extends BaseEntity<"Post"> { 87 | static modelName = "Post"; 88 | 89 | id: string; 90 | body: string; 91 | 92 | constructor(protected input: PostInput) { 93 | super(input); 94 | } 95 | } 96 | ``` 97 | 98 | 99 | 100 | As you may deduce, entities follow two rules: 101 | 102 | - Scalars are **fetched by default** 103 | - Relations are **lazy by default**. It means they're fetched only when accessing the property. (*More on that later*) 104 | 105 | Entities are "dumb" class which only responsability is to hold data. In most ORMs, entities are used to actually map your datamodel. 106 | With Prisma, because the datamodel is defined declaratively in the `datamodel.prisma` file, we do it the other way around and generate base entities for you from it. 107 | 108 | These base entities can then be extended to add computed fields: 109 | 110 | ```ts 111 | // ./entities/User.ts 112 | import { User as UserBase } from './generated' 113 | 114 | export class User extends UserBase { 115 | fullName() { 116 | return this.firstName + ' ' + this.lastName; 117 | } 118 | } 119 | ``` 120 | 121 | 122 | 123 | ## What does a repository look like ? 124 | 125 | 126 | 127 | Given the same datamodel above, the following base repositories will be generated: 128 | 129 | ```typescript 130 | export class UserBaseRepository extends Repository { 131 | static modelName = "User"; 132 | } 133 | 134 | export class PostBaseRepository extends Repository { 135 | static modelName = "Post"; 136 | } 137 | ``` 138 | 139 | 140 | 141 | As said earlier, repositories' responsabilities is to to fetch data + hydrate your entities (transform the javascript object returned from the `prisma-client` to the actual entity classes). 142 | 143 | 144 | 145 | By default, repositories have the following methods (**the write part is not yet done**): 146 | 147 | ```typescript 148 | // This is a schematic representation of the actual repository class 149 | class Repository { 150 | // Read 151 | findOne(args: FindOneOpts): Promise 152 | findMany(args: FindManyOpts): Primise 153 | 154 | // Write (to be done) 155 | create(args: CreateOpts:): Promise 156 | update(args: UpdateOpts): Promise 157 | delete(args: DeleteOpts): Promise 158 | } 159 | ``` 160 | 161 | 162 | 163 | Just like entities, repositories can be extended as well to add more specific methods: 164 | 165 | ```typescript 166 | // ./repositories/User.ts 167 | import { UserBaseRepository } from './generated.ts' 168 | 169 | export const class UserRepository extends UserBaseRepository { 170 | findUserWithPosts(id: string) { 171 | return this.findOne({ id, prefetch: { posts: true } }) 172 | } 173 | 174 | findUsersWithPostsAndAuthors() { 175 | return this.findMany({ 176 | prefetch: { 177 | posts: { author: true } 178 | } 179 | }) 180 | } 181 | } 182 | ``` 183 | 184 | 185 | 186 | As you may have noticed, `findOne` (and `findMany`) have a `prefetch` option. This allow you to prefetch (or eager-load) relations in a fully type-safe way. Later, when doing `await user.posts()`, the promise will already be resolved and thus return you the value instantly. 187 | 188 | 189 | 190 | ## How do I use repositories ? 191 | 192 | Repositories are never constructed manually. Instead you use an `EntityManager` to provide you instances of your repositories. 193 | 194 | ```typescript 195 | import { EntityManager } from 'prisma-ent' 196 | import { prisma } from 'prisma-client' 197 | import { entities } from './generated' // generated file also export the entities for convenience 198 | 199 | const em = new EntityManager({ 200 | client: prisma, 201 | entities, 202 | typegen: { 203 | customEntitiesPath: [ // Register the custom entities so they get imported/used in the code-generation 204 | path.join(__dirname, './entities/*.ts') 205 | ] 206 | } 207 | }) 208 | 209 | const userRepository = em.getRepository("User") // or em.getRepository(UserBaseRepository) 210 | const user = userRepository.findOne({ id: 1 }) 211 | const userPost = await user.posts() 212 | ``` 213 | 214 | 215 | 216 | > Note: While the EntityManager is currently the highest abstraction, there should be another one on top of it later. (something like `const pe = new PrismaEntity()`) 217 | 218 | 219 | 220 | ## What now ? 221 | 222 | This is very early and WIP. Feel free to check out the source code and see how it looks like in `src/*.ts`. 223 | 224 | The [prisma-client V2](https://github.com/prisma/rfcs/blob/new-ts-client-rfc/text/0000-new-ts-client.md) first need to be released in order for this to work. 225 | 226 | Any feedback for design decisions though issues are well appreciated. 227 | -------------------------------------------------------------------------------- /lib/cache/cache.ts: -------------------------------------------------------------------------------- 1 | interface Data { 2 | data: any; 3 | [x: string]: any; 4 | } 5 | export interface ICache { 6 | read(id: string): T | undefined; 7 | write(id: string, data: U, opts?: Record): T | undefined; 8 | remove?(id: string): T | undefined; 9 | } 10 | -------------------------------------------------------------------------------- /lib/cache/index.ts: -------------------------------------------------------------------------------- 1 | export { ICache } from "./cache"; 2 | export { SimpleCache } from "./simple-cache"; 3 | -------------------------------------------------------------------------------- /lib/cache/simple-cache.ts: -------------------------------------------------------------------------------- 1 | import { ICache } from "./cache"; 2 | 3 | interface SimpleWriteInput { 4 | data: Record; 5 | } 6 | 7 | interface SimpleCacheEntry { 8 | data: SimpleWriteInput["data"]; 9 | expires: number; 10 | } 11 | 12 | export class SimpleCache implements ICache { 13 | constructor( 14 | protected ttl: number = 1000, 15 | protected cache: Record = {} 16 | ) {} 17 | 18 | read(id: string) { 19 | let data = this.cache[id]; 20 | 21 | if (data && this.now() > data.expires) { 22 | delete this.cache[id]; 23 | 24 | return undefined; 25 | } 26 | 27 | return data; 28 | } 29 | 30 | write(id: string, input: SimpleWriteInput, opts?: { ttl?: number }) { 31 | if (!opts) { 32 | opts = {}; 33 | } 34 | if (!opts.ttl) { 35 | opts.ttl = this.ttl; 36 | } 37 | const previous = this.read(id); 38 | 39 | this.cache[id] = { ...input, expires: this.now() + opts.ttl }; 40 | 41 | return previous; 42 | } 43 | 44 | remove(id: string) { 45 | const previous = this.read(id); 46 | 47 | delete this.cache[id]; 48 | 49 | return previous; 50 | } 51 | 52 | private now() { 53 | return new Date().getTime(); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /lib/codegen.ts: -------------------------------------------------------------------------------- 1 | import { IGQLType, IGQLField } from "prisma-datamodel"; 2 | import { InternalBaseEntity } from "./entity"; 3 | import { Constructor } from "./type-helpers"; 4 | import { writeFileSync } from "fs"; 5 | import { EOL } from "os"; 6 | import * as glob from "glob"; 7 | import { flatMap } from "./utils"; 8 | import * as path from "path"; 9 | 10 | type CustomEntities = Record< 11 | string, 12 | { 13 | path: string; 14 | entityClass: Constructor>; 15 | } 16 | >; 17 | 18 | const DMtoTSTypes: Record = { 19 | String: "string", 20 | ID: "string", 21 | DateTime: "string" 22 | }; 23 | 24 | export function codegen( 25 | datamodelTypes: IGQLType[], 26 | globs: string[], 27 | clientAbsolutePath: string 28 | ) { 29 | const outputPath = path.join(__dirname, "../src/generated.ts"); 30 | const customEntitiesWithPath = findCustomEntities(globs); 31 | const entitiesName = datamodelTypes.map(m => m.name); 32 | const rendered = renderCodegen( 33 | datamodelTypes, 34 | customEntitiesWithPath, 35 | entitiesName, 36 | clientAbsolutePath, 37 | outputPath 38 | ); 39 | 40 | writeFileSync(outputPath, rendered); 41 | } 42 | 43 | function renderCodegen( 44 | datamodelTypes: IGQLType[], 45 | customEntitiesWithPath: CustomEntities, 46 | entitiesName: string[], 47 | absoluteClientPath: string, 48 | outputPath: string 49 | ) { 50 | return `\ 51 | import { 52 | Repository, 53 | BaseEntity, 54 | BaseEntities, 55 | CustomEntities 56 | } from "../lib"; 57 | import { PrismaClient } from "${getImportPathRelativeToOutput( 58 | absoluteClientPath, 59 | outputPath 60 | )}" 61 | ${Object.keys(customEntitiesWithPath) 62 | .map( 63 | className => 64 | `import { ${className} as ${getCustomEntityName( 65 | customEntitiesWithPath[className].entityClass, 66 | entitiesName 67 | )} } from "${getImportPathRelativeToOutput( 68 | customEntitiesWithPath[className].path, 69 | outputPath 70 | )}"` 71 | ) 72 | .join(";" + EOL)} 73 | 74 | declare global { 75 | interface PrismaEntity { 76 | models: { 77 | ${datamodelTypes.map(m => ` ${m.name}: ${getEntityName(m)};`).join(EOL)} 78 | } 79 | repositories: { 80 | ${datamodelTypes 81 | .map(m => ` ${m.name}: ${getRepositoryName(m)};`) 82 | .join(EOL)} 83 | } 84 | selects: { 85 | ${datamodelTypes.map(m => ` ${m.name}: ${getSelectName(m)};`).join(EOL)} 86 | } 87 | baseEntities: ${datamodelTypes.map(m => getEntityName(m)).join(" | ")} 88 | customEntities: ${Object.keys(customEntitiesWithPath) 89 | .map(key => 90 | getCustomEntityName( 91 | customEntitiesWithPath[key].entityClass, 92 | entitiesName 93 | ) 94 | ) 95 | .join(" | ")} 96 | } 97 | } 98 | 99 | ${datamodelTypes 100 | .map(m => renderEntity(m, customEntitiesWithPath, entitiesName)) 101 | .join(EOL)} 102 | ${datamodelTypes 103 | .map(m => renderRepository(m, customEntitiesWithPath, entitiesName)) 104 | .join(EOL)} 105 | export const entities: { 106 | baseEntities: BaseEntities 107 | customEntities: CustomEntities 108 | } = { 109 | baseEntities: [${datamodelTypes.map(m => getEntityName(m)).join(", ")}], 110 | customEntities: [ 111 | ${Object.keys(customEntitiesWithPath) 112 | .map( 113 | className => 114 | `require("${getImportPathRelativeToOutput( 115 | customEntitiesWithPath[className].path, 116 | outputPath 117 | )}").${className}` 118 | ) 119 | .join("," + EOL)} 120 | ] 121 | } 122 | `; 123 | } 124 | 125 | function renderEntity( 126 | model: IGQLType, 127 | customEntities: CustomEntities, 128 | entitiesName: string[] 129 | ) { 130 | return `\ 131 | export class ${getEntityName(model)} extends BaseEntity<"${model.name}"> { 132 | static modelName = "${model.name}"; 133 | 134 | constructor(protected input: ${getConstructorInputName(model)}) { 135 | super(); 136 | 137 | ${model.fields.map(f => ` ${renderEntityFieldSetter(f)}`).join(EOL)} 138 | } 139 | 140 | ${model.fields 141 | .map(f => ` ${renderEntityFieldDefinition(f, customEntities, entitiesName)}`) 142 | .join(EOL)} 143 | } 144 | 145 | ${renderInput(model, customEntities, entitiesName)} 146 | 147 | ${renderSelectInterface(model)} 148 | `; 149 | } 150 | 151 | function renderInput( 152 | model: IGQLType, 153 | customEntities: CustomEntities, 154 | entitiesName: string[] 155 | ) { 156 | return `\ 157 | export interface ${getConstructorInputName(model)} { 158 | ${model.fields 159 | .map(f => ` ${renderEntityFieldDefinition(f, customEntities, entitiesName)}`) 160 | .join(EOL)} 161 | }`; 162 | } 163 | 164 | function renderSelectInterface(model: IGQLType) { 165 | return `\ 166 | export interface ${getSelectName(model)} { 167 | ${model.fields 168 | .filter(f => typeof f.type !== "string") 169 | .map(f => ` ${f.name}?: boolean | ${getSelectName(f.type as IGQLType)}`) 170 | .join(";" + EOL)} 171 | }`; 172 | } 173 | 174 | function renderSelectFieldDefinition(fields: IGQLField[]) { 175 | return; 176 | } 177 | 178 | function renderRepository( 179 | model: IGQLType, 180 | customEntities: CustomEntities, 181 | entitiesName: string[] 182 | ) { 183 | return `\ 184 | export class ${getRepositoryName(model)} extends Repository<${referenceEntity( 185 | model, 186 | customEntities, 187 | entitiesName 188 | )}, PrismaClient> { 189 | static modelName = "${model.name}"; 190 | } 191 | `; 192 | } 193 | 194 | function renderEntityFieldSetter(field: IGQLField) { 195 | return `this.${field.name} = input.${field.name};`; 196 | } 197 | 198 | function renderEntityFieldDefinition( 199 | field: IGQLField, 200 | customEntities: CustomEntities, 201 | entitiesName: string[] 202 | ) { 203 | if (typeof field.type === "string") { 204 | return `${field.name}: ${DMtoTSTypes[field.type]};`; 205 | } 206 | 207 | const entityName = 208 | customEntities[field.type.name] !== undefined 209 | ? getCustomEntityName( 210 | customEntities[field.type.name].entityClass, 211 | entitiesName 212 | ) 213 | : getEntityName(field.type); 214 | 215 | if (field.isList) { 216 | return `${field.name}: () => Promise<${entityName}[]>;`; 217 | } 218 | 219 | return `${field.name}: () => Promise<${entityName}>;`; 220 | } 221 | 222 | function getConstructorInputName(model: IGQLType) { 223 | return `${model.name}Input`; 224 | } 225 | 226 | function getEntityName(model: IGQLType) { 227 | return model.name; 228 | } 229 | 230 | function getCustomEntityName( 231 | entityClass: Constructor, 232 | entitiesNames: string[] 233 | ) { 234 | const customEntityName = entityClass.name; 235 | 236 | if (entitiesNames.includes(customEntityName)) { 237 | return `Extended${customEntityName}`; 238 | } 239 | 240 | return customEntityName; 241 | } 242 | 243 | function referenceEntity( 244 | model: IGQLType, 245 | customEntities: CustomEntities, 246 | entitiesNames: string[] 247 | ) { 248 | if (customEntities[model.name]) { 249 | return getCustomEntityName( 250 | customEntities[model.name].entityClass, 251 | entitiesNames 252 | ); 253 | } 254 | 255 | return getEntityName(model); 256 | } 257 | 258 | function getRepositoryName(model: IGQLType) { 259 | return `${model.name}BaseRepository`; 260 | } 261 | 262 | function getSelectName(model: IGQLType) { 263 | return `${model.name}Select`; 264 | } 265 | 266 | export function getImportPathRelativeToOutput( 267 | importPath: string, 268 | outputDir: string 269 | ): string { 270 | let relativePath = path.relative(path.dirname(outputDir), importPath); 271 | 272 | if (!relativePath.startsWith(".")) { 273 | relativePath = "./" + relativePath; 274 | } 275 | 276 | // remove .ts or .js file extension 277 | relativePath = relativePath.replace(/\.(ts|js)$/, ""); 278 | 279 | // remove /index 280 | relativePath = relativePath.replace(/\/index$/, ""); 281 | 282 | // replace \ with / 283 | relativePath = relativePath.replace(/\\/g, "/"); 284 | 285 | return relativePath; 286 | } 287 | 288 | function findCustomEntities(globs: string[]) { 289 | return flatMap(globs, g => glob.sync(g)).reduce< 290 | Record 291 | >((acc, entityPath) => { 292 | const mod = require(entityPath); 293 | const keys = Object.keys(mod).filter( 294 | key => mod[key].modelName !== undefined 295 | ); 296 | 297 | keys.forEach(key => { 298 | acc[key] = { className: key, path: entityPath, entityClass: mod[key] }; 299 | }); 300 | 301 | return acc; 302 | }, {}); 303 | } 304 | -------------------------------------------------------------------------------- /lib/entity-manager.ts: -------------------------------------------------------------------------------- 1 | import { IGQLType } from "prisma-datamodel"; 2 | import { ICache } from "./cache/cache"; 3 | import { SimpleCache } from "./cache/simple-cache"; 4 | import { BaseEntity, InternalBaseEntity } from "./entity"; 5 | import { Repository } from "./repository"; 6 | import { GetGen } from "./type-helpers"; 7 | import { 8 | Client as PrismaClient, 9 | ObjectType, 10 | GetRepositoryFromEntity, 11 | GetRepositoryFromName, 12 | BaseEntities, 13 | CustomEntities 14 | } from "./types"; 15 | import { arrayToMap } from "./utils"; 16 | import { codegen } from "./codegen"; 17 | 18 | export class EntityManager { 19 | public entitiesMap: Record>; 20 | public customeEntitiesMap: Record>; 21 | public baseEntities: BaseEntities; 22 | public customEntities: CustomEntities; 23 | 24 | public cache: ICache; 25 | public client: Client; 26 | public modelNameToMetadata: Record; 27 | protected repositoriesCache: Record>; 28 | 29 | constructor(input: { 30 | client: Client; 31 | entities: { 32 | baseEntities: BaseEntities; 33 | customEntities?: CustomEntities; 34 | }; 35 | typegen: 36 | | false 37 | | { 38 | entitiesPath: string[]; 39 | }; 40 | cache?: ICache; 41 | }) { 42 | /** 43 | * Should be stored in the base entities during code-generation step or read from the client 44 | */ 45 | const typesMetadata = input.client.getDatamodel().types; 46 | this.modelNameToMetadata = arrayToMap(typesMetadata, t => t.name); 47 | 48 | if (input.typegen) { 49 | console.time("Generating types..."); 50 | codegen( 51 | typesMetadata, 52 | input.typegen.entitiesPath, 53 | input.client.getFilePath() 54 | ); 55 | console.timeEnd("Generating types..."); 56 | } 57 | 58 | this.cache = input.cache || new SimpleCache(); 59 | this.client = input.client; 60 | this.baseEntities = input.entities.baseEntities; 61 | this.customEntities = input.entities.customEntities || []; 62 | this.entitiesMap = arrayToMap( 63 | input.entities.baseEntities, 64 | e => e.modelName 65 | ); 66 | this.customeEntitiesMap = arrayToMap(this.customEntities, e => e.modelName); 67 | this.repositoriesCache = {}; 68 | } 69 | 70 | getRepository>( 71 | entity: Entity 72 | ): GetRepositoryFromName; 73 | getRepository BaseEntity>( 74 | entity: Entity 75 | ): GetRepositoryFromEntity; 76 | getRepository(entity: Entity): Repository { 77 | const modelName = 78 | typeof entity === "string" ? entity : (entity as any).modelName; 79 | 80 | if ( 81 | typeof entity !== "string" && 82 | !this.baseEntities.includes(entity as any) && 83 | !this.customEntities.includes(entity as any) 84 | ) { 85 | throw new Error( 86 | `Entity not found: ${modelName}. Register it in the entity manager constructor.` 87 | ); 88 | } 89 | 90 | const metadata = this.modelNameToMetadata[modelName]; 91 | 92 | if (this.repositoriesCache[modelName]) { 93 | return this.repositoriesCache[modelName] as any; 94 | } 95 | 96 | if (!metadata) { 97 | throw new Error(`Could not find metadata for model: "${entity}"`); 98 | } 99 | 100 | const newRepository = new Repository(this, metadata); 101 | this.repositoriesCache[modelName] = newRepository; 102 | 103 | return newRepository as any; 104 | } 105 | 106 | getCustomRepository(repository: ObjectType): T { 107 | const metadata = this.modelNameToMetadata[(repository as any).modelName]; 108 | 109 | if (!metadata) { 110 | throw new Error( 111 | `Could not find metadata for model: "${(repository as any).modelName}"` 112 | ); 113 | } 114 | 115 | const instance = new (repository as any)(this, metadata); 116 | 117 | return instance; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /lib/entity.ts: -------------------------------------------------------------------------------- 1 | export class BaseEntity { 2 | // Required for Typescript not to ignore the `Model` generic and allow to infer 3 | private __INTERNAL__MODEL__NAME__: Model; 4 | } 5 | 6 | export class InternalBaseEntity { 7 | static modelName: string; 8 | } 9 | -------------------------------------------------------------------------------- /lib/index.ts: -------------------------------------------------------------------------------- 1 | export { EntityManager } from "./entity-manager"; 2 | export { BaseEntity } from "./entity"; 3 | export { ICache, SimpleCache } from "./cache"; 4 | export { Repository } from "./repository"; 5 | export * from "./type-helpers"; 6 | export * from "./types"; 7 | -------------------------------------------------------------------------------- /lib/repository.ts: -------------------------------------------------------------------------------- 1 | import { IGQLField, IGQLType } from "prisma-datamodel"; 2 | import { EntityManager } from "./entity-manager"; 3 | import { BaseEntity } from "./entity"; 4 | import { 5 | FindOneRepoOptions, 6 | GetModelName, 7 | FindManyRepoOptions, 8 | Client as PrismaClient, 9 | ObjectType 10 | } from "./types"; 11 | 12 | export class Repository, Client extends any> { 13 | private modelNameToFindOne: Record; 14 | private modelNameToFindMany: Record; 15 | 16 | constructor( 17 | protected manager: EntityManager, 18 | protected metadata: IGQLType 19 | ) { 20 | this.modelNameToFindOne = this.generateModelNameToFindOne(metadata.name); 21 | this.modelNameToFindMany = this.generateModelNameToFindMany(metadata.name); 22 | } 23 | 24 | public async findOne( 25 | opts: FindOneRepoOptions> 26 | ): Promise { 27 | return this.internalFindOne(opts, this.metadata.name); 28 | } 29 | 30 | public async findMany( 31 | opts?: FindManyRepoOptions> 32 | ): Promise { 33 | return this.internalFindMany(opts || {}, this.metadata.name); 34 | } 35 | 36 | public transformToEntity( 37 | result: Record, 38 | selectStatement?: Record 39 | ): Entity { 40 | const entity = this.internalTransformToEntity( 41 | result, 42 | selectStatement, 43 | this.metadata.name 44 | ); 45 | 46 | return entity as any; 47 | } 48 | 49 | private async internalFindOne( 50 | opts: FindOneRepoOptions>, 51 | modelName: string 52 | ): Promise { 53 | const cacheId = `findOne-${modelName}-${JSON.stringify(opts)}`; 54 | const cached = this.manager.cache.read(cacheId); 55 | 56 | if (cached) { 57 | return cached.data; 58 | } 59 | 60 | const result = await this.manager.client[ 61 | this.modelNameToFindOne[modelName] 62 | ](opts); 63 | const entity = this.internalTransformToEntity( 64 | result, 65 | opts.select, 66 | modelName 67 | ); 68 | 69 | this.manager.cache.write(cacheId, { data: entity }); 70 | 71 | return entity as any; 72 | } 73 | 74 | private async internalFindMany( 75 | opts: FindManyRepoOptions>, 76 | modelName: string 77 | ): Promise { 78 | const cacheId = `findMany-${modelName}-${JSON.stringify(opts)}`; 79 | const cached = this.manager.cache.read(cacheId); 80 | 81 | if (cached) { 82 | return cached.data; 83 | } 84 | 85 | const results = await this.manager.client[ 86 | this.modelNameToFindMany[modelName] 87 | ](opts); 88 | 89 | const entities = results.map((result: Record) => 90 | this.internalTransformToEntity(result, opts.select, modelName) 91 | ); 92 | 93 | this.manager.cache.write(cacheId, { data: entities }); 94 | 95 | return entities; 96 | } 97 | 98 | private async loadRelation( 99 | relation: IGQLField, 100 | parentName: string, 101 | parentId: string | undefined 102 | ) { 103 | const relationType = relation.type as IGQLType; 104 | 105 | if (relation.isList) { 106 | return this.internalFindMany( 107 | { 108 | where: { [this.modelNameToFindOne[parentName]]: { id: parentId } } 109 | } as any, 110 | relationType.name 111 | ); 112 | } 113 | 114 | return this.internalFindOne({ id: parentId! }, relationType.name); 115 | } 116 | 117 | private internalTransformToEntity( 118 | result: Record, 119 | selectStatement: Record | undefined, 120 | modelName: string 121 | ): ObjectType { 122 | const metadata = this.manager.modelNameToMetadata[modelName]; 123 | 124 | const scalars = metadata.fields.filter(f => typeof f.type === "string"); 125 | const relations = metadata.fields.filter(f => typeof f.type !== "string"); 126 | 127 | let entityConstructorValues = scalars.reduce>( 128 | (acc, scalar) => { 129 | acc[scalar.name] = result[scalar.name]; 130 | 131 | return acc; 132 | }, 133 | {} 134 | ); 135 | 136 | relations.forEach(relation => { 137 | const relationName = relation.name; 138 | 139 | if (relation.isList) { 140 | if ( 141 | selectStatement && 142 | selectStatement[relationName] && 143 | result[relationName] 144 | ) { 145 | entityConstructorValues[relationName] = function() { 146 | return Promise.resolve(result[relation.name]); 147 | }; 148 | } else { 149 | entityConstructorValues = this.enableLazyLoad( 150 | relation, 151 | metadata, 152 | entityConstructorValues 153 | ); 154 | } 155 | } 156 | }); 157 | 158 | const entityClass = this.manager.customeEntitiesMap[modelName] 159 | ? this.manager.customeEntitiesMap[modelName] 160 | : this.manager.entitiesMap[modelName]; 161 | 162 | return new (entityClass as any)(entityConstructorValues as any); 163 | } 164 | 165 | private enableLazyLoad( 166 | relation: IGQLField, 167 | parent: IGQLType, 168 | entity: Record 169 | ) { 170 | const relationLoader = this; 171 | const dataIndex = "__" + relation.name + "__"; // in what property of the entity loaded data will be stored 172 | const promiseIndex = "__promise_" + relation.name + "__"; // in what property of the entity loading promise will be stored 173 | const resolveIndex = "__has_" + relation.name + "__"; // indicates if relation data already was loaded or not, we need this flag if loaded data is empty 174 | 175 | entity[relation.name] = function() { 176 | if (this[resolveIndex] === true || this[dataIndex]) { 177 | // if related data already was loaded then simply return it 178 | return Promise.resolve(this[dataIndex]); 179 | } 180 | 181 | if (this[promiseIndex]) { 182 | // if related data is loading then return a promise relationLoader loads it 183 | return this[promiseIndex]; 184 | } 185 | 186 | // nothing is loaded yet, load relation data and save it in the model once they are loaded 187 | this[promiseIndex] = relationLoader 188 | .loadRelation(relation, parent.name, entity.id) 189 | .then(result => { 190 | this[dataIndex] = result; 191 | this[resolveIndex] = true; 192 | delete this[promiseIndex]; 193 | return this[dataIndex]; 194 | }); 195 | return this[promiseIndex]; 196 | }; 197 | 198 | return entity; 199 | } 200 | 201 | // TODO: Change hard-coded values by computed ones 202 | private generateModelNameToFindOne(_modelName: string) { 203 | return { 204 | User: "user", 205 | Post: "post" 206 | }; 207 | } 208 | 209 | // TODO: Change hard-coded values by computed ones 210 | private generateModelNameToFindMany(_modelName: string) { 211 | return { 212 | User: "users", 213 | Post: "posts" 214 | }; 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /lib/type-helpers.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | interface PrismaEntity {} 3 | } 4 | 5 | export type Constructor = new (...args: any[]) => T; 6 | 7 | /** 8 | * Helpers for handling the generated types 9 | */ 10 | export type GenTypesShapeKeys = 11 | | "models" 12 | | "repositories" 13 | | "selects" 14 | | "customEntities" 15 | | "baseEntities"; 16 | 17 | export type GenTypesShape = Record; 18 | 19 | export type GetGen< 20 | K extends GenTypesShapeKeys, 21 | Fallback = any 22 | > = PrismaEntity extends infer GenTypes 23 | ? GenTypes extends GenTypesShape 24 | ? GenTypes[K] 25 | : Fallback 26 | : Fallback; 27 | 28 | export type GetGen2< 29 | K extends GenTypesShapeKeys, 30 | K2 extends keyof GenTypesShape[K] 31 | > = PrismaEntity extends infer GenTypes 32 | ? GenTypes extends GenTypesShape 33 | ? K extends keyof GenTypes 34 | ? K2 extends keyof GenTypes[K] 35 | ? GenTypes[K][K2] 36 | : any 37 | : any 38 | : any 39 | : any; 40 | -------------------------------------------------------------------------------- /lib/types.ts: -------------------------------------------------------------------------------- 1 | import { BaseEntity } from "./entity"; 2 | import { GetGen2, GetGen, Constructor } from "./type-helpers"; 3 | import { ISDL } from "prisma-datamodel"; 4 | 5 | export type GetModelName> = T extends BaseEntity< 6 | infer U 7 | > 8 | ? U 9 | : never; 10 | 11 | export type GetSelectType = GetGen2<"selects", T>; 12 | 13 | export type GetRepositoryFromEntity< 14 | T extends new (...args: any[]) => BaseEntity 15 | > = GetModelName> extends keyof GetGen<"repositories"> 16 | ? GetGen2<"repositories", GetModelName>> 17 | : never; 18 | export type GetRepositoryFromName< 19 | T extends keyof GetGen<"repositories"> 20 | > = GetGen2<"repositories", T>; 21 | 22 | export type EntityType> = GetGen2<"models", T>; 23 | 24 | export type ObjectType = { new (...args: any[]): T } | Function; 25 | export type BaseEntities = Array< 26 | Constructor> & { 27 | modelName: string; 28 | } 29 | >; 30 | export type CustomEntities = Array< 31 | Constructor> & { modelName: string } 32 | >; 33 | 34 | export type FindOneRepoOptions = { 35 | select?: GetSelectType; 36 | first?: number; 37 | id: string; 38 | }; 39 | 40 | export type FindManyRepoOptions = { 41 | select?: GetSelectType; 42 | where?: object; 43 | first?: number; 44 | last?: number; 45 | }; 46 | 47 | export interface Client { 48 | getDatamodel(): ISDL; 49 | [x: string]: any; 50 | } 51 | -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | export function groupBy( 2 | array: T[], 3 | callbackfn: (item: T) => R 4 | ): { id: R; items: T[] }[] { 5 | return array.reduce( 6 | (groupedArray, value) => { 7 | const key = callbackfn(value); 8 | let grouped = groupedArray.find(i => i.id === key); 9 | if (!grouped) { 10 | grouped = { id: key, items: [] }; 11 | groupedArray.push(grouped); 12 | } 13 | grouped.items.push(value); 14 | return groupedArray; 15 | }, 16 | [] as Array<{ id: R; items: T[] }> 17 | ); 18 | } 19 | 20 | export function arrayToMap( 21 | array: T[], 22 | callbackfn: (item: T) => string | number 23 | ): Record { 24 | return array.reduce>((acc, item) => { 25 | const id = callbackfn(item); 26 | 27 | acc[id] = item; 28 | return acc; 29 | }, {}); 30 | } 31 | 32 | export function flatMap( 33 | array: T[], 34 | callbackfn: (value: T, index: number, array: T[]) => U[] 35 | ): U[] { 36 | return Array.prototype.concat(...array.map(callbackfn)); 37 | } 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "entity_examples", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "@types/glob": "^7.1.1", 8 | "@types/node": "^11.13.5", 9 | "ajv": "^6.10.0", 10 | "apollo-server": "^2.4.8", 11 | "glob": "^7.1.3", 12 | "js-yaml": "^3.13.1", 13 | "nexus": "^0.11.6", 14 | "prisma": "^1.31.1", 15 | "prisma-datamodel": "^1.31.1", 16 | "prisma-json-schema": "^0.1.3", 17 | "ts-node": "^8.1.0", 18 | "typescript": "^3.4.4" 19 | }, 20 | "devDependencies": { 21 | "@types/js-yaml": "^3.12.1", 22 | "ts-node-dev": "^1.0.0-pre.32" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /prisma/client.ts: -------------------------------------------------------------------------------- 1 | import { FindOneRepoOptions } from "../lib/types"; 2 | import { ISDL } from "prisma-datamodel"; 3 | import { readPrismaYml, findDatamodelAndComputeSchema } from "./read-datamodel"; 4 | 5 | const postsData = [ 6 | { id: "1", title: "title_1", body: "body_1", user_id: "1" }, 7 | { id: "2", title: "title_2", body: "body_2", user_id: "1" }, 8 | { id: "3", title: "title_3", body: "body_3", user_id: "2" }, 9 | { id: "4", title: "title_4", body: "body_4", user_id: "2" }, 10 | { id: "5", title: "title_5", body: "body_5", user_id: "3" } 11 | ]; 12 | 13 | const usersData: User[] = [ 14 | { id: "1", firstName: "firstName_1", lastName: "lastName_1" }, 15 | { id: "2", firstName: "firstName_2", lastName: "lastName_2" }, 16 | { id: "3", firstName: "firstName_3", lastName: "lastName_3" } 17 | ]; 18 | 19 | interface User { 20 | id: string; 21 | firstName: string; 22 | lastName: string; 23 | posts?: Post[]; 24 | } 25 | 26 | interface Post { 27 | id: string; 28 | title: string; 29 | body: string; 30 | } 31 | 32 | type WhereInput = { 33 | user: { id: string }; 34 | }; 35 | 36 | export type PrismaClient = { 37 | user(opts: { 38 | id: string; 39 | select?: FindOneRepoOptions<"User">["select"]; 40 | }): Promise; 41 | users(opts: { 42 | select?: FindOneRepoOptions<"User">["select"]; 43 | }): Promise; 44 | posts(opts: { where?: WhereInput }): Promise; 45 | getDatamodel(): ISDL; 46 | getFilePath(): string; 47 | }; 48 | 49 | export const client: PrismaClient = { 50 | async user(opts: { 51 | id: string; 52 | select?: FindOneRepoOptions<"User">["select"]; 53 | where?: WhereInput; 54 | }) { 55 | await stall(2000); 56 | let result: any = usersData.find(u => u.id === opts.id); 57 | 58 | if (opts.select && opts.select.posts) { 59 | result["posts"] = postsData.filter(p => p.user_id === result.id); 60 | } 61 | 62 | return result; 63 | }, 64 | async users(opts: { 65 | select?: FindOneRepoOptions<"User">["select"]; 66 | where?: WhereInput; 67 | }) { 68 | await stall(2000); 69 | let result = usersData; 70 | 71 | if (opts.select && opts.select.posts) { 72 | result = result.map(r => { 73 | r.posts = postsData.filter(p => p.user_id === r.id); 74 | 75 | return r; 76 | }); 77 | } 78 | 79 | return result; 80 | }, 81 | async posts(opts: { where?: WhereInput }) { 82 | await stall(2000); 83 | 84 | if (opts.where) { 85 | return postsData.filter(p => p.user_id === opts.where!.user.id); 86 | } 87 | 88 | return postsData; 89 | }, 90 | getDatamodel() { 91 | const prisma = readPrismaYml(); 92 | 93 | /** 94 | * Should be stored in the base entities during code-generation step or read from the client 95 | */ 96 | return findDatamodelAndComputeSchema(prisma.configPath, prisma.config) 97 | .datamodel; 98 | }, 99 | getFilePath() { 100 | return __filename; 101 | } 102 | }; 103 | 104 | function stall(ms: number) { 105 | return new Promise(resolve => setTimeout(resolve, ms)); 106 | } 107 | -------------------------------------------------------------------------------- /prisma/datamodel.prisma: -------------------------------------------------------------------------------- 1 | type User { 2 | id: ID! @id 3 | firstName: String! 4 | lastName: String! 5 | posts: [Post] 6 | } 7 | 8 | type Post { 9 | id: ID! @id 10 | title: String! 11 | body: String! 12 | } 13 | -------------------------------------------------------------------------------- /prisma/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | prisma: 4 | image: prismagraphql/prisma:1.31 5 | restart: always 6 | ports: 7 | - "4466:4466" 8 | environment: 9 | PRISMA_CONFIG: | 10 | port: 4466 11 | # uncomment the next line and provide the env var PRISMA_MANAGEMENT_API_SECRET=my-secret to activate cluster security 12 | # managementApiSecret: my-secret 13 | databases: 14 | default: 15 | connector: postgres 16 | host: postgres 17 | user: prisma 18 | password: prisma 19 | rawAccess: true 20 | port: 5432 21 | migrations: true 22 | postgres: 23 | image: postgres 24 | restart: always 25 | # Uncomment the next two lines to connect to your your database from outside the Docker environment, e.g. using a database GUI like Postico 26 | # ports: 27 | # - "5432:5432" 28 | environment: 29 | POSTGRES_USER: prisma 30 | POSTGRES_PASSWORD: prisma 31 | volumes: 32 | - postgres:/var/lib/postgresql/data 33 | volumes: 34 | postgres: 35 | -------------------------------------------------------------------------------- /prisma/generated/prisma-client/index.ts: -------------------------------------------------------------------------------- 1 | // Code generated by Prisma (prisma@1.31.1). DO NOT EDIT. 2 | // Please don't change this file manually but run `prisma generate` to update it. 3 | // For more information, please read the docs: https://www.prisma.io/docs/prisma-client/ 4 | 5 | import { DocumentNode } from "graphql"; 6 | import { 7 | makePrismaClientClass, 8 | BaseClientOptions, 9 | Model 10 | } from "prisma-client-lib"; 11 | import { typeDefs } from "./prisma-schema"; 12 | 13 | export type AtLeastOne }> = Partial & 14 | U[keyof U]; 15 | 16 | export interface Exists { 17 | post: (where?: PostWhereInput) => Promise; 18 | user: (where?: UserWhereInput) => Promise; 19 | } 20 | 21 | export interface Node {} 22 | 23 | export type FragmentableArray = Promise> & Fragmentable; 24 | 25 | export interface Fragmentable { 26 | $fragment(fragment: string | DocumentNode): Promise; 27 | } 28 | 29 | export interface Prisma { 30 | $exists: Exists; 31 | $graphql: ( 32 | query: string, 33 | variables?: { [key: string]: any } 34 | ) => Promise; 35 | 36 | /** 37 | * Queries 38 | */ 39 | 40 | post: (where: PostWhereUniqueInput) => PostPromise; 41 | posts: (args?: { 42 | where?: PostWhereInput; 43 | orderBy?: PostOrderByInput; 44 | skip?: Int; 45 | after?: String; 46 | before?: String; 47 | first?: Int; 48 | last?: Int; 49 | }) => FragmentableArray; 50 | postsConnection: (args?: { 51 | where?: PostWhereInput; 52 | orderBy?: PostOrderByInput; 53 | skip?: Int; 54 | after?: String; 55 | before?: String; 56 | first?: Int; 57 | last?: Int; 58 | }) => PostConnectionPromise; 59 | user: (where: UserWhereUniqueInput) => UserPromise; 60 | users: (args?: { 61 | where?: UserWhereInput; 62 | orderBy?: UserOrderByInput; 63 | skip?: Int; 64 | after?: String; 65 | before?: String; 66 | first?: Int; 67 | last?: Int; 68 | }) => FragmentableArray; 69 | usersConnection: (args?: { 70 | where?: UserWhereInput; 71 | orderBy?: UserOrderByInput; 72 | skip?: Int; 73 | after?: String; 74 | before?: String; 75 | first?: Int; 76 | last?: Int; 77 | }) => UserConnectionPromise; 78 | node: (args: { id: ID_Output }) => Node; 79 | 80 | /** 81 | * Mutations 82 | */ 83 | 84 | createPost: (data: PostCreateInput) => PostPromise; 85 | updatePost: (args: { 86 | data: PostUpdateInput; 87 | where: PostWhereUniqueInput; 88 | }) => PostPromise; 89 | updateManyPosts: (args: { 90 | data: PostUpdateManyMutationInput; 91 | where?: PostWhereInput; 92 | }) => BatchPayloadPromise; 93 | upsertPost: (args: { 94 | where: PostWhereUniqueInput; 95 | create: PostCreateInput; 96 | update: PostUpdateInput; 97 | }) => PostPromise; 98 | deletePost: (where: PostWhereUniqueInput) => PostPromise; 99 | deleteManyPosts: (where?: PostWhereInput) => BatchPayloadPromise; 100 | createUser: (data: UserCreateInput) => UserPromise; 101 | updateUser: (args: { 102 | data: UserUpdateInput; 103 | where: UserWhereUniqueInput; 104 | }) => UserPromise; 105 | updateManyUsers: (args: { 106 | data: UserUpdateManyMutationInput; 107 | where?: UserWhereInput; 108 | }) => BatchPayloadPromise; 109 | upsertUser: (args: { 110 | where: UserWhereUniqueInput; 111 | create: UserCreateInput; 112 | update: UserUpdateInput; 113 | }) => UserPromise; 114 | deleteUser: (where: UserWhereUniqueInput) => UserPromise; 115 | deleteManyUsers: (where?: UserWhereInput) => BatchPayloadPromise; 116 | 117 | /** 118 | * Subscriptions 119 | */ 120 | 121 | $subscribe: Subscription; 122 | } 123 | 124 | export interface Subscription { 125 | post: ( 126 | where?: PostSubscriptionWhereInput 127 | ) => PostSubscriptionPayloadSubscription; 128 | user: ( 129 | where?: UserSubscriptionWhereInput 130 | ) => UserSubscriptionPayloadSubscription; 131 | } 132 | 133 | export interface ClientConstructor { 134 | new (options?: BaseClientOptions): T; 135 | } 136 | 137 | /** 138 | * Types 139 | */ 140 | 141 | export type PostOrderByInput = 142 | | "id_ASC" 143 | | "id_DESC" 144 | | "title_ASC" 145 | | "title_DESC" 146 | | "body_ASC" 147 | | "body_DESC"; 148 | 149 | export type UserOrderByInput = 150 | | "id_ASC" 151 | | "id_DESC" 152 | | "firstName_ASC" 153 | | "firstName_DESC" 154 | | "lastName_ASC" 155 | | "lastName_DESC"; 156 | 157 | export type MutationType = "CREATED" | "UPDATED" | "DELETED"; 158 | 159 | export interface UserUpdateOneRequiredWithoutPostsInput { 160 | create?: UserCreateWithoutPostsInput; 161 | update?: UserUpdateWithoutPostsDataInput; 162 | upsert?: UserUpsertWithoutPostsInput; 163 | connect?: UserWhereUniqueInput; 164 | } 165 | 166 | export type PostWhereUniqueInput = AtLeastOne<{ 167 | id: ID_Input; 168 | }>; 169 | 170 | export interface PostUpdateManyMutationInput { 171 | title?: String; 172 | body?: String; 173 | } 174 | 175 | export interface PostWhereInput { 176 | id?: ID_Input; 177 | id_not?: ID_Input; 178 | id_in?: ID_Input[] | ID_Input; 179 | id_not_in?: ID_Input[] | ID_Input; 180 | id_lt?: ID_Input; 181 | id_lte?: ID_Input; 182 | id_gt?: ID_Input; 183 | id_gte?: ID_Input; 184 | id_contains?: ID_Input; 185 | id_not_contains?: ID_Input; 186 | id_starts_with?: ID_Input; 187 | id_not_starts_with?: ID_Input; 188 | id_ends_with?: ID_Input; 189 | id_not_ends_with?: ID_Input; 190 | title?: String; 191 | title_not?: String; 192 | title_in?: String[] | String; 193 | title_not_in?: String[] | String; 194 | title_lt?: String; 195 | title_lte?: String; 196 | title_gt?: String; 197 | title_gte?: String; 198 | title_contains?: String; 199 | title_not_contains?: String; 200 | title_starts_with?: String; 201 | title_not_starts_with?: String; 202 | title_ends_with?: String; 203 | title_not_ends_with?: String; 204 | body?: String; 205 | body_not?: String; 206 | body_in?: String[] | String; 207 | body_not_in?: String[] | String; 208 | body_lt?: String; 209 | body_lte?: String; 210 | body_gt?: String; 211 | body_gte?: String; 212 | body_contains?: String; 213 | body_not_contains?: String; 214 | body_starts_with?: String; 215 | body_not_starts_with?: String; 216 | body_ends_with?: String; 217 | body_not_ends_with?: String; 218 | author?: UserWhereInput; 219 | AND?: PostWhereInput[] | PostWhereInput; 220 | OR?: PostWhereInput[] | PostWhereInput; 221 | NOT?: PostWhereInput[] | PostWhereInput; 222 | } 223 | 224 | export interface PostCreateInput { 225 | id?: ID_Input; 226 | title: String; 227 | body: String; 228 | author: UserCreateOneWithoutPostsInput; 229 | } 230 | 231 | export interface PostCreateManyWithoutAuthorInput { 232 | create?: PostCreateWithoutAuthorInput[] | PostCreateWithoutAuthorInput; 233 | connect?: PostWhereUniqueInput[] | PostWhereUniqueInput; 234 | } 235 | 236 | export interface UserCreateOneWithoutPostsInput { 237 | create?: UserCreateWithoutPostsInput; 238 | connect?: UserWhereUniqueInput; 239 | } 240 | 241 | export interface PostSubscriptionWhereInput { 242 | mutation_in?: MutationType[] | MutationType; 243 | updatedFields_contains?: String; 244 | updatedFields_contains_every?: String[] | String; 245 | updatedFields_contains_some?: String[] | String; 246 | node?: PostWhereInput; 247 | AND?: PostSubscriptionWhereInput[] | PostSubscriptionWhereInput; 248 | OR?: PostSubscriptionWhereInput[] | PostSubscriptionWhereInput; 249 | NOT?: PostSubscriptionWhereInput[] | PostSubscriptionWhereInput; 250 | } 251 | 252 | export interface UserCreateWithoutPostsInput { 253 | id?: ID_Input; 254 | firstName: String; 255 | lastName: String; 256 | } 257 | 258 | export interface PostUpdateManyDataInput { 259 | title?: String; 260 | body?: String; 261 | } 262 | 263 | export interface PostUpdateInput { 264 | title?: String; 265 | body?: String; 266 | author?: UserUpdateOneRequiredWithoutPostsInput; 267 | } 268 | 269 | export type UserWhereUniqueInput = AtLeastOne<{ 270 | id: ID_Input; 271 | }>; 272 | 273 | export interface PostUpdateManyWithoutAuthorInput { 274 | create?: PostCreateWithoutAuthorInput[] | PostCreateWithoutAuthorInput; 275 | delete?: PostWhereUniqueInput[] | PostWhereUniqueInput; 276 | connect?: PostWhereUniqueInput[] | PostWhereUniqueInput; 277 | set?: PostWhereUniqueInput[] | PostWhereUniqueInput; 278 | disconnect?: PostWhereUniqueInput[] | PostWhereUniqueInput; 279 | update?: 280 | | PostUpdateWithWhereUniqueWithoutAuthorInput[] 281 | | PostUpdateWithWhereUniqueWithoutAuthorInput; 282 | upsert?: 283 | | PostUpsertWithWhereUniqueWithoutAuthorInput[] 284 | | PostUpsertWithWhereUniqueWithoutAuthorInput; 285 | deleteMany?: PostScalarWhereInput[] | PostScalarWhereInput; 286 | updateMany?: 287 | | PostUpdateManyWithWhereNestedInput[] 288 | | PostUpdateManyWithWhereNestedInput; 289 | } 290 | 291 | export interface PostUpsertWithWhereUniqueWithoutAuthorInput { 292 | where: PostWhereUniqueInput; 293 | update: PostUpdateWithoutAuthorDataInput; 294 | create: PostCreateWithoutAuthorInput; 295 | } 296 | 297 | export interface UserUpdateWithoutPostsDataInput { 298 | firstName?: String; 299 | lastName?: String; 300 | } 301 | 302 | export interface PostUpdateWithWhereUniqueWithoutAuthorInput { 303 | where: PostWhereUniqueInput; 304 | data: PostUpdateWithoutAuthorDataInput; 305 | } 306 | 307 | export interface UserUpsertWithoutPostsInput { 308 | update: UserUpdateWithoutPostsDataInput; 309 | create: UserCreateWithoutPostsInput; 310 | } 311 | 312 | export interface UserSubscriptionWhereInput { 313 | mutation_in?: MutationType[] | MutationType; 314 | updatedFields_contains?: String; 315 | updatedFields_contains_every?: String[] | String; 316 | updatedFields_contains_some?: String[] | String; 317 | node?: UserWhereInput; 318 | AND?: UserSubscriptionWhereInput[] | UserSubscriptionWhereInput; 319 | OR?: UserSubscriptionWhereInput[] | UserSubscriptionWhereInput; 320 | NOT?: UserSubscriptionWhereInput[] | UserSubscriptionWhereInput; 321 | } 322 | 323 | export interface UserCreateInput { 324 | id?: ID_Input; 325 | firstName: String; 326 | lastName: String; 327 | posts?: PostCreateManyWithoutAuthorInput; 328 | } 329 | 330 | export interface PostCreateWithoutAuthorInput { 331 | id?: ID_Input; 332 | title: String; 333 | body: String; 334 | } 335 | 336 | export interface UserUpdateInput { 337 | firstName?: String; 338 | lastName?: String; 339 | posts?: PostUpdateManyWithoutAuthorInput; 340 | } 341 | 342 | export interface UserWhereInput { 343 | id?: ID_Input; 344 | id_not?: ID_Input; 345 | id_in?: ID_Input[] | ID_Input; 346 | id_not_in?: ID_Input[] | ID_Input; 347 | id_lt?: ID_Input; 348 | id_lte?: ID_Input; 349 | id_gt?: ID_Input; 350 | id_gte?: ID_Input; 351 | id_contains?: ID_Input; 352 | id_not_contains?: ID_Input; 353 | id_starts_with?: ID_Input; 354 | id_not_starts_with?: ID_Input; 355 | id_ends_with?: ID_Input; 356 | id_not_ends_with?: ID_Input; 357 | firstName?: String; 358 | firstName_not?: String; 359 | firstName_in?: String[] | String; 360 | firstName_not_in?: String[] | String; 361 | firstName_lt?: String; 362 | firstName_lte?: String; 363 | firstName_gt?: String; 364 | firstName_gte?: String; 365 | firstName_contains?: String; 366 | firstName_not_contains?: String; 367 | firstName_starts_with?: String; 368 | firstName_not_starts_with?: String; 369 | firstName_ends_with?: String; 370 | firstName_not_ends_with?: String; 371 | lastName?: String; 372 | lastName_not?: String; 373 | lastName_in?: String[] | String; 374 | lastName_not_in?: String[] | String; 375 | lastName_lt?: String; 376 | lastName_lte?: String; 377 | lastName_gt?: String; 378 | lastName_gte?: String; 379 | lastName_contains?: String; 380 | lastName_not_contains?: String; 381 | lastName_starts_with?: String; 382 | lastName_not_starts_with?: String; 383 | lastName_ends_with?: String; 384 | lastName_not_ends_with?: String; 385 | posts_every?: PostWhereInput; 386 | posts_some?: PostWhereInput; 387 | posts_none?: PostWhereInput; 388 | AND?: UserWhereInput[] | UserWhereInput; 389 | OR?: UserWhereInput[] | UserWhereInput; 390 | NOT?: UserWhereInput[] | UserWhereInput; 391 | } 392 | 393 | export interface UserUpdateManyMutationInput { 394 | firstName?: String; 395 | lastName?: String; 396 | } 397 | 398 | export interface PostUpdateWithoutAuthorDataInput { 399 | title?: String; 400 | body?: String; 401 | } 402 | 403 | export interface PostScalarWhereInput { 404 | id?: ID_Input; 405 | id_not?: ID_Input; 406 | id_in?: ID_Input[] | ID_Input; 407 | id_not_in?: ID_Input[] | ID_Input; 408 | id_lt?: ID_Input; 409 | id_lte?: ID_Input; 410 | id_gt?: ID_Input; 411 | id_gte?: ID_Input; 412 | id_contains?: ID_Input; 413 | id_not_contains?: ID_Input; 414 | id_starts_with?: ID_Input; 415 | id_not_starts_with?: ID_Input; 416 | id_ends_with?: ID_Input; 417 | id_not_ends_with?: ID_Input; 418 | title?: String; 419 | title_not?: String; 420 | title_in?: String[] | String; 421 | title_not_in?: String[] | String; 422 | title_lt?: String; 423 | title_lte?: String; 424 | title_gt?: String; 425 | title_gte?: String; 426 | title_contains?: String; 427 | title_not_contains?: String; 428 | title_starts_with?: String; 429 | title_not_starts_with?: String; 430 | title_ends_with?: String; 431 | title_not_ends_with?: String; 432 | body?: String; 433 | body_not?: String; 434 | body_in?: String[] | String; 435 | body_not_in?: String[] | String; 436 | body_lt?: String; 437 | body_lte?: String; 438 | body_gt?: String; 439 | body_gte?: String; 440 | body_contains?: String; 441 | body_not_contains?: String; 442 | body_starts_with?: String; 443 | body_not_starts_with?: String; 444 | body_ends_with?: String; 445 | body_not_ends_with?: String; 446 | AND?: PostScalarWhereInput[] | PostScalarWhereInput; 447 | OR?: PostScalarWhereInput[] | PostScalarWhereInput; 448 | NOT?: PostScalarWhereInput[] | PostScalarWhereInput; 449 | } 450 | 451 | export interface PostUpdateManyWithWhereNestedInput { 452 | where: PostScalarWhereInput; 453 | data: PostUpdateManyDataInput; 454 | } 455 | 456 | export interface NodeNode { 457 | id: ID_Output; 458 | } 459 | 460 | export interface BatchPayload { 461 | count: Long; 462 | } 463 | 464 | export interface BatchPayloadPromise 465 | extends Promise, 466 | Fragmentable { 467 | count: () => Promise; 468 | } 469 | 470 | export interface BatchPayloadSubscription 471 | extends Promise>, 472 | Fragmentable { 473 | count: () => Promise>; 474 | } 475 | 476 | export interface UserPreviousValues { 477 | id: ID_Output; 478 | firstName: String; 479 | lastName: String; 480 | } 481 | 482 | export interface UserPreviousValuesPromise 483 | extends Promise, 484 | Fragmentable { 485 | id: () => Promise; 486 | firstName: () => Promise; 487 | lastName: () => Promise; 488 | } 489 | 490 | export interface UserPreviousValuesSubscription 491 | extends Promise>, 492 | Fragmentable { 493 | id: () => Promise>; 494 | firstName: () => Promise>; 495 | lastName: () => Promise>; 496 | } 497 | 498 | export interface PostEdge { 499 | node: Post; 500 | cursor: String; 501 | } 502 | 503 | export interface PostEdgePromise extends Promise, Fragmentable { 504 | node: () => T; 505 | cursor: () => Promise; 506 | } 507 | 508 | export interface PostEdgeSubscription 509 | extends Promise>, 510 | Fragmentable { 511 | node: () => T; 512 | cursor: () => Promise>; 513 | } 514 | 515 | export interface User { 516 | id: ID_Output; 517 | firstName: String; 518 | lastName: String; 519 | } 520 | 521 | export interface UserPromise extends Promise, Fragmentable { 522 | id: () => Promise; 523 | firstName: () => Promise; 524 | lastName: () => Promise; 525 | posts: >(args?: { 526 | where?: PostWhereInput; 527 | orderBy?: PostOrderByInput; 528 | skip?: Int; 529 | after?: String; 530 | before?: String; 531 | first?: Int; 532 | last?: Int; 533 | }) => T; 534 | } 535 | 536 | export interface UserSubscription 537 | extends Promise>, 538 | Fragmentable { 539 | id: () => Promise>; 540 | firstName: () => Promise>; 541 | lastName: () => Promise>; 542 | posts: >>(args?: { 543 | where?: PostWhereInput; 544 | orderBy?: PostOrderByInput; 545 | skip?: Int; 546 | after?: String; 547 | before?: String; 548 | first?: Int; 549 | last?: Int; 550 | }) => T; 551 | } 552 | 553 | export interface PostConnection { 554 | pageInfo: PageInfo; 555 | edges: PostEdge[]; 556 | } 557 | 558 | export interface PostConnectionPromise 559 | extends Promise, 560 | Fragmentable { 561 | pageInfo: () => T; 562 | edges: >() => T; 563 | aggregate: () => T; 564 | } 565 | 566 | export interface PostConnectionSubscription 567 | extends Promise>, 568 | Fragmentable { 569 | pageInfo: () => T; 570 | edges: >>() => T; 571 | aggregate: () => T; 572 | } 573 | 574 | export interface PostPreviousValues { 575 | id: ID_Output; 576 | title: String; 577 | body: String; 578 | } 579 | 580 | export interface PostPreviousValuesPromise 581 | extends Promise, 582 | Fragmentable { 583 | id: () => Promise; 584 | title: () => Promise; 585 | body: () => Promise; 586 | } 587 | 588 | export interface PostPreviousValuesSubscription 589 | extends Promise>, 590 | Fragmentable { 591 | id: () => Promise>; 592 | title: () => Promise>; 593 | body: () => Promise>; 594 | } 595 | 596 | export interface PostSubscriptionPayload { 597 | mutation: MutationType; 598 | node: Post; 599 | updatedFields: String[]; 600 | previousValues: PostPreviousValues; 601 | } 602 | 603 | export interface PostSubscriptionPayloadPromise 604 | extends Promise, 605 | Fragmentable { 606 | mutation: () => Promise; 607 | node: () => T; 608 | updatedFields: () => Promise; 609 | previousValues: () => T; 610 | } 611 | 612 | export interface PostSubscriptionPayloadSubscription 613 | extends Promise>, 614 | Fragmentable { 615 | mutation: () => Promise>; 616 | node: () => T; 617 | updatedFields: () => Promise>; 618 | previousValues: () => T; 619 | } 620 | 621 | export interface Post { 622 | id: ID_Output; 623 | title: String; 624 | body: String; 625 | } 626 | 627 | export interface PostPromise extends Promise, Fragmentable { 628 | id: () => Promise; 629 | title: () => Promise; 630 | body: () => Promise; 631 | author: () => T; 632 | } 633 | 634 | export interface PostSubscription 635 | extends Promise>, 636 | Fragmentable { 637 | id: () => Promise>; 638 | title: () => Promise>; 639 | body: () => Promise>; 640 | author: () => T; 641 | } 642 | 643 | export interface PageInfo { 644 | hasNextPage: Boolean; 645 | hasPreviousPage: Boolean; 646 | startCursor?: String; 647 | endCursor?: String; 648 | } 649 | 650 | export interface PageInfoPromise extends Promise, Fragmentable { 651 | hasNextPage: () => Promise; 652 | hasPreviousPage: () => Promise; 653 | startCursor: () => Promise; 654 | endCursor: () => Promise; 655 | } 656 | 657 | export interface PageInfoSubscription 658 | extends Promise>, 659 | Fragmentable { 660 | hasNextPage: () => Promise>; 661 | hasPreviousPage: () => Promise>; 662 | startCursor: () => Promise>; 663 | endCursor: () => Promise>; 664 | } 665 | 666 | export interface AggregateUser { 667 | count: Int; 668 | } 669 | 670 | export interface AggregateUserPromise 671 | extends Promise, 672 | Fragmentable { 673 | count: () => Promise; 674 | } 675 | 676 | export interface AggregateUserSubscription 677 | extends Promise>, 678 | Fragmentable { 679 | count: () => Promise>; 680 | } 681 | 682 | export interface UserEdge { 683 | node: User; 684 | cursor: String; 685 | } 686 | 687 | export interface UserEdgePromise extends Promise, Fragmentable { 688 | node: () => T; 689 | cursor: () => Promise; 690 | } 691 | 692 | export interface UserEdgeSubscription 693 | extends Promise>, 694 | Fragmentable { 695 | node: () => T; 696 | cursor: () => Promise>; 697 | } 698 | 699 | export interface AggregatePost { 700 | count: Int; 701 | } 702 | 703 | export interface AggregatePostPromise 704 | extends Promise, 705 | Fragmentable { 706 | count: () => Promise; 707 | } 708 | 709 | export interface AggregatePostSubscription 710 | extends Promise>, 711 | Fragmentable { 712 | count: () => Promise>; 713 | } 714 | 715 | export interface UserSubscriptionPayload { 716 | mutation: MutationType; 717 | node: User; 718 | updatedFields: String[]; 719 | previousValues: UserPreviousValues; 720 | } 721 | 722 | export interface UserSubscriptionPayloadPromise 723 | extends Promise, 724 | Fragmentable { 725 | mutation: () => Promise; 726 | node: () => T; 727 | updatedFields: () => Promise; 728 | previousValues: () => T; 729 | } 730 | 731 | export interface UserSubscriptionPayloadSubscription 732 | extends Promise>, 733 | Fragmentable { 734 | mutation: () => Promise>; 735 | node: () => T; 736 | updatedFields: () => Promise>; 737 | previousValues: () => T; 738 | } 739 | 740 | export interface UserConnection { 741 | pageInfo: PageInfo; 742 | edges: UserEdge[]; 743 | } 744 | 745 | export interface UserConnectionPromise 746 | extends Promise, 747 | Fragmentable { 748 | pageInfo: () => T; 749 | edges: >() => T; 750 | aggregate: () => T; 751 | } 752 | 753 | export interface UserConnectionSubscription 754 | extends Promise>, 755 | Fragmentable { 756 | pageInfo: () => T; 757 | edges: >>() => T; 758 | aggregate: () => T; 759 | } 760 | 761 | /* 762 | The `Boolean` scalar type represents `true` or `false`. 763 | */ 764 | export type Boolean = boolean; 765 | 766 | export type Long = string; 767 | 768 | /* 769 | The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `"4"`) or integer (such as `4`) input value will be accepted as an ID. 770 | */ 771 | export type ID_Input = string | number; 772 | export type ID_Output = string; 773 | 774 | /* 775 | The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. 776 | */ 777 | export type String = string; 778 | 779 | /* 780 | The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. 781 | */ 782 | export type Int = number; 783 | 784 | /** 785 | * Model Metadata 786 | */ 787 | 788 | export const models: Model[] = [ 789 | { 790 | name: "User", 791 | embedded: false 792 | }, 793 | { 794 | name: "Post", 795 | embedded: false 796 | } 797 | ]; 798 | 799 | /** 800 | * Type Defs 801 | */ 802 | 803 | export const Prisma = makePrismaClientClass>({ 804 | typeDefs, 805 | models, 806 | endpoint: `http://localhost:4466` 807 | }); 808 | export const prisma = new Prisma(); 809 | -------------------------------------------------------------------------------- /prisma/generated/prisma-client/prisma-schema.ts: -------------------------------------------------------------------------------- 1 | // Code generated by Prisma (prisma@1.31.1). DO NOT EDIT. 2 | // Please don't change this file manually but run `prisma generate` to update it. 3 | // For more information, please read the docs: https://www.prisma.io/docs/prisma-client/ 4 | 5 | export const typeDefs = /* GraphQL */ `type AggregatePost { 6 | count: Int! 7 | } 8 | 9 | type AggregateUser { 10 | count: Int! 11 | } 12 | 13 | type BatchPayload { 14 | count: Long! 15 | } 16 | 17 | scalar Long 18 | 19 | type Mutation { 20 | createPost(data: PostCreateInput!): Post! 21 | updatePost(data: PostUpdateInput!, where: PostWhereUniqueInput!): Post 22 | updateManyPosts(data: PostUpdateManyMutationInput!, where: PostWhereInput): BatchPayload! 23 | upsertPost(where: PostWhereUniqueInput!, create: PostCreateInput!, update: PostUpdateInput!): Post! 24 | deletePost(where: PostWhereUniqueInput!): Post 25 | deleteManyPosts(where: PostWhereInput): BatchPayload! 26 | createUser(data: UserCreateInput!): User! 27 | updateUser(data: UserUpdateInput!, where: UserWhereUniqueInput!): User 28 | updateManyUsers(data: UserUpdateManyMutationInput!, where: UserWhereInput): BatchPayload! 29 | upsertUser(where: UserWhereUniqueInput!, create: UserCreateInput!, update: UserUpdateInput!): User! 30 | deleteUser(where: UserWhereUniqueInput!): User 31 | deleteManyUsers(where: UserWhereInput): BatchPayload! 32 | } 33 | 34 | enum MutationType { 35 | CREATED 36 | UPDATED 37 | DELETED 38 | } 39 | 40 | interface Node { 41 | id: ID! 42 | } 43 | 44 | type PageInfo { 45 | hasNextPage: Boolean! 46 | hasPreviousPage: Boolean! 47 | startCursor: String 48 | endCursor: String 49 | } 50 | 51 | type Post { 52 | id: ID! 53 | title: String! 54 | body: String! 55 | author: User! 56 | } 57 | 58 | type PostConnection { 59 | pageInfo: PageInfo! 60 | edges: [PostEdge]! 61 | aggregate: AggregatePost! 62 | } 63 | 64 | input PostCreateInput { 65 | id: ID 66 | title: String! 67 | body: String! 68 | author: UserCreateOneWithoutPostsInput! 69 | } 70 | 71 | input PostCreateManyWithoutAuthorInput { 72 | create: [PostCreateWithoutAuthorInput!] 73 | connect: [PostWhereUniqueInput!] 74 | } 75 | 76 | input PostCreateWithoutAuthorInput { 77 | id: ID 78 | title: String! 79 | body: String! 80 | } 81 | 82 | type PostEdge { 83 | node: Post! 84 | cursor: String! 85 | } 86 | 87 | enum PostOrderByInput { 88 | id_ASC 89 | id_DESC 90 | title_ASC 91 | title_DESC 92 | body_ASC 93 | body_DESC 94 | } 95 | 96 | type PostPreviousValues { 97 | id: ID! 98 | title: String! 99 | body: String! 100 | } 101 | 102 | input PostScalarWhereInput { 103 | id: ID 104 | id_not: ID 105 | id_in: [ID!] 106 | id_not_in: [ID!] 107 | id_lt: ID 108 | id_lte: ID 109 | id_gt: ID 110 | id_gte: ID 111 | id_contains: ID 112 | id_not_contains: ID 113 | id_starts_with: ID 114 | id_not_starts_with: ID 115 | id_ends_with: ID 116 | id_not_ends_with: ID 117 | title: String 118 | title_not: String 119 | title_in: [String!] 120 | title_not_in: [String!] 121 | title_lt: String 122 | title_lte: String 123 | title_gt: String 124 | title_gte: String 125 | title_contains: String 126 | title_not_contains: String 127 | title_starts_with: String 128 | title_not_starts_with: String 129 | title_ends_with: String 130 | title_not_ends_with: String 131 | body: String 132 | body_not: String 133 | body_in: [String!] 134 | body_not_in: [String!] 135 | body_lt: String 136 | body_lte: String 137 | body_gt: String 138 | body_gte: String 139 | body_contains: String 140 | body_not_contains: String 141 | body_starts_with: String 142 | body_not_starts_with: String 143 | body_ends_with: String 144 | body_not_ends_with: String 145 | AND: [PostScalarWhereInput!] 146 | OR: [PostScalarWhereInput!] 147 | NOT: [PostScalarWhereInput!] 148 | } 149 | 150 | type PostSubscriptionPayload { 151 | mutation: MutationType! 152 | node: Post 153 | updatedFields: [String!] 154 | previousValues: PostPreviousValues 155 | } 156 | 157 | input PostSubscriptionWhereInput { 158 | mutation_in: [MutationType!] 159 | updatedFields_contains: String 160 | updatedFields_contains_every: [String!] 161 | updatedFields_contains_some: [String!] 162 | node: PostWhereInput 163 | AND: [PostSubscriptionWhereInput!] 164 | OR: [PostSubscriptionWhereInput!] 165 | NOT: [PostSubscriptionWhereInput!] 166 | } 167 | 168 | input PostUpdateInput { 169 | title: String 170 | body: String 171 | author: UserUpdateOneRequiredWithoutPostsInput 172 | } 173 | 174 | input PostUpdateManyDataInput { 175 | title: String 176 | body: String 177 | } 178 | 179 | input PostUpdateManyMutationInput { 180 | title: String 181 | body: String 182 | } 183 | 184 | input PostUpdateManyWithoutAuthorInput { 185 | create: [PostCreateWithoutAuthorInput!] 186 | delete: [PostWhereUniqueInput!] 187 | connect: [PostWhereUniqueInput!] 188 | set: [PostWhereUniqueInput!] 189 | disconnect: [PostWhereUniqueInput!] 190 | update: [PostUpdateWithWhereUniqueWithoutAuthorInput!] 191 | upsert: [PostUpsertWithWhereUniqueWithoutAuthorInput!] 192 | deleteMany: [PostScalarWhereInput!] 193 | updateMany: [PostUpdateManyWithWhereNestedInput!] 194 | } 195 | 196 | input PostUpdateManyWithWhereNestedInput { 197 | where: PostScalarWhereInput! 198 | data: PostUpdateManyDataInput! 199 | } 200 | 201 | input PostUpdateWithoutAuthorDataInput { 202 | title: String 203 | body: String 204 | } 205 | 206 | input PostUpdateWithWhereUniqueWithoutAuthorInput { 207 | where: PostWhereUniqueInput! 208 | data: PostUpdateWithoutAuthorDataInput! 209 | } 210 | 211 | input PostUpsertWithWhereUniqueWithoutAuthorInput { 212 | where: PostWhereUniqueInput! 213 | update: PostUpdateWithoutAuthorDataInput! 214 | create: PostCreateWithoutAuthorInput! 215 | } 216 | 217 | input PostWhereInput { 218 | id: ID 219 | id_not: ID 220 | id_in: [ID!] 221 | id_not_in: [ID!] 222 | id_lt: ID 223 | id_lte: ID 224 | id_gt: ID 225 | id_gte: ID 226 | id_contains: ID 227 | id_not_contains: ID 228 | id_starts_with: ID 229 | id_not_starts_with: ID 230 | id_ends_with: ID 231 | id_not_ends_with: ID 232 | title: String 233 | title_not: String 234 | title_in: [String!] 235 | title_not_in: [String!] 236 | title_lt: String 237 | title_lte: String 238 | title_gt: String 239 | title_gte: String 240 | title_contains: String 241 | title_not_contains: String 242 | title_starts_with: String 243 | title_not_starts_with: String 244 | title_ends_with: String 245 | title_not_ends_with: String 246 | body: String 247 | body_not: String 248 | body_in: [String!] 249 | body_not_in: [String!] 250 | body_lt: String 251 | body_lte: String 252 | body_gt: String 253 | body_gte: String 254 | body_contains: String 255 | body_not_contains: String 256 | body_starts_with: String 257 | body_not_starts_with: String 258 | body_ends_with: String 259 | body_not_ends_with: String 260 | author: UserWhereInput 261 | AND: [PostWhereInput!] 262 | OR: [PostWhereInput!] 263 | NOT: [PostWhereInput!] 264 | } 265 | 266 | input PostWhereUniqueInput { 267 | id: ID 268 | } 269 | 270 | type Query { 271 | post(where: PostWhereUniqueInput!): Post 272 | posts(where: PostWhereInput, orderBy: PostOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [Post]! 273 | postsConnection(where: PostWhereInput, orderBy: PostOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): PostConnection! 274 | user(where: UserWhereUniqueInput!): User 275 | users(where: UserWhereInput, orderBy: UserOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [User]! 276 | usersConnection(where: UserWhereInput, orderBy: UserOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): UserConnection! 277 | node(id: ID!): Node 278 | } 279 | 280 | type Subscription { 281 | post(where: PostSubscriptionWhereInput): PostSubscriptionPayload 282 | user(where: UserSubscriptionWhereInput): UserSubscriptionPayload 283 | } 284 | 285 | type User { 286 | id: ID! 287 | firstName: String! 288 | lastName: String! 289 | posts(where: PostWhereInput, orderBy: PostOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [Post!] 290 | } 291 | 292 | type UserConnection { 293 | pageInfo: PageInfo! 294 | edges: [UserEdge]! 295 | aggregate: AggregateUser! 296 | } 297 | 298 | input UserCreateInput { 299 | id: ID 300 | firstName: String! 301 | lastName: String! 302 | posts: PostCreateManyWithoutAuthorInput 303 | } 304 | 305 | input UserCreateOneWithoutPostsInput { 306 | create: UserCreateWithoutPostsInput 307 | connect: UserWhereUniqueInput 308 | } 309 | 310 | input UserCreateWithoutPostsInput { 311 | id: ID 312 | firstName: String! 313 | lastName: String! 314 | } 315 | 316 | type UserEdge { 317 | node: User! 318 | cursor: String! 319 | } 320 | 321 | enum UserOrderByInput { 322 | id_ASC 323 | id_DESC 324 | firstName_ASC 325 | firstName_DESC 326 | lastName_ASC 327 | lastName_DESC 328 | } 329 | 330 | type UserPreviousValues { 331 | id: ID! 332 | firstName: String! 333 | lastName: String! 334 | } 335 | 336 | type UserSubscriptionPayload { 337 | mutation: MutationType! 338 | node: User 339 | updatedFields: [String!] 340 | previousValues: UserPreviousValues 341 | } 342 | 343 | input UserSubscriptionWhereInput { 344 | mutation_in: [MutationType!] 345 | updatedFields_contains: String 346 | updatedFields_contains_every: [String!] 347 | updatedFields_contains_some: [String!] 348 | node: UserWhereInput 349 | AND: [UserSubscriptionWhereInput!] 350 | OR: [UserSubscriptionWhereInput!] 351 | NOT: [UserSubscriptionWhereInput!] 352 | } 353 | 354 | input UserUpdateInput { 355 | firstName: String 356 | lastName: String 357 | posts: PostUpdateManyWithoutAuthorInput 358 | } 359 | 360 | input UserUpdateManyMutationInput { 361 | firstName: String 362 | lastName: String 363 | } 364 | 365 | input UserUpdateOneRequiredWithoutPostsInput { 366 | create: UserCreateWithoutPostsInput 367 | update: UserUpdateWithoutPostsDataInput 368 | upsert: UserUpsertWithoutPostsInput 369 | connect: UserWhereUniqueInput 370 | } 371 | 372 | input UserUpdateWithoutPostsDataInput { 373 | firstName: String 374 | lastName: String 375 | } 376 | 377 | input UserUpsertWithoutPostsInput { 378 | update: UserUpdateWithoutPostsDataInput! 379 | create: UserCreateWithoutPostsInput! 380 | } 381 | 382 | input UserWhereInput { 383 | id: ID 384 | id_not: ID 385 | id_in: [ID!] 386 | id_not_in: [ID!] 387 | id_lt: ID 388 | id_lte: ID 389 | id_gt: ID 390 | id_gte: ID 391 | id_contains: ID 392 | id_not_contains: ID 393 | id_starts_with: ID 394 | id_not_starts_with: ID 395 | id_ends_with: ID 396 | id_not_ends_with: ID 397 | firstName: String 398 | firstName_not: String 399 | firstName_in: [String!] 400 | firstName_not_in: [String!] 401 | firstName_lt: String 402 | firstName_lte: String 403 | firstName_gt: String 404 | firstName_gte: String 405 | firstName_contains: String 406 | firstName_not_contains: String 407 | firstName_starts_with: String 408 | firstName_not_starts_with: String 409 | firstName_ends_with: String 410 | firstName_not_ends_with: String 411 | lastName: String 412 | lastName_not: String 413 | lastName_in: [String!] 414 | lastName_not_in: [String!] 415 | lastName_lt: String 416 | lastName_lte: String 417 | lastName_gt: String 418 | lastName_gte: String 419 | lastName_contains: String 420 | lastName_not_contains: String 421 | lastName_starts_with: String 422 | lastName_not_starts_with: String 423 | lastName_ends_with: String 424 | lastName_not_ends_with: String 425 | posts_every: PostWhereInput 426 | posts_some: PostWhereInput 427 | posts_none: PostWhereInput 428 | AND: [UserWhereInput!] 429 | OR: [UserWhereInput!] 430 | NOT: [UserWhereInput!] 431 | } 432 | 433 | input UserWhereUniqueInput { 434 | id: ID 435 | } 436 | ` -------------------------------------------------------------------------------- /prisma/prisma.yml: -------------------------------------------------------------------------------- 1 | endpoint: http://localhost:4466 2 | datamodel: datamodel.prisma 3 | 4 | generate: 5 | - generator: typescript-client 6 | output: ./generated/prisma-client/ -------------------------------------------------------------------------------- /prisma/read-datamodel.ts: -------------------------------------------------------------------------------- 1 | import * as Ajv from 'ajv' 2 | import * as fs from 'fs' 3 | import * as yaml from 'js-yaml' 4 | import * as path from 'path' 5 | import { DatabaseType, DefaultParser, ISDL } from 'prisma-datamodel' 6 | import { PrismaDefinition } from 'prisma-json-schema' 7 | const schema = require('prisma-json-schema/dist/schema.json') 8 | 9 | const ajv = new Ajv().addMetaSchema( 10 | require('ajv/lib/refs/json-schema-draft-06.json'), 11 | ) 12 | 13 | const validate = ajv.compile(schema) 14 | 15 | export function findDatamodelAndComputeSchema( 16 | configPath: string, 17 | config: PrismaDefinition, 18 | ): { 19 | datamodel: ISDL 20 | databaseType: DatabaseType 21 | } { 22 | const typeDefs = getTypesString(config.datamodel!, path.dirname(configPath)) 23 | const databaseType = getDatabaseType(config) 24 | const ParserInstance = DefaultParser.create(databaseType) 25 | 26 | return { 27 | datamodel: ParserInstance.parseFromSchemaString(typeDefs), 28 | databaseType, 29 | } 30 | } 31 | 32 | export function readPrismaYml() { 33 | const configPath = findPrismaConfigFile() 34 | 35 | if (!configPath) { 36 | throw new Error('Could not find `prisma.yml` file') 37 | } 38 | 39 | try { 40 | const file = fs.readFileSync(configPath, 'utf-8') 41 | const config = yaml.safeLoad(file) as PrismaDefinition 42 | 43 | const valid = validate(config) 44 | 45 | if (!valid) { 46 | let errorMessage = 47 | `Invalid prisma.yml file` + '\n' + ajv.errorsText(validate.errors) 48 | throw new Error(errorMessage) 49 | } 50 | 51 | if (!config.datamodel) { 52 | throw new Error('Invalid prisma.yml file: Missing `datamodel` property') 53 | } 54 | 55 | if (!config.generate) { 56 | throw new Error( 57 | 'Invalid prisma.yml file: Missing `generate` property for a `prisma-client`', 58 | ) 59 | } 60 | 61 | return { config, configPath } 62 | } catch (e) { 63 | throw new Error(`Yaml parsing error in ${configPath}: ${e.message}`) 64 | } 65 | } 66 | 67 | function findPrismaConfigFile(): string | null { 68 | let definitionPath: string | null = path.join(process.cwd(), 'prisma.yml') 69 | 70 | if (fs.existsSync(definitionPath)) { 71 | return definitionPath 72 | } 73 | 74 | definitionPath = path.join(process.cwd(), 'prisma', 'prisma.yml') 75 | 76 | if (fs.existsSync(definitionPath)) { 77 | return definitionPath 78 | } 79 | 80 | return null 81 | } 82 | 83 | export function getPrismaClientDir( 84 | prismaClientDir: string | undefined, 85 | prisma: { config: PrismaDefinition; configPath: string }, 86 | rootPath: string, 87 | ) { 88 | if (prismaClientDir) { 89 | return prismaClientDir.startsWith('/') 90 | ? prismaClientDir 91 | : path.resolve(rootPath, prismaClientDir) 92 | } 93 | 94 | const clientGenerators = prisma.config.generate!.filter(gen => 95 | ['typescript-client', 'javascript-client'].includes(gen.generator), 96 | ) 97 | 98 | if (clientGenerators.length === 0) { 99 | throw new Error( 100 | 'No prisma-client generators were found in your prisma.yml file', 101 | ) 102 | } 103 | if (clientGenerators.length > 1) { 104 | throw new Error( 105 | 'Several prisma-client generators are defined in your prisma.yml file. If all are needed, use the `--client` option to point to the right one.', 106 | ) 107 | } 108 | 109 | return path.join(path.dirname(prisma.configPath), clientGenerators[0].output) 110 | } 111 | 112 | function getTypesString(datamodel: string | string[], definitionDir: string) { 113 | const typesPaths = datamodel 114 | ? Array.isArray(datamodel) 115 | ? datamodel 116 | : [datamodel] 117 | : [] 118 | 119 | let allTypes = '' 120 | 121 | typesPaths.forEach(unresolvedTypesPath => { 122 | const typesPath = path.join(definitionDir, unresolvedTypesPath!) 123 | if (fs.existsSync(typesPath)) { 124 | const types = fs.readFileSync(typesPath, 'utf-8') 125 | allTypes += types + '\n' 126 | } else { 127 | throw new Error( 128 | `The types definition file "${typesPath}" could not be found.`, 129 | ) 130 | } 131 | }) 132 | 133 | return allTypes 134 | } 135 | 136 | export function findRootDirectory(): string { 137 | const cwd = process.cwd() 138 | const tsConfig = findConfigFile(cwd, 'tsconfig.json') 139 | 140 | if (tsConfig) { 141 | return path.dirname(tsConfig) 142 | } 143 | 144 | const packageJson = findConfigFile(cwd, 'package.json') 145 | 146 | if (packageJson) { 147 | return path.dirname(packageJson) 148 | } 149 | 150 | return cwd 151 | } 152 | 153 | function findConfigFile( 154 | searchPath: string, 155 | configName = 'package.json', 156 | ): string | undefined { 157 | return forEachAncestorDirectory(searchPath, ancestor => { 158 | const fileName = path.join(ancestor, configName) 159 | return fs.existsSync(fileName) ? fileName : undefined 160 | }) 161 | } 162 | 163 | /** Calls `callback` on `directory` and every ancestor directory it has, returning the first defined result. */ 164 | function forEachAncestorDirectory( 165 | directory: string, 166 | callback: (directory: string) => T | undefined, 167 | ): T | undefined { 168 | while (true) { 169 | const result = callback(directory) 170 | if (result !== undefined) { 171 | return result 172 | } 173 | 174 | const parentPath = path.dirname(directory) 175 | if (parentPath === directory) { 176 | return undefined 177 | } 178 | 179 | directory = parentPath 180 | } 181 | } 182 | 183 | export function getImportPathRelativeToOutput( 184 | importPath: string, 185 | outputDir: string, 186 | ): string { 187 | let relativePath = path.relative(path.dirname(outputDir), importPath) 188 | 189 | if (!relativePath.startsWith('.')) { 190 | relativePath = './' + relativePath 191 | } 192 | 193 | // remove .ts or .js file extension 194 | relativePath = relativePath.replace(/\.(ts|js)$/, '') 195 | 196 | // remove /index 197 | relativePath = relativePath.replace(/\/index$/, '') 198 | 199 | // replace \ with / 200 | relativePath = relativePath.replace(/\\/g, '/') 201 | 202 | return relativePath 203 | } 204 | 205 | function getDatabaseType(definition: PrismaDefinition): DatabaseType { 206 | if (!definition.databaseType) { 207 | return DatabaseType.postgres 208 | } 209 | 210 | return definition.databaseType === 'document' 211 | ? DatabaseType.mongo 212 | : DatabaseType.postgres 213 | } 214 | -------------------------------------------------------------------------------- /src/entities/User.entity.ts: -------------------------------------------------------------------------------- 1 | import { User as UserBase } from "../generated"; 2 | 3 | export class User extends UserBase { 4 | fullName() { 5 | return this.firstName + " " + this.lastName; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/entities/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./User.entity"; 2 | -------------------------------------------------------------------------------- /src/generated.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Repository, 3 | BaseEntity, 4 | BaseEntities, 5 | CustomEntities 6 | } from "../lib"; 7 | import { PrismaClient } from "../prisma/client" 8 | import { User as ExtendedUser } from "./entities/User.entity" 9 | 10 | declare global { 11 | interface PrismaEntity { 12 | models: { 13 | User: User; 14 | Post: Post; 15 | } 16 | repositories: { 17 | User: UserBaseRepository; 18 | Post: PostBaseRepository; 19 | } 20 | selects: { 21 | User: UserSelect; 22 | Post: PostSelect; 23 | } 24 | baseEntities: User | Post 25 | customEntities: ExtendedUser 26 | } 27 | } 28 | 29 | export class User extends BaseEntity<"User"> { 30 | static modelName = "User"; 31 | 32 | constructor(protected input: UserInput) { 33 | super(); 34 | 35 | this.id = input.id; 36 | this.firstName = input.firstName; 37 | this.lastName = input.lastName; 38 | this.posts = input.posts; 39 | } 40 | 41 | id: string; 42 | firstName: string; 43 | lastName: string; 44 | posts: () => Promise; 45 | } 46 | 47 | export interface UserInput { 48 | id: string; 49 | firstName: string; 50 | lastName: string; 51 | posts: () => Promise; 52 | } 53 | 54 | export interface UserSelect { 55 | posts?: boolean | PostSelect 56 | } 57 | 58 | export class Post extends BaseEntity<"Post"> { 59 | static modelName = "Post"; 60 | 61 | constructor(protected input: PostInput) { 62 | super(); 63 | 64 | this.id = input.id; 65 | this.title = input.title; 66 | this.body = input.body; 67 | } 68 | 69 | id: string; 70 | title: string; 71 | body: string; 72 | } 73 | 74 | export interface PostInput { 75 | id: string; 76 | title: string; 77 | body: string; 78 | } 79 | 80 | export interface PostSelect { 81 | 82 | } 83 | 84 | export class UserBaseRepository extends Repository { 85 | static modelName = "User"; 86 | } 87 | 88 | export class PostBaseRepository extends Repository { 89 | static modelName = "Post"; 90 | } 91 | 92 | export const entities: { 93 | baseEntities: BaseEntities 94 | customEntities: CustomEntities 95 | } = { 96 | baseEntities: [User, Post], 97 | customEntities: [ 98 | require("./entities/User.entity").User 99 | ] 100 | } 101 | -------------------------------------------------------------------------------- /src/nexus.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file was automatically generated by Nexus 0.11.6 3 | * Do not make changes to this file directly 4 | */ 5 | 6 | import * as baseEntities from "./generated" 7 | import * as ctx from "./test2" 8 | import * as userEntity from "./entities/User.entity" 9 | 10 | 11 | declare global { 12 | interface NexusGen extends NexusGenTypes {} 13 | } 14 | 15 | export interface NexusGenInputs { 16 | } 17 | 18 | export interface NexusGenEnums { 19 | } 20 | 21 | export interface NexusGenRootTypes { 22 | Post: baseEntities.Post; 23 | Query: {}; 24 | User: userEntity.User; 25 | String: string; 26 | Int: number; 27 | Float: number; 28 | Boolean: boolean; 29 | ID: string; 30 | } 31 | 32 | export interface NexusGenAllTypes extends NexusGenRootTypes { 33 | } 34 | 35 | export interface NexusGenFieldTypes { 36 | Post: { // field return type 37 | body: string; // String! 38 | title: string; // String! 39 | } 40 | Query: { // field return type 41 | user: NexusGenRootTypes['User']; // User! 42 | } 43 | User: { // field return type 44 | firstName: string; // String! 45 | fullName: string; // String! 46 | id: string; // ID! 47 | lastName: string; // String! 48 | posts: NexusGenRootTypes['Post'][]; // [Post!]! 49 | } 50 | } 51 | 52 | export interface NexusGenArgTypes { 53 | } 54 | 55 | export interface NexusGenAbstractResolveReturnTypes { 56 | } 57 | 58 | export interface NexusGenInheritedFields {} 59 | 60 | export type NexusGenObjectNames = "Post" | "Query" | "User"; 61 | 62 | export type NexusGenInputNames = never; 63 | 64 | export type NexusGenEnumNames = never; 65 | 66 | export type NexusGenInterfaceNames = never; 67 | 68 | export type NexusGenScalarNames = "Boolean" | "Float" | "ID" | "Int" | "String"; 69 | 70 | export type NexusGenUnionNames = never; 71 | 72 | export interface NexusGenTypes { 73 | context: ctx.Context; 74 | inputTypes: NexusGenInputs; 75 | rootTypes: NexusGenRootTypes; 76 | argTypes: NexusGenArgTypes; 77 | fieldTypes: NexusGenFieldTypes; 78 | allTypes: NexusGenAllTypes; 79 | inheritedFields: NexusGenInheritedFields; 80 | objectNames: NexusGenObjectNames; 81 | inputNames: NexusGenInputNames; 82 | enumNames: NexusGenEnumNames; 83 | interfaceNames: NexusGenInterfaceNames; 84 | scalarNames: NexusGenScalarNames; 85 | unionNames: NexusGenUnionNames; 86 | allInputTypes: NexusGenTypes['inputNames'] | NexusGenTypes['enumNames'] | NexusGenTypes['scalarNames']; 87 | allOutputTypes: NexusGenTypes['objectNames'] | NexusGenTypes['enumNames'] | NexusGenTypes['unionNames'] | NexusGenTypes['interfaceNames'] | NexusGenTypes['scalarNames']; 88 | allNamedTypes: NexusGenTypes['allInputTypes'] | NexusGenTypes['allOutputTypes'] 89 | abstractTypes: NexusGenTypes['interfaceNames'] | NexusGenTypes['unionNames']; 90 | abstractResolveReturn: NexusGenAbstractResolveReturnTypes; 91 | } -------------------------------------------------------------------------------- /src/repositories/User.ts: -------------------------------------------------------------------------------- 1 | import { UserBaseRepository } from "../generated"; 2 | 3 | export class UserRepository extends UserBaseRepository { 4 | findUsersWithPosts() { 5 | return this.findMany({ select: { posts: true } }); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/test1.ts: -------------------------------------------------------------------------------- 1 | import { client, PrismaClient } from "../prisma/client"; 2 | import { EntityManager } from "../lib"; 3 | import { UserRepository } from "./repositories/User"; 4 | import { entities } from "./generated"; 5 | import { join } from "path"; 6 | 7 | async function findUsersWithPosts(manager: EntityManager) { 8 | const repository = manager.getCustomRepository(UserRepository); 9 | const users = await repository.findUsersWithPosts(); 10 | const firstUser = users[0]; 11 | 12 | console.time("lazy load posts"); 13 | const posts = await firstUser.posts(); 14 | console.timeEnd("lazy load posts"); 15 | 16 | console.log({ posts }); 17 | console.log({ secondAccess: await firstUser.posts() }); 18 | console.log({ fullName: firstUser.fullName() }); 19 | } 20 | 21 | async function testCachingFindOne(manager: EntityManager) { 22 | const repository = manager.getCustomRepository(UserRepository); 23 | 24 | for (let i = 0; i < 100; i++) { 25 | const user = await repository.findOne({ id: "1" }); 26 | 27 | console.log({ firstName: user.firstName }); 28 | } 29 | } 30 | 31 | async function load100UsersWithLazyPosts(manager: EntityManager) { 32 | const userRepository = manager.getCustomRepository(UserRepository); 33 | 34 | for (let i = 0; i < 100; i++) { 35 | let users = await userRepository.findMany(); 36 | 37 | for (let j = 0; j < users.length; j++) { 38 | let user = users[j]; 39 | await user.posts(); 40 | 41 | console.log({ loadedTimes: i + 1, postNumber: j + 1 }); 42 | } 43 | } 44 | } 45 | 46 | async function main() { 47 | const manager = new EntityManager({ 48 | client, 49 | entities, 50 | typegen: { 51 | entitiesPath: [join(__dirname, "entities/*.entity.ts")] 52 | } 53 | }); 54 | 55 | //await load100UsersWithLazyPosts(manager); 56 | await findUsersWithPosts(manager); 57 | } 58 | 59 | main(); 60 | -------------------------------------------------------------------------------- /src/test2.ts: -------------------------------------------------------------------------------- 1 | import { client, PrismaClient } from "../prisma/client"; 2 | import { EntityManager, SimpleCache } from "../lib"; 3 | import { makeSchema, queryType, objectType } from "nexus"; 4 | import { join } from "path"; 5 | import { ApolloServer } from "apollo-server"; 6 | import { entities } from "./generated"; 7 | 8 | export interface Context { 9 | manager: EntityManager; 10 | } 11 | 12 | const em = new EntityManager({ 13 | client, 14 | entities, 15 | cache: new SimpleCache(5 * 1000), 16 | typegen: { 17 | entitiesPath: [join(__dirname, "./entities/*.entity.ts")] 18 | } 19 | }); 20 | 21 | const Query = queryType({ 22 | definition(t) { 23 | t.field("user", { 24 | type: "User", 25 | async resolve(root, args, ctx) { 26 | const userRepo = await ctx.manager.getRepository("User"); 27 | 28 | return userRepo.findOne({ id: "1" }); 29 | } 30 | }); 31 | } 32 | }); 33 | 34 | const User = objectType({ 35 | name: "User", 36 | definition(t) { 37 | t.id("id"); 38 | t.string("firstName"); 39 | t.string("lastName"); 40 | t.string("fullName"); 41 | t.list.field("posts", { 42 | type: "Post" 43 | }); 44 | } 45 | }); 46 | 47 | const Post = objectType({ 48 | name: "Post", 49 | definition(t) { 50 | t.string("body"); 51 | t.string("title"); 52 | } 53 | }); 54 | 55 | const schema = makeSchema({ 56 | types: [Query, User, Post], 57 | outputs: { 58 | schema: false, 59 | typegen: join(__dirname, "./nexus.ts") 60 | }, 61 | typegenAutoConfig: { 62 | sources: [ 63 | { 64 | source: __filename, 65 | alias: "ctx" 66 | }, 67 | { 68 | source: join(__dirname, "./generated.ts"), 69 | alias: "baseEntities" 70 | }, 71 | { 72 | source: join(__dirname, "./entities/User.entity.ts"), 73 | alias: "userEntity" 74 | } 75 | ], 76 | contextType: "ctx.Context" 77 | } 78 | }); 79 | 80 | const server = new ApolloServer({ 81 | schema, 82 | context: () => ({ manager: em }) 83 | }); 84 | 85 | const port = process.env.PORT || 4000; 86 | 87 | server.listen({ port }, () => 88 | console.log( 89 | `🚀 Server ready at http://localhost:${port}${server.graphqlPath}` 90 | ) 91 | ); 92 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["es2017", "dom", "esnext.array"], 4 | "target": "es5", 5 | "module": "commonjs", 6 | "outDir": "out", 7 | "sourceMap": true, 8 | "skipLibCheck": true, 9 | "experimentalDecorators": true, 10 | "emitDecoratorMetadata": true 11 | } 12 | } 13 | --------------------------------------------------------------------------------