WIP on type generation overhaul

This commit is contained in:
Tim Griesser 2018-11-18 15:06:28 -05:00
parent 2e739d7913
commit 2858d448e0
29 changed files with 1240 additions and 3016 deletions

23
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,23 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debug SWAPI Api",
"protocol": "inspector",
"cwd": "${workspaceRoot}/examples/star-wars",
"runtimeExecutable": "${workspaceRoot}/examples/star-wars/node_modules/.bin/ts-node-dev",
"args": [
"--no-notify",
"--transpileOnly",
"${workspaceRoot}/examples/star-wars/src/index.ts"
],
"restart": true,
"sourceMaps": true
}
]
}

View File

@ -1,4 +1,4 @@
Copyright (c) 2018- Tim Griesser
Copyright (c) 2018 Tim Griesser
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation

View File

@ -126,7 +126,7 @@ When using the TypeScript, configuring the [backing object type](typescript-setu
## Resolving: defaultResolver
GQLiteral allows you to define an override to the `defaultResolver` both globally for the schema, as well as on a per-type basis. This can be quite powerful when you wish to define unique default behavior that goes beyond
GQLiteral allows you to define an override to the `defaultResolver` on a per-type basis. This can be quite powerful when you wish to define unique default behavior that goes beyond
## Generating the IDL file

37
docs/type-generation.md Normal file
View File

@ -0,0 +1,37 @@
---
id: type-generation
title: Type Generation Details
sidebar_label: Type Generation Details
---
This is relevant to JavaScript as well as TypeScript users, as the Intellisense of tools like VSCode can utilize these types to aid in autocomplete. The goal is to have the best possible type coverage with the least possible manual type annotation.
## Overview
GQLiteral was designed with TypeScript in mind. In order to fully typecheck our GraphQL objects, we need to generate a number of types that combine the schema, any type or field configuration provided, and the GraphQL resolution algorithm to create as much type-safety as possible without any additional work importing and assigning types throughout the codebase.
## Backing Types
A **backing type** is a type representation of the value used to resolve the fields of a GraphQL object type. In GraphQL terminology this is the `rootValue` for the object. Other tools like Prisma refer to this as a `model`.
Whatever you want to call it, just think of it as the object that will be passed as the first argument of `resolve`. It can be a plain JS object, a database model, a mongoose document, a JS class, anything that fulfills the contract defined by the GraphQL object type, based on
the field definitions.
Sometimes GraphQL types are passthrough, and don't have a dedicated type backing them. One such case would be in the `Edge` of a Relay style pagination. In this case, GQLiteral will generate a type-definition which makes assumptions of the necessary value to fulfill the contract. If this is incorrect, you can always provide a concrete type for the resolver.
> In the case of default resolve functions, we will lose type safety if the
> backing type is not defined. For this reason, defining a backing type is required
> for any object with a type-level resolver. If you wish to disable this behavior,
> add `strict: false` to the options for the schema.
## Return Type
A **return type** is the valid return value used to a field on an object type. In GraphQL, promises can be returned at every level of the type resolution and it will automatically handle them, so for this reason properly typing a return type can be tricky. If the **backing type** is explicitly defined, we'll assume the returned value should be that. Otherwise we'll check for any of the more esoteric combinations of promises and values as GraphQL permits.
## Field Args, Input Types, and Enums
These types are straightforward, they are just representations of their concrete representations.
## Custom Resolvers
When you define a custom resolver for a type, you are intentionally replacing the algorithm GraphQL uses to resolve field members.

View File

@ -8,8 +8,8 @@ export type BaseScalarNames = "String" | "Int" | "Float" | "ID" | "Boolean";
export interface Generated_Type_Query_Field_launch {
returnType:
| Generated_Type_Launch["backingType"]
| PromiseLike<Generated_Type_Launch["backingType"]>;
| Generated_Type_Launch["rootType"]
| PromiseLike<Generated_Type_Launch["rootType"]>;
args: Generated_Type_Query_Field_launch_Args;
}
@ -20,8 +20,8 @@ export interface Generated_Type_Query_Field_launch_Args {
export interface Generated_Type_Query_Field_launches {
returnType:
| Generated_Type_LaunchConnection["backingType"]
| PromiseLike<Generated_Type_LaunchConnection["backingType"]>;
| Generated_Type_LaunchConnection["rootType"]
| PromiseLike<Generated_Type_LaunchConnection["rootType"]>;
args: Generated_Type_Query_Field_launches_Args;
}
@ -33,8 +33,8 @@ export interface Generated_Type_Query_Field_launches_Args {
export interface Generated_Type_Query_Field_me {
returnType:
| null
| Generated_Type_User["backingType"]
| PromiseLike<null | Generated_Type_User["backingType"]>;
| Generated_Type_User["rootType"]
| PromiseLike<null | Generated_Type_User["rootType"]>;
args: {};
}
@ -45,7 +45,7 @@ export interface Generated_Type_Query_Fields {
}
export interface Generated_Type_Query {
backingType: unknown;
rootType: unknown;
fields: Generated_Type_Query_Fields;
}
@ -61,15 +61,15 @@ export interface Generated_Type_Launch_Field_isBooked {
export interface Generated_Type_Launch_Field_mission {
returnType:
| Generated_Type_Mission["backingType"]
| PromiseLike<Generated_Type_Mission["backingType"]>;
| Generated_Type_Mission["rootType"]
| PromiseLike<Generated_Type_Mission["rootType"]>;
args: {};
}
export interface Generated_Type_Launch_Field_rocket {
returnType:
| Generated_Type_Rocket["backingType"]
| PromiseLike<Generated_Type_Rocket["backingType"]>;
| Generated_Type_Rocket["rootType"]
| PromiseLike<Generated_Type_Rocket["rootType"]>;
args: {};
}
@ -87,7 +87,7 @@ export interface Generated_Type_Launch_Fields {
}
export interface Generated_Type_Launch {
backingType: t.Launch;
rootType: t.Launch;
fields: Generated_Type_Launch_Fields;
}
@ -111,7 +111,7 @@ export interface Generated_Type_Mission_Fields {
}
export interface Generated_Type_Mission {
backingType: t.Mission;
rootType: t.Mission;
fields: Generated_Type_Mission_Fields;
}
@ -137,7 +137,7 @@ export interface Generated_Type_Rocket_Fields {
}
export interface Generated_Type_Rocket {
backingType: unknown;
rootType: unknown;
fields: Generated_Type_Rocket_Fields;
}
@ -153,9 +153,9 @@ export interface Generated_Type_LaunchConnection_Field_hasMore {
export interface Generated_Type_LaunchConnection_Field_launches {
returnType:
| Generated_Type_Launch["backingType"][]
| PromiseLike<Generated_Type_Launch["backingType"][]>
| PromiseLike<Generated_Type_Launch["backingType"]>[];
| Generated_Type_Launch["rootType"][]
| PromiseLike<Generated_Type_Launch["rootType"][]>
| PromiseLike<Generated_Type_Launch["rootType"]>[];
args: {};
}
@ -166,7 +166,7 @@ export interface Generated_Type_LaunchConnection_Fields {
}
export interface Generated_Type_LaunchConnection {
backingType: unknown;
rootType: unknown;
fields: Generated_Type_LaunchConnection_Fields;
}
@ -182,9 +182,9 @@ export interface Generated_Type_User_Field_id {
export interface Generated_Type_User_Field_trips {
returnType:
| Generated_Type_Launch["backingType"][]
| PromiseLike<Generated_Type_Launch["backingType"][]>
| PromiseLike<Generated_Type_Launch["backingType"]>[];
| Generated_Type_Launch["rootType"][]
| PromiseLike<Generated_Type_Launch["rootType"][]>
| PromiseLike<Generated_Type_Launch["rootType"]>[];
args: {};
}
@ -195,14 +195,14 @@ export interface Generated_Type_User_Fields {
}
export interface Generated_Type_User {
backingType: unknown;
rootType: unknown;
fields: Generated_Type_User_Fields;
}
export interface Generated_Type_Mutation_Field_bookTrips {
returnType:
| Generated_Type_TripUpdateResponse["backingType"]
| PromiseLike<Generated_Type_TripUpdateResponse["backingType"]>;
| Generated_Type_TripUpdateResponse["rootType"]
| PromiseLike<Generated_Type_TripUpdateResponse["rootType"]>;
args: Generated_Type_Mutation_Field_bookTrips_Args;
}
@ -212,8 +212,8 @@ export interface Generated_Type_Mutation_Field_bookTrips_Args {
export interface Generated_Type_Mutation_Field_cancelTrip {
returnType:
| Generated_Type_TripUpdateResponse["backingType"]
| PromiseLike<Generated_Type_TripUpdateResponse["backingType"]>;
| Generated_Type_TripUpdateResponse["rootType"]
| PromiseLike<Generated_Type_TripUpdateResponse["rootType"]>;
args: Generated_Type_Mutation_Field_cancelTrip_Args;
}
@ -237,15 +237,15 @@ export interface Generated_Type_Mutation_Fields {
}
export interface Generated_Type_Mutation {
backingType: unknown;
rootType: unknown;
fields: Generated_Type_Mutation_Fields;
}
export interface Generated_Type_TripUpdateResponse_Field_launches {
returnType:
| null
| Generated_Type_Launch["backingType"]
| PromiseLike<null | Generated_Type_Launch["backingType"]>;
| Generated_Type_Launch["rootType"]
| PromiseLike<null | Generated_Type_Launch["rootType"]>;
args: {};
}
@ -266,7 +266,7 @@ export interface Generated_Type_TripUpdateResponse_Fields {
}
export interface Generated_Type_TripUpdateResponse {
backingType: unknown;
rootType: unknown;
fields: Generated_Type_TripUpdateResponse_Fields;
}

View File

@ -13,14 +13,14 @@ const UserApi = require("fullstack-tutorial/final/server/src/datasources/user.js
const schema = GQLiteralSchema({
types,
definitionFilePath: path.join(__dirname, "../fullstack-schema.graphql"),
schemaFilePath: path.join(__dirname, "../fullstack-schema.graphql"),
typeGeneration: {
typesFilePath: path.join(__dirname, "../src/fullstackTypes.ts"),
outputPath: path.join(__dirname, "../src/fullstackTypes.ts"),
imports: {
t: path.join(__dirname, "../src/typeDefs.ts"),
},
contextType: "t.Context",
backingTypes: {
rootTypes: {
Launch: "t.Launch",
Mission: "t.Mission",
},

View File

@ -5,16 +5,16 @@
export interface Generated_Type_Query_Field_currentUser {
returnType:
| Generated_Type_User["backingType"]
| PromiseLike<Generated_Type_User["backingType"]>;
| Generated_Type_User["rootType"]
| PromiseLike<Generated_Type_User["rootType"]>;
args: {};
}
export interface Generated_Type_Query_Field_entry {
returnType:
| null
| Generated_Type_Entry["backingType"]
| PromiseLike<null | Generated_Type_Entry["backingType"]>;
| Generated_Type_Entry["rootType"]
| PromiseLike<null | Generated_Type_Entry["rootType"]>;
args: Generated_Type_Query_Field_entry_Args;
}
@ -24,9 +24,9 @@ export interface Generated_Type_Query_Field_entry_Args {
export interface Generated_Type_Query_Field_feed {
returnType:
| Generated_Type_Entry["backingType"][]
| PromiseLike<Generated_Type_Entry["backingType"][]>
| PromiseLike<Generated_Type_Entry["backingType"]>[];
| Generated_Type_Entry["rootType"][]
| PromiseLike<Generated_Type_Entry["rootType"][]>
| PromiseLike<Generated_Type_Entry["rootType"]>[];
args: Generated_Type_Query_Field_feed_Args;
}
@ -43,7 +43,7 @@ export interface Generated_Type_Query_Fields {
}
export interface Generated_Type_Query {
backingType: unknown;
rootType: unknown;
fields: Generated_Type_Query_Fields;
}
@ -69,7 +69,7 @@ export interface Generated_Type_User_Fields {
}
export interface Generated_Type_User {
backingType: unknown;
rootType: unknown;
fields: Generated_Type_User_Fields;
}
@ -80,9 +80,9 @@ export interface Generated_Type_Entry_Field_commentCount {
export interface Generated_Type_Entry_Field_comments {
returnType:
| Generated_Type_Comment["backingType"][]
| PromiseLike<Generated_Type_Comment["backingType"][]>
| PromiseLike<Generated_Type_Comment["backingType"]>[];
| Generated_Type_Comment["rootType"][]
| PromiseLike<Generated_Type_Comment["rootType"][]>
| PromiseLike<Generated_Type_Comment["rootType"]>[];
args: Generated_Type_Entry_Field_comments_Args;
}
@ -109,15 +109,15 @@ export interface Generated_Type_Entry_Field_id {
export interface Generated_Type_Entry_Field_postedBy {
returnType:
| null
| Generated_Type_User["backingType"]
| PromiseLike<null | Generated_Type_User["backingType"]>;
| Generated_Type_User["rootType"]
| PromiseLike<null | Generated_Type_User["rootType"]>;
args: {};
}
export interface Generated_Type_Entry_Field_repository {
returnType:
| Generated_Type_Repository["backingType"]
| PromiseLike<Generated_Type_Repository["backingType"]>;
| Generated_Type_Repository["rootType"]
| PromiseLike<Generated_Type_Repository["rootType"]>;
args: {};
}
@ -128,8 +128,8 @@ export interface Generated_Type_Entry_Field_score {
export interface Generated_Type_Entry_Field_vote {
returnType:
| Generated_Type_Vote["backingType"]
| PromiseLike<Generated_Type_Vote["backingType"]>;
| Generated_Type_Vote["rootType"]
| PromiseLike<Generated_Type_Vote["rootType"]>;
args: {};
}
@ -146,7 +146,7 @@ export interface Generated_Type_Entry_Fields {
}
export interface Generated_Type_Entry {
backingType: unknown;
rootType: unknown;
fields: Generated_Type_Entry_Fields;
}
@ -168,8 +168,8 @@ export interface Generated_Type_Comment_Field_id {
export interface Generated_Type_Comment_Field_postedBy {
returnType:
| null
| Generated_Type_User["backingType"]
| PromiseLike<null | Generated_Type_User["backingType"]>;
| Generated_Type_User["rootType"]
| PromiseLike<null | Generated_Type_User["rootType"]>;
args: {};
}
@ -187,7 +187,7 @@ export interface Generated_Type_Comment_Fields {
}
export interface Generated_Type_Comment {
backingType: unknown;
rootType: unknown;
fields: Generated_Type_Comment_Fields;
}
@ -219,8 +219,8 @@ export interface Generated_Type_Repository_Field_open_issues_count {
export interface Generated_Type_Repository_Field_owner {
returnType:
| null
| Generated_Type_User["backingType"]
| PromiseLike<null | Generated_Type_User["backingType"]>;
| Generated_Type_User["rootType"]
| PromiseLike<null | Generated_Type_User["rootType"]>;
args: {};
}
@ -240,7 +240,7 @@ export interface Generated_Type_Repository_Fields {
}
export interface Generated_Type_Repository {
backingType: unknown;
rootType: unknown;
fields: Generated_Type_Repository_Fields;
}
@ -254,14 +254,14 @@ export interface Generated_Type_Vote_Fields {
}
export interface Generated_Type_Vote {
backingType: unknown;
rootType: unknown;
fields: Generated_Type_Vote_Fields;
}
export interface Generated_Type_Mutation_Field_submitComment {
returnType:
| Generated_Type_Comment["backingType"]
| PromiseLike<Generated_Type_Comment["backingType"]>;
| Generated_Type_Comment["rootType"]
| PromiseLike<Generated_Type_Comment["rootType"]>;
args: Generated_Type_Mutation_Field_submitComment_Args;
}
@ -272,8 +272,8 @@ export interface Generated_Type_Mutation_Field_submitComment_Args {
export interface Generated_Type_Mutation_Field_submitRepository {
returnType:
| Generated_Type_Entry["backingType"]
| PromiseLike<Generated_Type_Entry["backingType"]>;
| Generated_Type_Entry["rootType"]
| PromiseLike<Generated_Type_Entry["rootType"]>;
args: Generated_Type_Mutation_Field_submitRepository_Args;
}
@ -283,8 +283,8 @@ export interface Generated_Type_Mutation_Field_submitRepository_Args {
export interface Generated_Type_Mutation_Field_vote {
returnType:
| Generated_Type_Entry["backingType"]
| PromiseLike<Generated_Type_Entry["backingType"]>;
| Generated_Type_Entry["rootType"]
| PromiseLike<Generated_Type_Entry["rootType"]>;
args: Generated_Type_Mutation_Field_vote_Args;
}
@ -300,7 +300,7 @@ export interface Generated_Type_Mutation_Fields {
}
export interface Generated_Type_Mutation {
backingType: unknown;
rootType: unknown;
fields: Generated_Type_Mutation_Fields;
}

View File

@ -6,9 +6,9 @@ const types = require("./schema");
const schema = GQLiteralSchema({
types,
definitionFilePath: path.join(__dirname, "../githunt-api-schema.graphql"),
schemaFilePath: path.join(__dirname, "../githunt-api-schema.graphql"),
typeGeneration: {
typesFilePath: path.join(__dirname, "../githuntTypes.ts"),
outputPath: path.join(__dirname, "../githuntTypes.ts"),
},
});

View File

@ -2,7 +2,7 @@
"name": "gqliteral-swapi-example",
"version": "0.0.0",
"scripts": {
"start": "ts-node-dev --no-notify --respawn ./src"
"start": "ts-node-dev --no-notify --transpileOnly --respawn ./src"
},
"dependencies": {
"gqliteral": "^0.1.1",

View File

@ -1,159 +1,178 @@
/* tslint:disable */
/**
* This file is automatically generated by gqliteral
* Do not make changes directly
* This file is automatically generated not make changes directly
* This fill will regenerate when the server is started
* and NODE_ENV !== "production".
*
* These types are only for internal use by gqliteral and subject
* to change. The "*Args" / InputObject types are the only types
* intended for external usage.
*
* If you want more robust types to use in your code, look into one of:
*
* https://github.com/dotansimha/graphql-code-generator
* https://github.com/prisma/graphqlgen
* https://github.com/apollographql/apollo-tooling
*/
import * as swapi from "./gqliteral/backingTypes";
export interface Generated_Interface_Character_Fields {
appearsIn: {
returnType:
| GeneratedEnums["Episode"][]
| PromiseLike<GeneratedEnums["Episode"][]>
| PromiseLike<GeneratedEnums["Episode"]>[];
args: {
id: string;
};
};
friends: {
returnType:
| Generated_Interface_Character["backingType"][]
| PromiseLike<Generated_Interface_Character["backingType"][]>
| PromiseLike<Generated_Interface_Character["backingType"]>[];
args: {};
};
id: {
returnType: string | PromiseLike<string>;
args: {};
};
name: {
returnType: string | PromiseLike<string>;
args: {};
};
}
// Maybe Promise
type MP<T> = PromiseLike<T> | T;
export interface Generated_Interface_Character {
implementingTypes: "Droid" | "Human";
backingType: swapi.Droid | swapi.Human;
fields: Generated_Interface_Character_Fields;
}
// Maybe Promise List
type MPL<T> = MP<T>[];
export interface Generated_Type_Query_Field_droid {
returnType:
| Generated_Type_Droid["backingType"]
| PromiseLike<Generated_Type_Droid["backingType"]>;
args: Generated_Type_Query_Field_droid_Args;
}
// Maybe Thunk
type MT<T> = T | (() => T);
export interface Generated_Type_Query_Field_droid_Args {
// Maybe Thunk, with args
type MTA<T, A> = T | ((args?: A) => T);
type QueryDroidReturnType = MP<DroidReturnType>;
export interface QueryDroidArgs {
id: string;
}
export interface Generated_Type_Query_Field_hero {
returnType:
| Generated_Interface_Character["backingType"]
| PromiseLike<Generated_Interface_Character["backingType"]>;
args: Generated_Type_Query_Field_hero_Args;
type QueryHeroReturnType = MP<CharacterReturnType>;
export interface QueryHeroArgs {
episode?: Episode;
}
export interface Generated_Type_Query_Field_hero_Args {
episode?: GeneratedEnums["Episode"] | null | undefined;
}
type QueryHumanReturnType = MP<HumanReturnType>;
export interface Generated_Type_Query_Field_human {
returnType:
| Generated_Type_Human["backingType"]
| PromiseLike<Generated_Type_Human["backingType"]>;
args: Generated_Type_Query_Field_human_Args;
}
export interface Generated_Type_Query_Field_human_Args {
export interface QueryHumanArgs {
id: string;
}
export interface Generated_Type_Query_Fields {
droid: Generated_Type_Query_Field_droid;
hero: Generated_Type_Query_Field_hero;
human: Generated_Type_Query_Field_human;
type QueryRootType = {};
type QueryReturnType = {};
type DroidAppearsInReturnType = MP<MPL<MP<Episode>>>;
export interface DroidAppearsInArgs extends CharacterAppearsInArgs {}
type DroidFriendsReturnType = MP<MPL<MP<CharacterReturnType>>>;
type DroidIdReturnType = MP<string>;
type DroidNameReturnType = MP<string>;
type DroidPrimaryFunctionReturnType = MP<string>;
interface DroidRootType {
appearsIn: Episode[];
friends: any[];
id: string;
name: string;
primaryFunction?: string;
}
export interface Generated_Type_Query {
backingType: unknown;
fields: Generated_Type_Query_Fields;
interface DroidReturnType {
appearsIn: Episode[];
friends: any[];
id: string;
name: string;
primaryFunction?: string;
}
export interface Generated_Type_Droid_Field_primaryFunction {
returnType: string | PromiseLike<string>;
args: {};
type CharacterAppearsInReturnType = MP<MPL<MP<Episode>>>;
export interface CharacterAppearsInArgs {
id: string;
}
export interface Generated_Type_Droid_Fields
extends Generated_Interface_Character_Fields {
primaryFunction: Generated_Type_Droid_Field_primaryFunction;
type CharacterFriendsReturnType = MP<MPL<MP<CharacterReturnType>>>;
type CharacterIdReturnType = MP<string>;
type CharacterNameReturnType = MP<string>;
export type Episode = 5 | 6 | 4;
type HumanAppearsInReturnType = MP<MPL<MP<Episode>>>;
export interface HumanAppearsInArgs extends CharacterAppearsInArgs {}
type HumanFriendsReturnType = MP<MPL<MP<CharacterReturnType>>>;
type HumanHomePlanetReturnType = MP<null | string>;
type HumanIdReturnType = MP<string>;
type HumanNameReturnType = MP<string>;
type HumanRootType = swapi.Human;
type HumanReturnType = swapi.Human
export type MoreEpisodes = 5 | 6 | 4 | "OTHER";
type CharacterRootType = DroidRootType | HumanRootType
type CharacterReturnType = DroidReturnType | HumanReturnType
interface GQLiteralGenArgTypes {
Query: {
droid: QueryDroidArgs;
hero: QueryHeroArgs;
human: QueryHumanArgs;
};
Droid: {
appearsIn: DroidAppearsInArgs;
};
Character: {
appearsIn: CharacterAppearsInArgs;
};
Human: {
appearsIn: HumanAppearsInArgs;
};
}
export interface Generated_Type_Droid {
backingType: swapi.Droid;
fields: Generated_Type_Droid_Fields;
interface GQLiteralGenRootTypes {
Character: CharacterRootType;
Query: QueryRootType;
Droid: DroidRootType;
Human: HumanRootType;
}
export interface Generated_Type_Human_Field_homePlanet {
returnType: null | string | PromiseLike<null | string>;
args: {};
interface GQLiteralGenReturnTypes {
Query: {
droid: QueryDroidReturnType;
hero: QueryHeroReturnType;
human: QueryHumanReturnType;
};
Droid: {
appearsIn: DroidAppearsInReturnType;
friends: DroidFriendsReturnType;
id: DroidIdReturnType;
name: DroidNameReturnType;
primaryFunction: DroidPrimaryFunctionReturnType;
};
Character: {
appearsIn: CharacterAppearsInReturnType;
friends: CharacterFriendsReturnType;
id: CharacterIdReturnType;
name: CharacterNameReturnType;
};
Human: {
appearsIn: HumanAppearsInReturnType;
friends: HumanFriendsReturnType;
homePlanet: HumanHomePlanetReturnType;
id: HumanIdReturnType;
name: HumanNameReturnType;
};
}
export interface Generated_Type_Human_Fields
extends Generated_Interface_Character_Fields {
homePlanet: Generated_Type_Human_Field_homePlanet;
interface GQLiteralGenTypes {
argTypes: GQLiteralGenArgTypes;
rootTypes: GQLiteralGenRootTypes;
returnTypes: GQLiteralGenReturnTypes;
}
export interface Generated_Type_Human {
backingType: swapi.Human;
fields: Generated_Type_Human_Fields;
}
export interface GeneratedScalars {}
export interface GeneratedInterfaces {
Character: Generated_Interface_Character;
}
export interface GeneratedUnions {}
export interface GeneratedEnums {
Episode: 5 | 6 | 4;
MoreEpisodes: 5 | 6 | 4 | "OTHER";
}
export interface GeneratedInputObjects {}
export interface GeneratedObjects {
Query: Generated_Type_Query;
Droid: Generated_Type_Droid;
Human: Generated_Type_Human;
}
export interface GeneratedSchema {
context: {};
enums: GeneratedEnums;
objects: GeneratedObjects;
inputObjects: GeneratedInputObjects;
unions: GeneratedUnions;
scalars: GeneratedScalars;
interfaces: GeneratedInterfaces;
allInputTypes:
| Extract<keyof GeneratedInputObjects, string>
| Extract<keyof GeneratedEnums, string>
| Extract<keyof GeneratedScalars, string>;
allOutputTypes:
| Extract<keyof GeneratedObjects, string>
| Extract<keyof GeneratedEnums, string>
| Extract<keyof GeneratedUnions, string>
| Extract<keyof GeneratedInterfaces, string>
| Extract<keyof GeneratedScalars, string>;
}
export type Gen = GeneratedSchema;
export type Gen = GQLiteralGenTypes;
declare global {
interface GQLiteralGen extends GeneratedSchema {}
interface GQLiteralGen extends GQLiteralGenTypes {}
}

View File

@ -8,15 +8,13 @@ import { GQLiteralSchema } from "gqliteral";
*/
export const schema = GQLiteralSchema({
types: allTypes,
definitionFilePath: path.join(__dirname, "../../star-wars-schema.graphql"),
typeGeneration: {
typesFilePath: path.join(__dirname, "../generatedTypes.ts"),
imports: {
swapi: path.join(__dirname, "backingTypes.ts"),
},
backingTypes: {
Droid: "swapi.Droid",
Human: "swapi.Human",
},
schemaPath: path.join(__dirname, "../../star-wars-schema.graphql"),
typegenPath: path.join(__dirname, "../generatedTypes.ts"),
typegenImports: {
swapi: path.join(__dirname, "rootTypes.ts"),
},
rootTypes: {
// Droid: "swapi.Droid",
Human: "swapi.Human",
},
});

View File

@ -8,6 +8,7 @@
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"skipLibCheck": true
"skipLibCheck": true,
"sourceMap": true
}
}

View File

@ -1,6 +1,5 @@
import {
defaultFieldResolver,
DirectiveLocationEnum,
GraphQLBoolean,
GraphQLDirective,
GraphQLEnumType,
@ -33,11 +32,14 @@ import {
isObjectType,
isOutputType,
isUnionType,
GraphQLSchema,
} from "graphql";
import { GQLiteralTypeWrapper } from "./definitions";
import { GQLiteralMetadata } from "./metadata";
import { GQLiteralAbstract, GQLiteralDirectiveType } from "./objects";
import * as Types from "./types";
import { propertyFieldResolver, suggestionList } from "./utils";
import { propertyFieldResolver, suggestionList, objValues } from "./utils";
import { isObject } from "util";
const isPromise = (val: any): val is Promise<any> =>
Boolean(val && typeof val.then === "function");
@ -80,7 +82,8 @@ export class SchemaBuilder {
*/
protected definedTypeMap: Record<string, GraphQLNamedType> = {};
/**
* The "pending type" map keeps track of all types that
* The "pending type" map keeps track of all types that were defined w/
* GQLiteral and haven't been processed into concrete types yet.
*/
protected pendingTypeMap: Record<string, GQLiteralTypeWrapper> = {};
protected pendingDirectiveMap: Record<
@ -88,23 +91,20 @@ export class SchemaBuilder {
GQLiteralDirectiveType<any>
> = {};
protected directiveMap: Record<string, GraphQLDirective> = {};
protected directiveUseMap: {
[K in DirectiveLocationEnum]?: Types.DirectiveUse
} = {};
constructor(
protected schemaConfig: Pick<
Types.SchemaConfig<any>,
"nullability" | "defaultResolver"
>
protected metadata: GQLiteralMetadata,
protected nullability: Types.NullabilityConfig = {}
) {}
addType(typeDef: GQLiteralTypeWrapper | GraphQLNamedType) {
if (this.finalTypeMap[typeDef.name] || this.pendingTypeMap[typeDef.name]) {
throw new Error(`Named type ${typeDef.name} declared more than once`);
throw extendError(typeDef.name);
}
if (isNamedType(typeDef)) {
this.metadata.addExternalType(typeDef);
this.finalTypeMap[typeDef.name] = typeDef;
this.definedTypeMap[typeDef.name] = typeDef;
} else {
this.pendingTypeMap[typeDef.name] = typeDef;
}
@ -125,17 +125,17 @@ export class SchemaBuilder {
if (this.finalTypeMap[key]) {
return;
}
if (this.definedTypeMap[key]) {
throw extendError(key);
}
this.finalTypeMap[key] = this.getOrBuildType(key);
this.buildingTypes.clear();
});
Object.keys(this.pendingDirectiveMap).forEach((key) => {});
return {
types: this.finalTypeMap,
directives: {
definitions: [],
uses: {},
hasUses: false,
},
typeMap: this.finalTypeMap,
metadata: this.metadata,
directiveMap: this.directiveMap,
};
}
@ -155,6 +155,7 @@ export class SchemaBuilder {
}
objectType(config: Types.ObjectTypeConfig) {
this.metadata.addObjectType(config);
return new GraphQLObjectType({
name: config.name,
interfaces: () => config.interfaces.map((i) => this.getInterface(i)),
@ -166,6 +167,9 @@ export class SchemaBuilder {
);
allInterfaces.forEach((i) => {
const iFields = i.getFields();
// We need to take the interface fields and reconstruct them
// this actually simplifies things becuase if we've modified
// the field at all it needs to happen here.
Object.keys(iFields).forEach((iFieldName) => {
const { isDeprecated, args, ...rest } = iFields[iFieldName];
interfaceFields[iFieldName] = {
@ -255,7 +259,7 @@ export class SchemaBuilder {
description: val.description,
deprecationReason: val.deprecationReason,
value: val.value,
astNode: val.astNode,
// astNode: val.astNode,
};
});
}
@ -358,7 +362,7 @@ export class SchemaBuilder {
}
protected mixAbstractOuput(
typeConfig: Types.InputTypeConfig,
typeConfig: Types.ObjectTypeConfig | Types.InterfaceTypeConfig,
fieldMap: GraphQLFieldConfigMap<any, any>,
type: GQLiteralAbstract<any>,
{ pick, omit }: Types.MixOpts<any>
@ -397,6 +401,7 @@ export class SchemaBuilder {
fieldConfig: Types.FieldConfig,
typeConfig: Types.ObjectTypeConfig | Types.InterfaceTypeConfig
): GraphQLFieldConfig<any, any> {
this.metadata.addField(typeConfig.name, fieldConfig);
return {
type: this.decorateOutputType(
this.getOutputType(fieldConfig.type),
@ -516,7 +521,7 @@ export class SchemaBuilder {
let finalType = type;
const nullConfig: typeof NULL_DEFAULTS = {
...NULL_DEFAULTS,
...this.schemaConfig.nullability,
...this.nullability,
...typeConfig.nullability,
};
const { list, nullable, listDepth, listItemNullable } = fieldConfig;
@ -560,7 +565,6 @@ export class SchemaBuilder {
"listItemNullable should only be set with list: true, this option is ignored"
);
}
if (!isNullable) {
return GraphQLNonNull(finalType);
}
@ -663,10 +667,7 @@ export class SchemaBuilder {
fieldOptions: Types.FieldConfig,
typeConfig: Types.ObjectTypeConfig | Types.InterfaceTypeConfig
) {
let resolver =
typeConfig.defaultResolver ||
this.schemaConfig.defaultResolver ||
defaultFieldResolver;
let resolver = typeConfig.defaultResolver || defaultFieldResolver;
if (fieldOptions.resolve) {
if (typeof fieldOptions.property !== "undefined") {
console.warn(
@ -706,3 +707,83 @@ function withDefaultValue(
return result;
};
}
function extendError(name: string) {
return new Error(
`${name} was already defined as a GraphQL type, check the docs for extending`
);
}
/**
* Builds the types, normalizing the "types" passed into the schema for a
* better developer experience
*/
export function buildTypes(
types: any,
config: Types.Omit<Types.SchemaConfig<any>, "types"> = {
schemaPath: false,
typegenPath: false,
}
): Types.BuildTypes {
const metadata = new GQLiteralMetadata(config);
const builder = new SchemaBuilder(metadata, config.nullability);
addTypes(builder, types);
return builder.getFinalTypeMap();
}
function addTypes(builder: SchemaBuilder, types: any) {
if (!types) {
return;
}
if (types instanceof GQLiteralTypeWrapper || isNamedType(types)) {
builder.addType(types);
} else if (types instanceof GQLiteralDirectiveType || isDirective(types)) {
builder.addDirective(types);
} else if (Array.isArray(types)) {
types.forEach((typeDef) => addTypes(builder, typeDef));
} else if (isObject(types)) {
Object.keys(types).forEach((key) => addTypes(builder, types[key]));
}
}
/**
* Builds the schema, returning both the schema and metadata.
*/
export function buildSchemaWithMetadata<GenTypes = GQLiteralGen>(
options: Types.SchemaConfig<GenTypes>
) {
const { typeMap: typeMap, directiveMap: directiveMap, metadata } = buildTypes(
options.types,
options
);
const { Query, Mutation, Subscription } = typeMap;
if (!isObjectType(Query)) {
throw new Error("You must supply a Query type to create a valid schema");
}
if (Mutation && !isObjectType(Mutation)) {
throw new Error(
`Expected Mutation to be a GraphQLObjectType, saw ${
Mutation.constructor.name
}`
);
}
if (Subscription && !isObjectType(Subscription)) {
throw new Error(
`Expected Subscription to be a GraphQLObjectType, saw ${
Subscription.constructor.name
}`
);
}
const schema = new GraphQLSchema({
query: Query,
mutation: Mutation,
subscription: Subscription,
directives: objValues(directiveMap),
types: objValues(typeMap),
});
metadata.finishConstruction();
return { schema, metadata };
}

View File

@ -1,23 +1,17 @@
import { assertValidName, GraphQLScalarType } from "graphql";
import {
GraphQLSchema,
GraphQLNamedType,
isObjectType,
GraphQLScalarType,
assertValidName,
} from "graphql";
import * as Types from "./types";
import {
GQLiteralNamedType,
GQLiteralObjectType,
GQLiteralInterfaceType,
GQLiteralAbstract,
GQLiteralDirectiveType,
GQLiteralEnumType,
GQLiteralInputObjectType,
GQLiteralAbstract,
GQLiteralInterfaceType,
GQLiteralNamedType,
GQLiteralObjectType,
GQLiteralUnionType,
GQLiteralDirectiveType,
} from "./objects";
import { enumShorthandMembers, buildTypes } from "./utils";
import { runBuildArtifacts } from "./typebuilder";
import * as Types from "./types";
import { enumShorthandMembers } from "./utils";
import { buildSchemaWithMetadata } from "./builder";
/**
* Wraps a GQLiteralType object, since all GQLiteral types have a
@ -224,40 +218,16 @@ export function GQLiteralDirective<
export function GQLiteralSchema<GenTypes = GQLiteralGen>(
options: Types.SchemaConfig<GenTypes>
) {
const { types: typeMap, directives } = buildTypes(options.types, options);
const { Query, Mutation, Subscription } = typeMap;
if (!isObjectType(Query)) {
throw new Error("You must supply a Query type to create a valid schema");
}
if (Mutation && !isObjectType(Mutation)) {
throw new Error(
`Expected Mutation to be a GraphQLObjectType, saw ${
Mutation.constructor.name
}`
);
}
if (Subscription && !isObjectType(Subscription)) {
throw new Error(
`Expected Subscription to be a GraphQLObjectType, saw ${
Subscription.constructor.name
}`
);
}
const schema = new GraphQLSchema({
query: Query,
mutation: Mutation,
subscription: Subscription,
directives: directives.definitions,
types: Object.keys(typeMap).reduce((result: GraphQLNamedType[], key) => {
result.push(typeMap[key]);
return result;
}, []),
});
const { schema, metadata } = buildSchemaWithMetadata<GenTypes>(options);
// Only in development envs do we want to worry about regenerating the
// schema definition and/or generated types.
if (process.env.NODE_ENV !== "production") {
runBuildArtifacts<GenTypes>(schema, options, directives);
const {
shouldGenerateArtifacts = process.env.NODE_ENV !== "production",
} = options;
if (shouldGenerateArtifacts) {
metadata.generateArtifacts(schema);
}
return schema;

View File

@ -1,10 +0,0 @@
/**
* Any extended GraphQL types
*/
import { GraphQLSchema, GraphQLSchemaConfig } from "graphql";
export class GraphQLSchema_GQLiteral extends GraphQLSchema {
constructor(props: GraphQLSchemaConfig) {
super(props);
}
}

View File

@ -1,3 +1,5 @@
import * as GQLiteralTypes from "./types";
export { GQLiteralTypes };
export {
GQLiteralArg,
GQLiteralObject,
@ -10,5 +12,4 @@ export {
GQLiteralAbstractType,
GQLiteralDirective,
} from "./definitions";
import * as GQLiteralTypes from "./types";
export { GQLiteralTypes };
export { buildSchemaWithMetadata, buildTypes } from "./builder";

26
src/lang.ts Normal file
View File

@ -0,0 +1,26 @@
import { dedent } from "./utils";
export const SDL_HEADER = dedent`
### ---
### This file was autogenerated by gqliteral
### Do not edit the contents directly
### ---
`;
export const TYPEGEN_HEADER = dedent`
/**
* This file is automatically generated not make changes directly
* This fill will regenerate when the server is started
* and NODE_ENV !== "production".
*
* These types are only for internal use by gqliteral and subject
* to change. The "*Args" / InputObject types are the only types
* intended for external usage.
*
* If you want more robust types to use in your code, look into one of:
*
* https://github.com/dotansimha/graphql-code-generator
* https://github.com/prisma/graphqlgen
* https://github.com/apollographql/apollo-tooling
*/
`;

254
src/metadata.ts Normal file
View File

@ -0,0 +1,254 @@
import {
DirectiveLocationEnum,
GraphQLNamedType,
GraphQLSchema,
lexicographicSortSchema,
visit,
parse,
printSchema,
GraphQLObjectType,
} from "graphql";
import path from "path";
import * as Types from "./types";
import { SDL_HEADER } from "./lang";
import { eachObj } from "./utils";
import { buildTypeDefinitions } from "./typegen";
export interface DirectiveUse {
location: DirectiveLocationEnum;
typeName: string;
args: [];
argName?: string;
fieldName?: string;
}
/**
* Used in the schema builder, this keeps track of any necessary
* field / type metadata we need to be aware of when building the schema.
*
* - Directive usage
* - Type backing value
* - Type default resolver
* - Field property
* - Field resolver
* - Field defaultValue
* - Whether the type is outside of
*/
export class GQLiteralMetadata {
protected completed = false;
protected objectMeta: Record<string, Types.ObjectTypeConfig> = {};
protected interfaceMeta: Record<string, Types.InterfaceTypeConfig> = {};
protected objectFieldMeta: Record<
string,
Record<string, Types.FieldConfig>
> = {};
protected metaTypeMap: Record<string, any> = {};
protected rootTypeMap: Types.RootTypeMap = {};
protected rootImportMapping: Record<string, [string, string]> = {}; // Typename, Alias
constructor(protected config: Types.Omit<Types.SchemaConfig<any>, "types">) {
eachObj(
config.rootTypes || {},
(val, typeName) => val && this.addRootType(typeName, val)
);
}
finishConstruction() {
this.completed = true;
}
// Predicates:
isExternalType(typeName: string) {
return Boolean(this.objectMeta[typeName]);
}
hasResolver(typeName: string, fieldName: string) {
if (this.isFieldModified(typeName, fieldName)) {
}
return Boolean(
this.objectFieldMeta[typeName] &&
this.objectFieldMeta[typeName][fieldName] &&
this.objectFieldMeta[typeName][fieldName].resolve
);
}
hasDefaultResolver(typeName: string) {
return Boolean(
this.objectMeta[typeName] && this.objectMeta[typeName].defaultResolver
);
}
hasPropertyResolver(typeName: string, fieldName: string) {
return Boolean(
this.objectFieldMeta[typeName] &&
this.objectFieldMeta[typeName][fieldName] &&
this.objectFieldMeta[typeName][fieldName].property
);
}
hasDefaultValue(typeName: string, fieldName: string) {
return Boolean(
this.objectFieldMeta[typeName] &&
this.objectFieldMeta[typeName][fieldName] &&
this.objectFieldMeta[typeName][fieldName].default
);
}
/**
* Whether we have a used defined type for the "rootValue" of a type
*/
hasRootTyping(typeName: string) {
return Boolean(this.rootTypeMap[typeName]);
}
/**
* Whether we have a dedicated typing for the scalar
*/
hasScalarTyping(typeName: string) {
return Boolean(this.rootTypeMap[typeName]);
}
getRootTyping(typeName: string) {
return this.rootTypeMap[typeName];
}
isFieldModified(typeName: string, fieldName: string) {
return Boolean(
this.objectMeta[typeName] &&
this.objectMeta[typeName].fieldModifications[fieldName]
);
}
isInterfaceField(type: GraphQLObjectType, fieldName: string) {
return Boolean(
type.getInterfaces().forEach((i) => i.getFields()[fieldName])
);
}
// Type Genreation Helpers:
getPropertyResolver(typeName: string, fieldName: string) {
return (
this.objectFieldMeta[typeName] &&
this.objectFieldMeta[typeName][fieldName].property
);
}
// Schema construction helpers:
addRootType(typeName: string, val: string | Types.ImportedType) {
if (this.rootTypeMap[typeName]) {
console.warn(
`Root Type ${JSON.stringify(
this.rootTypeMap[typeName]
)} already exists for ${typeName} and is being replaced with ${JSON.stringify(
val
)}`
);
}
this.rootTypeMap[typeName] = val;
}
addScalar(config: Types.ScalarOpts) {
this.checkMutable();
}
addInterfaceType(config: Types.InterfaceTypeConfig) {
this.checkMutable();
this.interfaceMeta[config.name] = config;
}
addObjectType(config: Types.ObjectTypeConfig) {
this.checkMutable();
this.objectMeta[config.name] = config;
}
addExternalType(type: GraphQLNamedType) {
this.checkMutable();
}
addField(typeName: string, field: Types.FieldConfig) {
this.checkMutable();
this.objectFieldMeta[typeName] = this.objectFieldMeta[typeName] || {};
this.objectFieldMeta[typeName][field.name] = field;
}
/**
* Generates the artifacts of the build based on what we
* know about the schema and how it was defined.
*/
generateArtifacts(schema: GraphQLSchema) {
const sortedSchema = this.sortSchema(schema);
if (this.config.schemaPath) {
this.writeFile(
"schema",
this.generateSchemaFile(sortedSchema),
this.config.schemaPath
);
}
if (this.config.typegenPath) {
this.writeFile(
"types",
this.generateTypesFile(sortedSchema),
this.config.typegenPath
);
}
}
sortSchema(schema: GraphQLSchema) {
let sortedSchema = schema;
if (typeof lexicographicSortSchema !== "undefined") {
sortedSchema = lexicographicSortSchema(schema);
}
return sortedSchema;
}
writeFile(name: string, output: string, filePath: string) {
if (typeof filePath !== "string" || !path.isAbsolute(filePath)) {
throw new Error(
`Expected an absolute path to output the GQLiteral ${name}, saw ${filePath}`
);
}
const fs = require("fs") as typeof import("fs");
fs.writeFile(filePath, output, (err) => {
if (err) {
console.error(err);
}
});
}
/**
* Generates the schema, adding any directives as necessary
*/
generateSchemaFile(schema: GraphQLSchema): string {
let printedSchema = printSchema(schema);
/**
* If there are directives defined to be used on the types,
* we need to add these manually to the AST. Directives shouldn't
* be too common, since we're defining the schema programatically
* rather than by hand.
*/
if (Object.keys({}).length > 0) {
printedSchema = printSchema(
visit(parse(printedSchema), {
// TODO: Add directives
})
);
}
return [SDL_HEADER, printedSchema].join("\n\n");
}
/**
* Generates the type definitions
*/
generateTypesFile(schema: GraphQLSchema): string {
return buildTypeDefinitions(schema, this);
}
protected checkMutable() {
if (this.completed) {
throw new Error("Metadata cannot be modified after the schema is built");
}
}
}

View File

@ -6,7 +6,6 @@ import {
GraphQLUnionType,
GraphQLEnumType,
GraphQLIsTypeOfFn,
GraphQLResolveInfo,
DirectiveLocationEnum,
assertValidName,
} from "graphql";
@ -215,6 +214,7 @@ export class GQLiteralObjectType<
fields: [],
interfaces: [],
directives: [],
fieldModifications: {},
};
}
@ -295,16 +295,6 @@ export class GQLiteralObjectType<
...options,
},
});
return {
resolver(
fn: (
root: Types.RootValue<GenTypes, TypeName>,
args: Types.ArgsValue<GenTypes, TypeName, FieldName>,
context: Types.ContextValue<GenTypes>,
info: GraphQLResolveInfo
) => Types.ResultValue<GenTypes, TypeName, FieldName>
): void {},
};
}
/**
@ -338,9 +328,9 @@ export class GQLiteralObjectType<
*/
modify<FieldName extends Types.ObjectTypeFields<GenTypes, TypeName>>(
field: FieldName,
options?: Types.ModifyFieldOpts<GenTypes, TypeName, FieldName>
options: Types.ModifyFieldOpts<GenTypes, TypeName, FieldName>
) {
throw new Error("TODO");
this.typeConfig.fieldModifications[field as string] = options;
}
/**
@ -390,10 +380,10 @@ export class GQLiteralObjectType<
* to the shape of the type.
*
* Note: This value can also be set when building the schema via
* the "backingTypes" option to `GQLiteralSchema`
* the "rootTypes" option to `GQLiteralSchema`
*/
backingType(typeImport: Types.ImportedType) {
this.typeConfig.backingType = typeImport;
rootType(typeImport: Types.ImportedType) {
this.typeConfig.rootType = typeImport;
}
/**
@ -528,6 +518,18 @@ export class GQLiteralInterfaceType<
});
}
/**
* Supply the default field resolver for all members of this interface
*/
defaultResolver(
resolverFn: GraphQLFieldResolver<
Types.RootValue<GenTypes, TypeName>,
Types.ContextValue<GenTypes>
>
) {
this.typeConfig.defaultResolver = resolverFn;
}
/**
* Internal use only. Creates the configuration to create
* the GraphQL named type.

View File

@ -1,427 +0,0 @@
import {
GraphQLSchema,
GraphQLNamedType,
GraphQLObjectType,
GraphQLInputObjectType,
GraphQLArgs,
GraphQLUnionType,
GraphQLInterfaceType,
GraphQLScalarType,
isObjectType,
isInputObjectType,
isScalarType,
GraphQLEnumType,
isUnionType,
isInterfaceType,
isEnumType,
isNamedType,
GraphQLInputType,
isNonNullType,
isListType,
GraphQLOutputType,
GraphQLField,
GraphQLArgument,
GraphQLInputField,
printSchema,
lexicographicSortSchema,
} from "graphql";
import { dedent, addDirectives } from "./utils";
import * as Types from "./types";
const SCALAR_TYPES = {
Int: "number",
String: "string",
ID: "string",
Float: "number",
Date: "Date",
JSON: "any",
Boolean: "boolean",
};
export interface TypeBuilderOptions {
/**
* An array of additional strings to prefix on the header of the TS file
*/
prefix?: string[];
}
/**
* Unlike other codegen tools, we're only interested in 3 things here:
*
* 1. What are the "backing values" that are used in a resolver for a type.
* 2. What are possible arguments for any output fields.
* 3. What are valid "return values" for a resolver given the type it needs to resolve.
*
* We won't export all of these generated type, we will only use them for
* completion. If you want types to use in your code, look into one of:
*
* https://github.com/dotansimha/graphql-code-generator
* https://github.com/prisma/graphqlgen
* https://github.com/apollographql/apollo-tooling
*/
export class TypeBuilder {
protected objectTypes = new Set<GraphQLObjectType>();
protected inputObjectTypes = new Set<GraphQLInputObjectType>();
protected unionTypes = new Set<GraphQLUnionType>();
protected interfaceTypes = new Set<GraphQLInterfaceType>();
protected scalarTypes = new Set<GraphQLScalarType>();
protected enumTypes = new Set<GraphQLEnumType>();
protected scalarMapping: Record<string, string> = SCALAR_TYPES;
constructor(
protected schema: GraphQLSchema,
protected options: TypeBuilderOptions = {}
) {
Object.keys(schema.getTypeMap()).forEach((typeName) => {
if (typeName.indexOf("__") === 0) {
return;
}
// All types will be "unused" until we say otherwise,
// if we need to strip them.
const type = schema.getType(typeName);
if (isObjectType(type)) {
// If it's an object type, it potentially has a backing
// value, which is either set on the field or on the schema.
this.objectTypes.add(type);
} else if (isInputObjectType(type)) {
this.inputObjectTypes.add(type);
} else if (isScalarType(type)) {
this.scalarTypes.add(type);
} else if (isUnionType(type)) {
this.unionTypes.add(type);
} else if (isInterfaceType(type)) {
this.interfaceTypes.add(type);
} else if (isEnumType(type)) {
this.enumTypes.add(type);
}
});
}
/**
* Formats the field name for adding to `${fieldName}${typeName}Args`
* or `${fieldName}${typeName}Meta`
*/
formatFieldNameForType(fieldName: string) {
return fieldName
.slice(0, 1)
.toUpperCase()
.concat(fieldName.slice(1));
}
generate() {
return dedent`
/* tslint:disable */
/**
* This file is automatically generated not make changes directly
* This fill will regenerate when the server is started
* and NODE_ENV !== "production".
*
* These types are only for internal use by gqliteral and subject
* to change. The "*Args" / InputObject types are the only types
* intended for external usage.
*
* If you want more robust types to use in your code, look into one of:
*
* https://github.com/dotansimha/graphql-code-generator
* https://github.com/prisma/graphqlgen
* https://github.com/apollographql/apollo-tooling
*/
${this.options.prefix ? this.options.prefix.join("\n") : ""}
// Maybe Promise
type MP<T> = PromiseLike<T> | T;
// Maybe Promise Array
type MPA<T> = MP<MP<T>[]>
${this.allTypes()}
${this.generateGQLiteral()}
declare global {
interface GQLiteralGen extends GQLiteralGenTypes {}
}
export type Gen = GQLiteralGenTypes;
`;
}
// At some point soon it'd be nice to nix prettier and just
// have well formatted output from the start.
generatePretty() {
try {
require.resolve("prettier");
return require("prettier").format(this.generate(), {
parser: "typescript",
});
} catch (e) {
return this.generate();
}
}
/**
* Generate the argTypes, rootValueTypes, and returnTypes.
*
* Return types are the potential contract that will
* properly fulfill the the field's resolver. If we know the
* backing type specifically, we'll use that, otherwise we can
* infer it by:
*
* 1. If the field is nullable, or has a defaultValue, the type
* isn't required.
*
* 2. If there is a resolver on the backing type, we'll assume the
* field is not needed.
*
* 3. If there is an alias, we'll assume that field should be provided
* instead.
*/
allTypes() {
const argDefs: Record<string, Record<string, GraphQLField<any, any>>> = {};
const argTypes: string[] = [];
const backingTypes: string[] = [];
const returnTypes: string[] = [];
const returnTypeMap: Record<string, string> = {};
const processField = (
field: GraphQLField<any, any>,
type: GraphQLObjectType | GraphQLInterfaceType
) => {
if (field.args.length) {
const argsTypeName = `${this.formatFieldNameForType(field.name)}${
type.name
}Args`;
argDefs[type.name] = argDefs[type.name] || {};
argDefs[type.name][field.name] = field;
if (isObjectType(type)) {
const predefined = type
.getInterfaces()
.map((i) => {
if (argDefs[i.name] && argDefs[i.name][field.name]) {
return `${this.formatFieldNameForType(field.name)}${
i.name
}Args`;
}
})
.filter((f) => f);
if (predefined.length) {
argTypes.push(
`interface ${argsTypeName} extends ${predefined.join(", ")} {}`
);
return;
}
}
argTypes.push(
wrapInterface(
argsTypeName,
field.args.map((arg) => this.printArgOrField(arg))
)
);
}
};
this.interfaceTypes.forEach((type) => {
eachObj(type.getFields(), (field) => processField(field, type));
});
this.objectTypes.forEach((type) => {
eachObj(type.getFields(), (field) => processField(field, type));
});
return dedent`
${argTypes.join("\n\n")}
${exp(
wrapInterface(
"GQLiteralGenArgTypes",
mapObj(argDefs, (val, key) => {
return dedent`
${key}: {
${mapObj(val, (field) => {
return `${field.name}: ${this.formatFieldNameForType(
field.name
)}Args;`;
}).join("\n")}
};
`;
})
)
)}
${this.generateEnums()}
${exp(wrapInterface("GQLiteralGenBackingTypes", []))}
${exp(wrapInterface("GQLiteralGenReturnTypes", []))}
`;
}
printReturnField(field: GraphQLField<any, any>): string {
if (isNonNullType(field.type)) {
return `${field.name}: ${this.printReturnType(field.type)}`;
}
return `${field.name}?: ${this.printReturnType(field.type)}`;
}
printReturnType(type: GraphQLOutputType): string {
if (isNonNullType(type)) {
return `MP<${this.printReturnValue(type.ofType)}>`;
}
return `MP<null | ${this.printReturnValue(type)}>`;
}
printReturnValue(type: GraphQLOutputType): string {
if (isListType(type)) {
return `MPA<${this.printReturnType(type.ofType)}>`;
}
return `MP<>`;
}
printArgOrField(arg: GraphQLArgument | GraphQLInputField): string {
if (isNonNullType(arg.type)) {
return `${arg.name}: ${this.printInputType(arg.type)}`;
}
return `${arg.name}?: ${this.printInputType(arg.type)}`;
}
/**
* Input objects don't have arguments, so this should just be a
* simple value mapping.
*/
generateInputObjects() {
return `${map(this.inputObjectTypes, (o) => this.makeInputObject(o))}`;
}
makeInputObject(o: GraphQLInputObjectType) {
return exp(
wrapInterface(
o.name,
mapObj(o.getFields(), (field) => this.printArgOrField(field))
)
);
}
printInputType(type: GraphQLInputType): string {
if (isNonNullType(type)) {
return this.printInputTypeValue(type.ofType);
}
return `null | ${this.printInputTypeValue(type)}`;
}
printInputTypeValue(type: GraphQLInputType): string {
if (isListType(type)) {
return `Array<${this.printInputType(type.ofType)}>`;
}
if (isScalarType(type)) {
return this.scalarValue(type.name);
}
return `${type.name}`;
}
scalarValue(scalarName: string) {
return this.scalarMapping[scalarName] || this.unknown();
}
unknown() {
return "unknown";
}
getBackingType(obj: GraphQLObjectType) {
return this.unknown();
}
generateEnums() {
return dedent`
${map(this.enumTypes, (node) => {
return exp(
wrapType(
node.name,
node.getValues().map((value) => `${JSON.stringify(value.value)}`),
" | "
)
);
})}
`;
}
generateGQLiteral() {
return exp(
wrapInterface("GQLiteralGenTypes", [
"argTypes: GQLiteralGenArgTypes",
"backingTypes: GQLiteralGenBackingTypes",
"returnTypes: GQLiteralGenReturnTypes",
])
);
}
}
function map<T>(
nodes: Set<T> | Array<T>,
iterator: (item: T, index: number) => string,
join = "\n"
) {
return Array.from(nodes)
.map(iterator)
.join(join);
}
function mapObj<T, R>(
obj: Record<string, T>,
mapper: (val: T, key: string, index: number) => R
) {
return Object.keys(obj).map((key, index) => mapper(obj[key], key, index));
}
function eachObj<T>(
obj: Record<string, T>,
iter: (val: T, key: string, index: number) => void
) {
Object.keys(obj).forEach((name, i) => iter(obj[name], name, i));
}
function exp(str: string) {
return `export ${str}`;
}
function wrapInterface(name: string, items: string[], join = "\n"): string {
return [`interface ${name} {`, items.join(join), "}"].join("\n");
}
function wrapType(name: string, items: string[], joinItems = "\n"): string {
return `type ${name} = ${items.join(joinItems)}`;
}
/**
* Depending on the option configuration, builds & saves the
* .graphql schema and .ts files
*/
export function runBuildArtifacts<GenTypes = GQLiteralGen>(
schema: GraphQLSchema,
options: Types.SchemaConfig<GenTypes>,
directives: Types.BuildTypesDirectives
) {
let sortedSchema = schema;
if (typeof lexicographicSortSchema !== "undefined") {
sortedSchema = lexicographicSortSchema(schema);
}
const generatedSchema = addDirectives(printSchema(sortedSchema), directives);
const fs = require("fs");
const header = dedent(`
### ---
### This file was autogenerated by gqliteral
### Do not edit the contents directly
### ---
`);
fs.writeFile(
options.definitionFilePath,
[header, generatedSchema].join("\n\n"),
(err: Error | null) => {
if (err) {
return console.error(err);
}
}
);
if (options.typeGeneration) {
console.log(new TypeBuilder(sortedSchema).generatePretty());
// typegen(options.typeGeneration, sortedSchema).catch((e) => {
// console.error(e);
// });
}
}

View File

@ -1,11 +1,24 @@
import {
GraphQLArgument,
GraphQLField,
GraphQLInputField,
GraphQLInputType,
GraphQLInterfaceType,
GraphQLObjectType,
GraphQLOutputType,
GraphQLSchema,
schemaToTemplateContext,
SchemaTemplateContext,
Field,
Argument,
} from "graphql-codegen-core";
import * as Types from "./types";
isEnumType,
isInputObjectType,
isInterfaceType,
isListType,
isNonNullType,
isObjectType,
isScalarType,
isUnionType,
} from "graphql";
import { TYPEGEN_HEADER } from "./lang";
import { mapObj, eachObj, arrPush } from "./utils";
import { GQLiteralMetadata } from "./metadata";
const SCALAR_TYPES = {
Int: "number",
@ -17,258 +30,421 @@ const SCALAR_TYPES = {
Boolean: "boolean",
};
export async function typegen(
options: Types.GQLiteralTypegenOptions<any>,
schema: GraphQLSchema
type AllTypes =
| "enums"
| "objects"
| "inputObjects"
| "interfaces"
| "scalars"
| "unions";
// This is intentionally concise and self contained, since it shouldn't be
// unnecessarily abstracted and should run only with only a single iteration
// of the schema. The types created here are generally meant for internal use
// by GQLiteral. If we find a need, this could be factored out into the
export function buildTypeDefinitions(
schema: GraphQLSchema,
metadata: GQLiteralMetadata
) {
const fs = require("fs") as typeof import("fs");
const context = schemaToTemplateContext(schema);
const data = await makeTypes(options, context);
await Promise.all(
data.map(async ({ filename, content }) => {
await new Promise((resolve, reject) => {
fs.writeFile(filename, content, (err) => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
})
);
return options;
}
export const makeTypes = (
options: Types.GQLiteralTypegenOptions<any>,
context: SchemaTemplateContext
) => {
const {
typesFilePath,
backingTypes: backing = {},
typeGenPrefix: prefix = "Generated",
prefix: headerPrefix = [],
} = options;
const backingTypes: Record<string, string> = {
const schemaTypeMap = schema.getTypeMap();
const scalarMapping: Record<string, string> = {
...SCALAR_TYPES,
...backing,
};
const tmpl = `
/* tslint:disable */
/**
* This file is automatically generated by gqliteral
* Do not make changes directly
*/
${headerPrefix.join("\n")}
${map(context.interfaces, (i) => {
const typeName = `${prefix}_Interface_${i.name}`;
// Keeping track of all of the types we have for each
// type in the schema.
const typeNames: Record<AllTypes, string[]> = {
enums: [],
objects: [],
inputObjects: [],
interfaces: [],
scalars: [],
unions: [],
};
const allTypeStrings: string[] = [];
return `
export interface ${typeName}_Fields {
${map(i.fields, (f) => fieldDef(typeName, f))}
const rootTypeImports: {
[importPath: string]: [string, string][]; // [importName, alias][]
} = {};
const interfaceRootTypes: {
[interfaceName: string]: string[]; // objectRootType
} = {};
const returnTypeFields: {
[typeName: string]: [string, string][]; // fieldName, returnTypeName
} = {};
const argTypeFields: {
[typeName: string]: [string, string][]; // fieldName, argTypeName
} = {};
// If for some reason the user has types that conflict with the type names,
// rename them.
let MP = "MP";
let MPL = "MPL";
let MT = "MT";
let MTA = "MTA";
if (schema.getType("MP")) {
MP = "_MP";
}
if (schema.getType("MPL")) {
MPL = "_MPL";
}
if (schema.getType("MT")) {
MT = "_MT";
}
if (schema.getType("MTA")) {
MTA = "_MTA";
}
function argFieldName(fieldName: string, typeName: string) {
return `${typeName}${ucFirst(fieldName)}Args`;
}
function makeReturnTypeName(fieldName: string, typeName: string) {
return `${typeName}${ucFirst(fieldName)}ReturnType`;
}
// Takes a type and turns it into a "return type", based
// on nullability and whether it's a list.
function getReturnType(fieldType: GraphQLOutputType): string {
let type = fieldType;
let typeStr = "";
if (isNonNullType(fieldType)) {
type = fieldType.ofType;
} else {
typeStr += "null | ";
}
if (isListType(type)) {
return `${MP}<${typeStr}${MPL}<${getReturnType(type.ofType)}>>`;
}
if (isObjectType(type) || isInterfaceType(type) || isUnionType(type)) {
typeStr += `${type.name}ReturnType`;
} else if (isEnumType(type)) {
typeStr += type.name;
} else if (isScalarType(type)) {
typeStr += scalarMapping[type.name];
}
return `${MP}<${typeStr}>`;
}
function printInputType(fieldType: GraphQLInputType): string {
let type = fieldType;
let typeStr = "";
if (isNonNullType(fieldType)) {
type = fieldType.ofType;
} else {
typeStr += "null | ";
}
if (isListType(type)) {
return typeStr
? `Array<${typeStr}${printInputType(type.ofType)}>`
: `${printInputType(type.ofType)}[]`;
}
if (isInputObjectType(type) || isEnumType(type)) {
return type.name;
}
if (isScalarType(type)) {
return scalarMapping[type.name] || "unknown";
}
throw new Error(`Unexpected type ${type}`);
}
function printArgOrFieldMember({
name,
type,
}: GraphQLArgument | GraphQLInputField): string {
if (isNonNullType(type)) {
return ` ${name}: ${printInputType(type)};`;
}
return ` ${name}?: ${printInputType(type)};`;
}
function makeFieldArgs(argTypeName: string, field: GraphQLField<any, any>) {
allTypeStrings.push(
[
`export interface ${argTypeName} {`,
map(field.args, (arg) => printArgOrFieldMember(arg)),
"}",
].join("\n")
);
}
function processField(
type: GraphQLObjectType | GraphQLInterfaceType,
field: GraphQLField<any, any>
) {
const returnTypeName = makeReturnTypeName(field.name, type.name);
arrPush(returnTypeFields, type.name, [field.name, returnTypeName]);
allTypeStrings.push(
`type ${returnTypeName} = ${getReturnType(field.type)};`
);
if (field.args.length) {
const argTypeName = argFieldName(field.name, type.name);
arrPush(argTypeFields, type.name, [field.name, argTypeName]);
if (isObjectType(type)) {
const interfaces = type
.getInterfaces()
.filter((i) => i.getFields()[field.name])
.map((i) => argFieldName(field.name, i.name));
if (interfaces.length) {
allTypeStrings.push(
`export interface ${argTypeName} extends ${interfaces.join(
", "
)} {}`
);
} else {
makeFieldArgs(argTypeName, field);
}
} else {
makeFieldArgs(argTypeName, field);
}
}
}
export interface ${typeName} {
implementingTypes: ${map(i.implementingTypes, (t) => `"${t}"`, "|")};
backingType: ${map(i.implementingTypes, backingType, "|")};
fields: ${typeName}_Fields
};
`;
})}
function fieldRootType(fieldType: GraphQLOutputType): string {
let type = fieldType;
let typeStr = "";
if (isNonNullType(fieldType)) {
type = fieldType.ofType;
} else {
typeStr += "null | ";
}
if (isListType(type)) {
const toWrap = fieldRootType(type.ofType);
return toWrap.indexOf("null | ") === 0
? `${typeStr}Array<${toWrap}>`
: `${typeStr}${toWrap}[]`;
}
if (isScalarType(type)) {
return `${typeStr}${scalarMapping[type.name] || "any"}`;
}
if (isObjectType(type)) {
// return field.
}
if (isEnumType(type)) {
return `${typeStr}${type.name}`;
}
return `${typeStr}any`;
}
${map(context.types, (t) => {
const typeName = `${prefix}_Type_${t.name}`;
return `
${map(nonInterfaceFields(context, t), (f) => {
const args = f.hasArguments ? `${typeName}_Field_${f.name}_Args` : "{}";
return `
export interface ${typeName}_Field_${f.name} {
returnType: ${getReturnType(f)};
args: ${args};
}
${
f.hasArguments
? `
export interface ${typeName}_Field_${f.name}_Args {
${map(f.arguments, (arg) => {
const name = `${arg.name}${arg.isRequired ? ":" : "?:"}`;
return `${name} ${getArgType(arg)}`;
})}
}
`
: ""
}
`;
})}
function fieldBackingName(typeName: string, field: GraphQLField<any, any>) {
const colon = metadata.hasDefaultValue(typeName, field.name)
? "?:"
: isNonNullType(field.type)
? ":"
: "?:";
if (metadata.hasPropertyResolver(typeName, field.name)) {
return `${metadata.getPropertyResolver(typeName, field.name)}${colon}`;
}
return `${field.name}${colon}`;
}
export interface ${typeName}_Fields ${interfaceFields(prefix, t)} {
${map(
nonInterfaceFields(context, t),
(f) => `${f.name}: ${typeName}_Field_${f.name}`
)}
function getOrMakeRootType(type: GraphQLObjectType) {
if (metadata.hasRootTyping(type.name)) {
allTypeStrings.push(
`type ${type.name}RootType = ${metadata.getRootTyping(type.name)};`
);
allTypeStrings.push(
`type ${type.name}ReturnType = ${metadata.getRootTyping(type.name)}`
);
} else {
const rootMembers = mapObj(type.getFields(), (f) => {
if (metadata.hasResolver(type.name, f.name)) {
return null;
}
return ` ${fieldBackingName(type.name, f)} ${fieldRootType(f.type)};`;
}).filter((f) => f);
if (rootMembers.length === 0) {
allTypeStrings.push(`type ${type.name}RootType = {};`);
} else {
allTypeStrings.push(
[
`interface ${type.name}RootType {`,
rootMembers.join("\n"),
`}`,
].join("\n")
);
}
export interface ${typeName} {
backingType: ${backingType(t.name)};
fields: ${typeName}_Fields
};
`;
})}
export interface ${prefix}Scalars {
${map(context.scalars, (scalar) => `${scalar.name}: any;`)}
}
export interface ${prefix}Interfaces {
${map(
context.interfaces,
(i) => `${i.name}: ${prefix}_Interface_${i.name}`
)}
const returnMembers = mapObj(type.getFields(), (f) => {
if (metadata.hasResolver(type.name, f.name)) {
return null;
}
return ` ${fieldBackingName(type.name, f)} ${fieldRootType(f.type)};`;
}).filter((f) => f);
if (returnMembers.length === 0) {
allTypeStrings.push(`type ${type.name}ReturnType = {};`);
} else {
allTypeStrings.push(
[
`interface ${type.name}ReturnType {`,
returnMembers.join("\n"),
`}`,
].join("\n")
);
}
}
}
export interface ${prefix}Unions {}
export interface ${prefix}Enums {
${map(context.enums, (e) => {
return `${e.name}: ${map(
e.values,
(v) => JSON.stringify(v.value),
"|"
)};`;
})}
}
export interface ${prefix}InputObjects {
${map(context.inputTypes, (i) => {
return `${i.name}: any; // TODO!!`;
})}
}
export interface ${prefix}Objects {
${map(context.types, (t) => `${t.name}: ${prefix}_Type_${t.name}`)}
}
Object.keys(schemaTypeMap).forEach((typeName) => {
if (typeName.indexOf("__") === 0) {
return;
}
// All types will be "unused" until we say otherwise,
// if we need to strip them.
const type = schema.getType(typeName);
export interface ${prefix}Schema {
context: ${options.contextType || "{}"};
enums: ${prefix}Enums;
objects: ${prefix}Objects;
inputObjects: ${prefix}InputObjects;
unions: ${prefix}Unions;
scalars: ${prefix}Scalars;
interfaces: ${prefix}Interfaces;
allInputTypes:
| Extract<keyof ${prefix}InputObjects, string>
| Extract<keyof ${prefix}Enums, string>
| Extract<keyof ${prefix}Scalars, string>;
allOutputTypes:
| Extract<keyof ${prefix}Objects, string>
| Extract<keyof ${prefix}Enums, string>
| Extract<keyof ${prefix}Unions, string>
| Extract<keyof ${prefix}Interfaces, string>
| Extract<keyof ${prefix}Scalars, string>;
}
export type Gen = ${prefix}Schema;
declare global {
interface GQLiteralGen extends ${prefix}Schema {}
}
`;
const prettier = require("prettier");
const content = prettier.format(tmpl, {
parser: "typescript",
// An object type has a "backing type", and fields
// which may have an "arg type" and have a "return type"
if (isObjectType(type)) {
typeNames.objects.push(type.name);
eachObj(type.getFields(), (field) => processField(type, field));
type
.getInterfaces()
.forEach((i) => arrPush(interfaceRootTypes, i.name, type.name));
getOrMakeRootType(type);
} else if (isInputObjectType(type)) {
typeNames.inputObjects.push(type.name);
allTypeStrings.push(
[
`interface ${type.name} {`,
mapObj(type.getFields(), (inputField) =>
printArgOrFieldMember(inputField)
).join("\n"),
`}`,
].join("\n")
);
} else if (isScalarType(type)) {
typeNames.scalars.push(type.name);
} else if (isUnionType(type)) {
typeNames.unions.push(type.name);
allTypeStrings.push(
`type ${type.name}RootType = ${map(
type.getTypes(),
({ name }) => `${name}RootType`,
" | "
)}`
);
allTypeStrings.push(
`type ${type.name}ReturnType = ${map(
type.getTypes(),
({ name }) => `${name}ReturnType`,
" | "
)}`
);
} else if (isInterfaceType(type)) {
typeNames.interfaces.push(type.name);
eachObj(type.getFields(), (field) => processField(type, field));
} else if (isEnumType(type)) {
typeNames.enums.push(type.name);
allTypeStrings.push(
`export type ${type.name} = ${map(
type.getValues(),
({ value }) => JSON.stringify(value),
" | "
)};`
);
}
});
function fieldDef(typeName: string, field: Field) {
return `${field.name}: {
returnType: ${getReturnType(field)};
args: {
${map(field.arguments, (arg) => {
const name = `${arg.name}${arg.isRequired ? ":" : "?:"}`;
return `${name} ${getArgType(arg)}`;
})}
}
};`;
}
eachObj(interfaceRootTypes, (members, interfaceName) => {
allTypeStrings.push(
`type ${interfaceName}RootType = ${members
.map((name) => `${name}RootType`)
.join(" | ")}`
);
allTypeStrings.push(
`type ${interfaceName}ReturnType = ${members
.map((name) => `${name}ReturnType`)
.join(" | ")}`
);
});
function backingType(name: string) {
return backingTypes[name] || "unknown";
}
const rootTypeImportStrings = Object.keys(rootTypeImports).map((path) => {
return `import {${map(
rootTypeImports[path],
([importName, alias]) => `${importName} as ${alias}`,
", "
)}} from ${path}`;
});
function getArgType(field: Field | Argument) {
let type = "any";
if (field.isInterface) {
type = `${prefix}_Interface_${field.type}['backingType']`;
} else if (field.isType) {
type = `${prefix}_Type_${field.type}['backingType']`;
} else if (field.isScalar && backingTypes[field.type]) {
type = backingTypes[field.type];
} else if (field.isEnum) {
type = `${prefix}Enums["${field.type}"]`;
}
const nullSuffix = field.isRequired ? "" : "| null | undefined";
if (field.isArray) {
return `${type}${"[]".repeat(field.dimensionOfArray)}${nullSuffix}`;
}
return `${type}${nullSuffix}`;
}
return `${TYPEGEN_HEADER}
${rootTypeImportStrings.join("\n")}
function getReturnType(field: Field | Argument) {
let type = "any";
if (field.isInterface) {
type = `${prefix}_Interface_${field.type}['backingType']`;
} else if (field.isType) {
type = `${prefix}_Type_${field.type}['backingType']`;
} else if (field.isScalar && backingTypes[field.type]) {
type = backingTypes[field.type];
} else if (field.isEnum) {
type = `${prefix}Enums["${field.type}"]`;
}
const nullPrefix = field.isRequired ? "" : "null | ";
if (field.isArray) {
const arraySuffix = "[]".repeat(field.dimensionOfArray);
return `${nullPrefix}${type}${arraySuffix} | PromiseLike<${nullPrefix}${type}${arraySuffix}> | PromiseLike<${type}>${arraySuffix}`;
}
return `${nullPrefix}${type} | PromiseLike<${nullPrefix}${type}>`;
}
// Maybe Promise
type ${MP}<T> = PromiseLike<T> | T;
return [{ filename: typesFilePath, content }];
};
// Maybe Promise List
type ${MPL}<T> = ${MP}<T>[];
type Mapper<T> = (item: T, index: number) => string;
// Maybe Thunk
type ${MT}<T> = T | (() => T);
function map<T>(arr: T[], mapper: Mapper<T>, join = "\n") {
return arr
.map(mapper)
.filter((f) => f)
// Maybe Thunk, with args
type ${MTA}<T, A> = T | ((args?: A) => T);
${allTypeStrings.join("\n\n")}
${stringifyTypeFieldMapping("GQLiteralGenArgTypes", argTypeFields)}
interface GQLiteralGenRootTypes {
${map(
typeNames.interfaces.concat(typeNames.objects),
(name) => ` ${name}: ${name}RootType;`
)}
}
${stringifyTypeFieldMapping("GQLiteralGenReturnTypes", returnTypeFields)}
interface GQLiteralGenTypes {
argTypes: GQLiteralGenArgTypes;
rootTypes: GQLiteralGenRootTypes;
returnTypes: GQLiteralGenReturnTypes;
}
export type Gen = GQLiteralGenTypes;
declare global {
interface GQLiteralGen extends GQLiteralGenTypes {}
}
`;
}
function stringifyTypeMapping(tsInterfaceName: string) {}
function stringifyTypeFieldMapping(
tsInterfaceName: string,
obj: Record<string, [string, string][]>
) {
const argTypeLines = Object.keys(obj).reduce((result: string[], typeName) => {
return result
.concat(` ${typeName}: {`)
.concat(
obj[typeName].reduce((fields: string[], [fieldName, mappingName]) => {
return fields.concat(` ${fieldName}: ${mappingName};`);
}, [])
)
.concat(" };");
}, []);
const argTypes = [`interface ${tsInterfaceName} {`]
.concat(argTypeLines)
.concat("}")
.join("\n");
return argTypes;
}
function map<T>(
nodes: Set<T> | Array<T>,
iterator: (item: T, index: number) => string,
join = "\n"
) {
return Array.from(nodes)
.map(iterator)
.join(join);
}
function interfaceFields(prefix: string, t: SchemaTemplateContext["types"][0]) {
if (t.interfaces.length) {
return `extends ${map(
t.interfaces,
(i) => `
${prefix}_Interface_${i}_Fields`,
", "
)}`;
}
return "";
}
function nonInterfaceFields(
ctx: SchemaTemplateContext,
t: SchemaTemplateContext["types"][0]
) {
const allInterfaceFields = new Set<string>();
t.interfaces.forEach((name) => {
const iface = ctx.interfaces.find((i) => i.name === name);
if (iface) {
iface.fields.forEach((field) => {
allInterfaceFields.add(field.name);
});
}
});
return t.fields.filter((field) => !allInterfaceFields.has(field.name));
function ucFirst(fieldName: string) {
return fieldName
.slice(0, 1)
.toUpperCase()
.concat(fieldName.slice(1));
}

View File

@ -8,6 +8,7 @@ import {
GraphQLDirective,
} from "graphql";
import { GQLiteralAbstract } from "./objects";
import { GQLiteralMetadata } from "./metadata";
export enum NodeType {
MIX = "MIX",
@ -74,22 +75,9 @@ export interface EnumMemberConfig {
}
export interface BuildTypes {
types: Record<string, GraphQLNamedType>;
directives: BuildTypesDirectives;
}
export interface BuildTypesDirectives {
definitions: GraphQLDirective[];
uses: { [K in DirectiveLocationEnum]?: DirectiveUse };
hasUses: boolean;
}
export interface DirectiveUse {
location: DirectiveLocationEnum;
typeName: string;
args: [];
argName?: string;
fieldName?: string;
typeMap: Record<string, GraphQLNamedType>;
metadata: GQLiteralMetadata;
directiveMap: Record<string, GraphQLDirective>;
}
/**
@ -263,6 +251,10 @@ export interface ScalarOpts
GraphQLScalarTypeConfig<any, any>,
"description" | "serialize" | "parseValue" | "parseLiteral"
> {
/**
* Backing type for the scalar
*/
typing?: string | ImportedType;
/**
* Any deprecation info for this scalar type
*/
@ -343,6 +335,11 @@ export interface ObjectTypeConfig
*/
interfaces: string[];
/**
* Any modifications to the field config
*/
fieldModifications: Record<string, ModifyFieldOpts<any, any, any>>;
/**
* An (optional) isTypeOf check for the object type
*/
@ -352,7 +349,7 @@ export interface ObjectTypeConfig
* The backing type for this object type. This can
* also be set when constructing the schema.
*/
backingType?: ImportedType;
rootType?: ImportedType | string;
}
export interface AbstractTypeConfig {
@ -380,7 +377,21 @@ export interface InterfaceTypeConfig
resolveType?: TypeResolver<any, any>;
}
export interface SchemaConfig<GenTypes> extends Nullability, DefaultResolver {
export interface ImportedType {
/**
* The name of the imported type. If the type is a
* default export, this should be "default".
*/
name: string;
/**
* The absolute path to import from
* if omitted it's assumed you already
* imported the type, or it's a global.
*/
importPath: string;
}
export interface SchemaConfig<GenTypes> extends Nullability {
/**
* All of the GraphQL types. This is an any for simplicity of developer experience,
* if it's an object we get the values, if it's an array we flatten out the
@ -388,13 +399,36 @@ export interface SchemaConfig<GenTypes> extends Nullability, DefaultResolver {
*/
types: any;
/**
* Absolute path to where the GraphQL IDL file should be written
* Absolute path where the GraphQL IDL file should be written
*/
definitionFilePath: string | false;
schemaPath: string | false;
/**
* Generates the types for Intellisense/TypeScript
* File path where generated types should be saved
*/
typeGeneration?: GQLiteralTypegenOptions<GenTypes>;
typegenPath: string | false;
/**
* Whether the schema & types are generated when the server
* starts. Default is process.env.NODE_ENV !== "production"
*/
shouldGenerateArtifacts?: boolean;
/**
* A map of all types and what fields they can resolve to
*/
rootTypes?: { [K in ObjectNames<GenTypes>]?: ImportedType | string };
/**
* The type of the context for the resolvers
*/
contextType?: ImportedType | string;
/**
* An string or strings to prefix on the header of the TS file
*/
typegenHeader?: string | string[];
/**
* If you want to glob import from a file for use in the backing types,
* you can use this as a convenience by specifying `{ importName: absolutePath }`
* and the import will automatically be resolved relative to the `typegenPath`.
*/
typegenImports?: Record<string, string>;
}
export type NullabilityConfig = {
@ -523,7 +557,7 @@ export type EnumMembers<
export type ObjectTypeDef<GenTypes, TypeName> = GenTypes extends GenTypesShape
? TypeName extends keyof GenTypes["objects"]
? GenTypes["objects"][TypeName]["backingType"]
? GenTypes["objects"][TypeName]["rootType"]
: never
: string;
@ -550,9 +584,9 @@ export type AllOutputTypes<GenTypes> = GenTypes extends GenTypesShape
export type RootValue<GenTypes, TypeName> = GenTypes extends GenTypesShape
? TypeName extends keyof GenTypes["objects"]
? GenTypes["objects"][TypeName]["backingType"]
? GenTypes["objects"][TypeName]["rootType"]
: TypeName extends keyof GenTypes["interfaces"]
? GenTypes["interfaces"][TypeName]["backingType"]
? GenTypes["interfaces"][TypeName]["rootType"]
: any
: never;
@ -597,44 +631,4 @@ export type DirectiveConfig<GenTypes, DirectiveName> = {
args?: [];
};
export interface ImportedType {
/**
* The name of the imported type.
*/
name: string;
/**
* Optional alias for the type name:
*
* import { $name as $alias } from '$absolutePath';
*/
alias?: string;
/**
* The absolute path to import from
* if omitted it's assumed you already
* imported the type, or it's a global.
*/
absolutePath?: string;
}
export interface GQLiteralTypegenOptions<GenTypes> {
/**
* File path where generated types should be saved
*/
typesFilePath: string;
/**
* A map of all types and what fields they can resolve to
*/
backingTypes?: { [K in ObjectNames<GenTypes>]?: string };
/**
* Optional prefix for all fields generated, defaults to "Generated"
*/
typeGenPrefix?: string;
/**
* An array of additional strings to prefix on the header of the TS file
*/
prefix?: string[];
/**
* The type of the context for the resolvers
*/
contextType?: ImportedType;
}
export type RootTypeMap = Record<string, string | ImportedType | undefined>;

View File

@ -1,30 +1,13 @@
import {
GraphQLFieldResolver,
isNamedType,
parse,
visit,
print,
isDirective,
GraphQLSchema,
isObjectType,
} from "graphql";
import { SchemaBuilder } from "./builder";
import * as Types from "./types";
import { GQLiteralTypeWrapper } from "./definitions";
import { GQLiteralAbstract, GQLiteralDirectiveType } from "./objects";
/**
* Builds the types, normalizing the "types" passed into the schema for a
* better developer experience
*/
export function buildTypes(
types: any,
config?: Pick<Types.SchemaConfig<any>, "nullability" | "defaultResolver">
): Types.BuildTypes {
const builder = new SchemaBuilder(config || {});
addTypes(builder, types);
return builder.getFinalTypeMap();
}
const isObject = (obj: any): boolean => obj !== null && typeof obj === "object";
import { GQLiteralAbstract } from "./objects";
export function addMix(
obj: { fields: Types.FieldDefType[] },
@ -46,23 +29,6 @@ export function addMix(
}
}
function addTypes(builder: SchemaBuilder, types: any) {
if (!types) {
return;
}
if (types instanceof GQLiteralTypeWrapper || isNamedType(types)) {
builder.addType(types);
} else if (types instanceof GQLiteralDirectiveType || isDirective(types)) {
builder.addDirective(types);
} else if (Array.isArray(types)) {
types.forEach((typeDef) => addTypes(builder, typeDef));
} else if (isObject(types)) {
Object.keys(types).forEach((key) => {
addTypes(builder, types[key]);
});
}
}
export function withDeprecationComment(description?: string | null) {
return description;
}
@ -89,8 +55,8 @@ export const enumShorthandMembers = (
*/
export const propertyFieldResolver = (
key: string
): GraphQLFieldResolver<any, any> =>
function(source, args, contextValue, info) {
): GraphQLFieldResolver<any, any> => {
return function(source, args, contextValue, info) {
// ensure source is a value for which property access is acceptable.
if (typeof source === "object" || typeof source === "function") {
// TODO: Maybe warn here if key doesn't exist on source?
@ -101,26 +67,7 @@ export const propertyFieldResolver = (
return property;
}
};
/**
* If there are directives defined to be used on the types,
* we need to add these manually to the AST. Directives shouldn't
* be too common, since we're defining the schema programatically
* rather than by hand.
*/
export function addDirectives(
schema: string,
directives: Types.BuildTypesDirectives
) {
if (Object.keys(directives.uses).length > 0) {
return print(
visit(parse(schema), {
// TODO: Add directives
})
);
}
return schema;
}
};
// ----------------------------
@ -209,6 +156,8 @@ function lexicalDistance(aStr: string, bStr: string): number {
return d[aLength][bLength];
}
// ----------------------------
// Borrowed from https://github.com/dmnd/dedent
export function dedent(
@ -260,3 +209,40 @@ export function dedent(
.replace(/\\n/g, "\n")
);
}
// ----------------------------
// Helper Fns
export function arrPush<T, O extends Record<string, T[]>>(
obj: O,
property: string,
value: T
) {
obj[property] = obj[property] || [];
obj[property].push(value);
}
export function objValues<T>(obj: Record<string, T>): T[] {
return Object.keys(obj).reduce((result: T[], key) => {
result.push(obj[key]);
return result;
}, []);
}
export function mapObj<T, R>(
obj: Record<string, T>,
mapper: (val: T, key: string, index: number) => R
) {
return Object.keys(obj).map((key, index) => mapper(obj[key], key, index));
}
export function eachObj<T>(
obj: Record<string, T>,
iter: (val: T, key: string, index: number) => void
) {
Object.keys(obj).forEach((name, i) => iter(obj[name], name, i));
}
export const isObject = (obj: any): boolean =>
obj !== null && typeof obj === "object";

View File

@ -10,7 +10,8 @@
"strict": true,
"skipLibCheck": true,
"declaration": true,
"importHelpers": true
"importHelpers": true,
"sourceMap": true
},
"exclude": ["./examples", "./dist", "./src/__tests__", "./website"]
}

View File

@ -21,6 +21,10 @@
"title": "GQLiteral",
"sidebar_label": "Getting Started"
},
"type-generation-details": {
"title": "Type Generation Details",
"sidebar_label": "Type Generation Details"
},
"typescript-setup": {
"title": "TypeScript Configuration",
"sidebar_label": "Use with TypeScript"

View File

@ -58,11 +58,10 @@ export const Playground: React.SFC<PlaygroundProps> = (props) => {
},
scrollBeyondLastLine: false,
});
editor.onDidChangeModelContent(
debounce(() => {
setContent(editor.getValue());
}, 100)
);
const debouncedChange = debounce(() => {
setContent(editor.getValue());
}, 100);
editor.onDidChangeModelContent(debouncedChange as any);
return () => editor.dispose();
}
}, []);
@ -172,9 +171,9 @@ function getCurrentSchema(code): SchemaOrError {
);
const schema = GQLiteralSchema({
types: cache,
definitionFilePath: false,
schemaFilePath: false,
typeGeneration: {
typesFilePath: "file:///index.ts",
outputPath: "file:///index.ts",
},
});
return { schema, error: null };
@ -206,7 +205,6 @@ const allTypeDefs = [
require("raw-loader!gqliteral/dist/definitions.d.ts"),
require("raw-loader!gqliteral/dist/index.d.ts"),
require("raw-loader!gqliteral/dist/objects.d.ts"),
require("raw-loader!gqliteral/dist/typegen.d.ts"),
require("raw-loader!gqliteral/dist/types.d.ts"),
require("raw-loader!gqliteral/dist/utils.d.ts"),
];
@ -225,7 +223,6 @@ const files = [
"gqliteral/definitions.d.ts",
"gqliteral/index.d.ts",
"gqliteral/objects.d.ts",
"gqliteral/typegen.d.ts",
"gqliteral/types.d.ts",
"gqliteral/utils.d.ts",
];

View File

@ -3,7 +3,7 @@
"Documentation": [
"getting-started",
"best-practices",
"typescript-setup",
"type-generation-details",
"api-reference",
"faq"
]

View File

@ -93,6 +93,7 @@ const siteConfig = {
// You may provide arbitrary config keys to be used as needed by your
// template. For example, if you need your repo's URL...
// repoUrl: 'https://github.com/facebook/test-site',
separateCss: ["static/separate-css"],
};
module.exports = siteConfig;

File diff suppressed because it is too large Load Diff